<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>飘逸峰</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://blog.hanqunfeng.com/</id>
  <link href="https://blog.hanqunfeng.com/" rel="alternate"/>
  <link href="https://blog.hanqunfeng.com/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, 飘逸峰</rights>
  <subtitle>Spring--Java程序员的春天</subtitle>
  <title>飘逸峰的博客</title>
  <updated>2026-07-17T09:10:00.842Z</updated>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="微服务" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    <category term="spring-cloud-alibaba" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/%E5%BE%AE%E6%9C%8D%E5%8A%A1/spring-cloud-alibaba/"/>
    <category term="spring-cloud-alibaba" scheme="https://blog.hanqunfeng.com/tags/spring-cloud-alibaba/"/>
    <category term="sentinel" scheme="https://blog.hanqunfeng.com/tags/sentinel/"/>
    <content>
      <![CDATA[<!-- **加粗** *斜体* ***加粗并斜体*** ~~删除线~~ ==突出显示== `突出显示(推荐)` ++下划线++ ~下标~ ^上标^ 脚注，参考文献[^1]，然后在文档最下方要添加这个1对应的内容，如：[^1]: My reference. 图片设置宽度和高度(通过uPic上传后，需要将upic修改过为blog，用于添加水印) ![](https://upic-oss.oss-cn-beijing.aliyuncs.com/blog/innodb_buffer_pool.png =900x600) +++ **点击折叠** 这是被隐藏的内容 +++::: tips success warning danger这里是容器内的内容:::% note info % success warning danger这里是容器内的内容% endnote %引用本地其它文章连接{} 大括号开始% post_link 文件名称(不包含.md) %大括号结束 --><h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2"><p>依据 <a href="https://sca.aliyun.com/docs/2025.x/overview/version-explain/">Spring Cloud Alibaba 版本发布说明</a>，搭建与 <strong>Spring Cloud Alibaba 2025.0.0.0</strong> 对应的 <strong>Sentinel Dashboard 1.8.9</strong>。</p></li><li class="lvl-2"><p>本文侧重 Dashboard <strong>单机</strong>搭建（JAR / Docker / systemd），并补充应用侧接入、<strong>OpenFeign / RestTemplate / Gateway</strong> 适配，以及 Nacos 规则持久化。</p></li><li class="lvl-2"><p>生产推荐：规则持久化到 <strong>Nacos</strong>（Push 模式）；<strong>Dashboard 可选</strong>（仅监控时需要），限流生效不依赖控制台。</p></li><li class="lvl-2"><p>同版本配套的 Nacos Server 见 <a href="/2026/07/14/sca-2025-nacos-install/" title="Spring Cloud Alibaba 2025.0.0.0 配套搭建 Nacos 3.0.3">Spring Cloud Alibaba 2025.0.0.0 配套搭建 Nacos 3.0.3</a>。</p></li></ul><span id="more"></span><h2 id="一、版本对照">一、版本对照</h2><h3 id="1-Spring-Cloud-Alibaba-2025-0-x-适配关系">1. Spring Cloud Alibaba 2025.0.x 适配关系</h3><p>摘自官方 <a href="https://sca.aliyun.com/docs/2025.x/overview/version-explain/">版本发布说明</a>：</p><table><thead><tr><th>Spring Cloud Alibaba Version</th><th>Spring Cloud Version</th><th>Spring Boot Version</th></tr></thead><tbody><tr><td>2025.0.0.0</td><td>2025.0.0</td><td>3.5.0</td></tr></tbody></table><p>同页「组件版本关系」中，与 <code>2025.0.0.0</code> 对应的 Sentinel 版本为：</p><table><thead><tr><th>Spring Cloud Alibaba Version</th><th>Sentinel Version</th></tr></thead><tbody><tr><td>2025.0.0.0</td><td>1.8.9</td></tr></tbody></table><blockquote><p>对比：同属 2025.x 的 <code>2025.1.0.0</code> 亦使用 Sentinel <code>1.8.9</code>，服务端 Dashboard 版本可共用。</p></blockquote><h3 id="2-环境准备">2. 环境准备</h3><table><thead><tr><th>项目</th><th>要求说明</th></tr></thead><tbody><tr><td>Sentinel Dashboard 1.8.9</td><td><strong>JDK 1.8+</strong> 即可（也可直接用 JDK 17）</td></tr><tr><td>Docker（可选）</td><td>Docker 20+</td></tr></tbody></table><p>端口规划（与 Nacos 同机时建议错开控制台端口）：</p><table><thead><tr><th>用途</th><th>端口</th><th>说明</th></tr></thead><tbody><tr><td>Sentinel Dashboard</td><td><code>8858</code></td><td>本文示例端口；默认官方示例常用 <code>8080</code>，易与 Nacos 3.x 控制台冲突</td></tr></tbody></table><hr><h2 id="二、搭建-Sentinel-Dashboard-1-8-9">二、搭建 Sentinel Dashboard 1.8.9</h2><p>官方发布包：<a href="https://github.com/alibaba/Sentinel/releases/tag/1.8.9">Sentinel v1.8.9 Release</a>（资源文件 <code>sentinel-dashboard-1.8.9.jar</code>）。<br>控制台说明见 <a href="https://github.com/alibaba/Sentinel/wiki/Dashboard">Sentinel Wiki · Dashboard</a>。</p><h3 id="1-方式一：JAR-直接启动（推荐）">1. 方式一：JAR 直接启动（推荐）</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></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p /usr/local/sentinel &amp;&amp; <span class="built_in">cd</span> /usr/local/sentinel</span><br><span class="line"></span><br><span class="line"><span class="comment"># 跟随请求重定向</span></span><br><span class="line">curl -L https://github.com/alibaba/Sentinel/releases/download/1.8.9/sentinel-dashboard-1.8.9.jar</span><br><span class="line"></span><br><span class="line"><span class="comment"># 使用 8858，避免与 Nacos 控制台 8080 冲突</span></span><br><span class="line"><span class="built_in">nohup</span> java \</span><br><span class="line">  -Dserver.port=8858 \</span><br><span class="line">  -Dcsp.sentinel.dashboard.server=10.10.2.45:8858 \</span><br><span class="line">  -Dproject.name=sentinel-dashboard \</span><br><span class="line">  -jar sentinel-dashboard-1.8.9.jar \</span><br><span class="line">  &gt; sentinel-dashboard.log 2&gt;&amp;1 &amp;</span><br></pre></td></tr></table></figure><p>访问：<code>http://127.0.0.1:8858</code><br>默认账号 / 密码：<code>sentinel</code> / <code>sentinel</code>（生产务必修改）。</p><h3 id="2-方式二：Docker-启动">2. 方式二：Docker 启动</h3><p>官方在 <code>sentinel-dashboard</code> 目录提供 Dockerfile（<code>1.8.9</code> / <code>1.8</code> 分支均有）。镜像构建时会从 GitHub Release 再下载 <code>sentinel-dashboard-1.8.9.jar</code>，因此构建机需能访问 GitHub；网络不稳时优先用上文「方式一」官方 JAR。</p><blockquote><p><strong>构建前必读</strong></p><ol><li class="lvl-3"><strong>基础镜像失效</strong>：官方 Dockerfile 的基础镜像 <code>openjdk:8-jre-slim</code> 已在 Docker Hub 上失效，无论用下面（1）（2）（3）哪种方式，都需先修改 <code>Dockerfile</code> 中的 <code>FROM</code>（例如改为 <code>openjdk:8u212-jre-slim</code>），再执行 <code>docker build</code>。具体替换命令与 slim / 非 slim 说明见下文 <strong>（3）</strong>。</li><li class="lvl-3"><strong>GitHub 下载失败</strong>：官方 Dockerfile 在构建阶段会通过 <code>curl</code> 访问 <code>release-assets.githubusercontent.com</code> 拉取 JAR。若出现 <code>curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL</code>，说明 <strong>Docker 构建网络无法稳定访问 GitHub Release</strong>（国内环境较常见）。此时请改用下文 <strong>（3.1）宿主机预下载 JAR</strong>，或直接用上文「方式一」JAR 启动。</li></ol></blockquote><p><code>docker build</code> 的上下文必须是含 <code>Dockerfile</code> 的 <code>sentinel-dashboard</code> 目录，任选下面一种准备源码：</p><h4 id="（1）完整克隆（最简单）">（1）完整克隆（最简单）</h4><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></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/alibaba/Sentinel.git</span><br><span class="line"><span class="built_in">cd</span> Sentinel</span><br><span class="line">git checkout 1.8.9</span><br><span class="line"></span><br><span class="line">docker build \</span><br><span class="line">  --build-arg SENTINEL_VERSION=1.8.9 \</span><br><span class="line">  -t sentinel-dashboard:1.8.9 \</span><br><span class="line">  sentinel-dashboard</span><br></pre></td></tr></table></figure><h4 id="（2）稀疏克隆（只取-sentinel-dashboard，省流量）">（2）稀疏克隆（只取 <code>sentinel-dashboard</code>，省流量）</h4><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">git <span class="built_in">clone</span> --filter=blob:none --sparse \</span><br><span class="line">  https://github.com/alibaba/Sentinel.git</span><br><span class="line"><span class="built_in">cd</span> Sentinel</span><br><span class="line">git sparse-checkout <span class="built_in">set</span> sentinel-dashboard</span><br><span class="line">git checkout 1.8.9</span><br><span class="line"></span><br><span class="line">docker build \</span><br><span class="line">  --build-arg SENTINEL_VERSION=1.8.9 \</span><br><span class="line">  -t sentinel-dashboard:1.8.9 \</span><br><span class="line">  sentinel-dashboard</span><br></pre></td></tr></table></figure><h4 id="（3）不克隆仓库，只下载-Dockerfile">（3）不克隆仓库，只下载 Dockerfile</h4><p>官方 Dockerfile 本质是拉取 Release JAR，也可单独下载该文件后构建。</p><blockquote><p>注意：官方 Dockerfile 的基础镜像为 <code>openjdk:8-jre-slim</code>，该标签在 Docker Hub 上已不可用（<code>docker pull</code> 会报 <code>not found</code>）。下载后需改为仍可拉取的标签，例如 <code>openjdk:8u212-jre-slim</code>（与本节开头「构建前必读」一致）。</p></blockquote><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></pre></td><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> -p sentinel-dashboard &amp;&amp; <span class="built_in">cd</span> sentinel-dashboard</span><br><span class="line">curl -fsSL -o Dockerfile \</span><br><span class="line">  https://raw.githubusercontent.com/alibaba/Sentinel/1.8.9/sentinel-dashboard/Dockerfile</span><br><span class="line"></span><br><span class="line"><span class="comment"># 将已失效的 openjdk:8-jre-slim 替换为可用镜像</span></span><br><span class="line">sed -i.bak <span class="string">&#x27;s|FROM openjdk:8-jre-slim|FROM openjdk:8u212-jre-slim|&#x27;</span> Dockerfile</span><br><span class="line"><span class="comment"># macOS / Linux 通用；也可用编辑器手动改 FROM 那一行</span></span><br><span class="line"></span><br><span class="line">docker build \</span><br><span class="line">  --build-arg SENTINEL_VERSION=1.8.9 \</span><br><span class="line">  -t sentinel-dashboard:1.8.9 \</span><br><span class="line">  .</span><br></pre></td></tr></table></figure><p>修改后的关键行应类似：</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> openjdk:<span class="number">8</span>u212-jre-slim</span><br></pre></td></tr></table></figure><blockquote><p>PS：<code>openjdk:8-*-slim</code> 与不带 <code>slim</code> 的镜像区别</p><ul class="lvl-1"><li class="lvl-2"><strong>带 <code>slim</code></strong>：基于 Debian slim 精简根文件系统，体积更小、攻击面更小，适合只跑 JRE 的生产镜像；但预装工具更少（如部分调试命令、额外字体/locale 可能缺失）。</li><li class="lvl-2"><strong>不带 <code>slim</code>（如 <code>openjdk:8u212-jre</code>）</strong>：完整 Debian 基础层，体积更大，调试与兼容性通常更好，一般用于开发排查或对系统库有额外依赖的场景。<br>Sentinel Dashboard 仅依赖 JRE 跑 JAR，优先用 <code>slim</code> 即可；若构建或运行时缺库，再改用不带 <code>slim</code> 的同版本标签。</li></ul></blockquote><h4 id="（4）构建阶段-GitHub-下载失败（SSL-ERROR-SYSCALL）">（4）构建阶段 GitHub 下载失败（<code>SSL_ERROR_SYSCALL</code>）</h4><p>若 <code>docker build</code> 在 <code>installer</code> 阶段报错类似：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to release-assets.githubusercontent.com:443</span><br></pre></td></tr></table></figure><p>说明官方多阶段 Dockerfile 在 <strong>容器内</strong> 拉取 GitHub Release 失败。可在 <strong>宿主机</strong> 先下载 JAR，再改用本地 <code>COPY</code>，避免构建时再访问 GitHub：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><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="built_in">mkdir</span> -p sentinel-dashboard &amp;&amp; <span class="built_in">cd</span> sentinel-dashboard</span><br><span class="line"></span><br><span class="line"><span class="comment"># 在宿主机下载 JAR（可用代理、镜像站，或从内网机器 scp 过来）</span></span><br><span class="line">wget https://github.com/alibaba/Sentinel/releases/download/1.8.9/sentinel-dashboard-1.8.9.jar</span><br><span class="line"></span><br><span class="line"><span class="built_in">cat</span> &gt; Dockerfile &lt;&lt;<span class="string">&#x27;EOF&#x27;</span></span><br><span class="line">FROM openjdk:8u212-jre-slim</span><br><span class="line"></span><br><span class="line">COPY sentinel-dashboard-1.8.9.jar /home/sentinel-dashboard.jar</span><br><span class="line"></span><br><span class="line">ENV JAVA_OPTS <span class="string">&#x27;-Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080&#x27;</span></span><br><span class="line"></span><br><span class="line">RUN <span class="built_in">chmod</span> -R +x /home/sentinel-dashboard.jar</span><br><span class="line"></span><br><span class="line">EXPOSE 8080</span><br><span class="line"></span><br><span class="line">CMD java <span class="variable">$&#123;JAVA_OPTS&#125;</span> -jar /home/sentinel-dashboard.jar</span><br><span class="line">EOF</span><br><span class="line"></span><br><span class="line">docker build -t sentinel-dashboard:1.8.9 .</span><br></pre></td></tr></table></figure><p>若宿主机同样无法访问 GitHub，可在一台能下载的机器上取回 <code>sentinel-dashboard-1.8.9.jar</code> 后拷贝到构建目录，再执行 <code>docker build</code>。若不需要容器化，直接用上文「方式一」JAR 启动更简单。</p><h4 id="（5）运行容器">（5）运行容器</h4><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">docker run -d \</span><br><span class="line">  --name sentinel-dashboard \</span><br><span class="line">  -p 8858:8080 \</span><br><span class="line">  -e JAVA_OPTS=<span class="string">&#x27;-Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080&#x27;</span> \</span><br><span class="line">  sentinel-dashboard:1.8.9</span><br></pre></td></tr></table></figure><p>访问宿主机：<code>http://127.0.0.1:8858</code>。</p><p>社区也有第三方镜像（如 <code>bladex/sentinel-dashboard:1.8.9</code>），使用前请确认来源可信；本文优先官方 JAR。</p><h3 id="3-systemd-托管示例（可选）">3. systemd 托管示例（可选）</h3><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># /etc/systemd/system/sentinel-dashboard.service</span></span><br><span class="line"><span class="section">[Unit]</span></span><br><span class="line"><span class="attr">Description</span>=Sentinel Dashboard <span class="number">1.8</span>.<span class="number">9</span></span><br><span class="line"><span class="attr">After</span>=network.target</span><br><span class="line"></span><br><span class="line"><span class="section">[Service]</span></span><br><span class="line"><span class="attr">Type</span>=simple</span><br><span class="line"><span class="attr">User</span>=root</span><br><span class="line"><span class="attr">WorkingDirectory</span>=/usr/local/sentinel</span><br><span class="line"><span class="attr">ExecStart</span>=/usr/bin/java -Dserver.port=<span class="number">8858</span> -Dcsp.sentinel.dashboard.server=<span class="number">127.0</span>.<span class="number">0.1</span>:<span class="number">8858</span> -Dproject.name=sentinel-dashboard -jar /usr/local/sentinel/sentinel-dashboard-<span class="number">1.8</span>.<span class="number">9</span>.jar</span><br><span class="line"><span class="attr">Restart</span>=<span class="literal">on</span>-failure</span><br><span class="line"><span class="attr">RestartSec</span>=<span class="number">5</span></span><br><span class="line"></span><br><span class="line"><span class="section">[Install]</span></span><br><span class="line"><span class="attr">WantedBy</span>=multi-user.target</span><br></pre></td></tr></table></figure><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">systemctl daemon-reload</span><br><span class="line">systemctl <span class="built_in">enable</span> --now sentinel-dashboard</span><br><span class="line">systemctl status sentinel-dashboard</span><br></pre></td></tr></table></figure><hr><h2 id="三、应用侧快速接入（SCA-2025-0-0-0）">三、应用侧快速接入（SCA 2025.0.0.0）</h2><p>官方指南：<a href="https://sca.aliyun.com/docs/2025.x/user-guide/sentinel/quick-start/">SCA · Sentinel 快速开始</a>。</p><h3 id="1-BOM-与依赖">1. BOM 与依赖</h3><p>父 POM / 依赖管理：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependencyManagement</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-dependencies<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.5.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-dependencies<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">version</span>&gt;</span>2025.0.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-alibaba-dependencies<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">version</span>&gt;</span>2025.0.0.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencyManagement</span>&gt;</span></span><br></pre></td></tr></table></figure><p>业务模块引入：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-sentinel<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><blockquote><p>Starter 会传递依赖 <code>sentinel-* :1.8.9</code>，与上文 Dashboard 版本对齐。</p></blockquote><h3 id="2-application-yml-示例">2. <code>application.yml</code> 示例</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">server:</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">18082</span></span><br><span class="line"></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">application:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">demo-provider</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">sentinel:</span></span><br><span class="line">      <span class="attr">transport:</span></span><br><span class="line">        <span class="attr">dashboard:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span><span class="string">:8858</span></span><br><span class="line">        <span class="comment"># 客户端供 Dashboard 回调的本地端口，默认 8719，冲突时可改</span></span><br><span class="line">        <span class="comment"># port: 8719</span></span><br><span class="line">      <span class="attr">eager:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><h3 id="3-代码最小示例">3. 代码最小示例</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RestController</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestController</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/hello&quot;)</span></span><br><span class="line">    <span class="meta">@SentinelResource(&quot;hello&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">hello</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;Hello Sentinel&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>应用启动并访问一次受保护接口后，打开 Sentinel 控制台，应能看到应用名 <code>demo-provider</code> 及资源 <code>hello</code>，随后可在控制台配置流控规则验证限流。</p><div class="note warning"><p>仅在控制台「新增流控规则」时，规则默认只推到客户端<strong>内存</strong>。应用重启或 Dashboard 重启后规则会丢。生产请接下文 <strong>「五、单台 Dashboard + Nacos 规则持久化」</strong>；本地联调可先用手写控制台规则快速验证。</p></div><hr><h2 id="四、客户端组件适配（OpenFeign-RestTemplate-Gateway）">四、客户端组件适配（OpenFeign / RestTemplate / Gateway）</h2><p><code>spring-cloud-starter-alibaba-sentinel</code> 对 Spring Cloud 生态常见 HTTP 客户端做了适配。完整说明见 <a href="https://sca.aliyun.com/docs/2025.x/user-guide/sentinel/advanced-guide/">SCA · Sentinel 进阶指南 · 客户端支持</a>。</p><blockquote><p>Servlet 侧的 <code>spring.cloud.sentinel.filter.*</code>、<code>servlet.block-page</code> 等配置对 <strong>OpenFeign / RestTemplate 不生效</strong>，需按下面各自方式配置限流与降级处理。</p></blockquote><h3 id="1-OpenFeign">1. OpenFeign</h3><p>依赖：Sentinel Starter + OpenFeign（版本由 BOM 管理）：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-sentinel<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-openfeign<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>需显式打开开关：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">feign:</span></span><br><span class="line">  <span class="attr">sentinel:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>示例：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><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">@FeignClient(</span></span><br><span class="line"><span class="meta">    name = &quot;service-provider&quot;,</span></span><br><span class="line"><span class="meta">    fallback = EchoServiceFallback.class,</span></span><br><span class="line"><span class="meta">    configuration = FeignConfiguration.class</span></span><br><span class="line"><span class="meta">)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">EchoService</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@GetMapping(&quot;/echo/&#123;str&#125;&quot;)</span></span><br><span class="line">    String <span class="title function_">echo</span><span class="params">(<span class="meta">@PathVariable(&quot;str&quot;)</span> String str)</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">FeignConfiguration</span> &#123;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> EchoServiceFallback <span class="title function_">echoServiceFallback</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">EchoServiceFallback</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="keyword">class</span> <span class="title class_">EchoServiceFallback</span> <span class="keyword">implements</span> <span class="title class_">EchoService</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> String <span class="title function_">echo</span><span class="params">(<span class="meta">@PathVariable(&quot;str&quot;)</span> String str)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;echo fallback&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>要点：</p><table><thead><tr><th>项</th><th>说明</th></tr></thead><tbody><tr><td>开关</td><td>默认不启用，必须 <code>feign.sentinel.enabled=true</code></td></tr><tr><td>资源名</td><td><code>httpmethod:protocol://requesturl</code>；上例 <code>echo</code> 为 <code>GET:http://service-provider/echo/&#123;str&#125;</code></td></tr><tr><td><code>@FeignClient</code></td><td>注解属性（含 <code>fallback</code> / <code>fallbackFactory</code> 等）Sentinel 均兼容</td></tr><tr><td>规则配置</td><td>对上述资源名配 <code>flow</code> / <code>degrade</code> 即可（可写在 Nacos，见第五节）</td></tr></tbody></table><h3 id="2-RestTemplate">2. RestTemplate</h3><p>对 <code>RestTemplate</code> Bean 加 <code>@SentinelRestTemplate</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="meta">@SentinelRestTemplate(</span></span><br><span class="line"><span class="meta">    blockHandler = &quot;handleException&quot;,</span></span><br><span class="line"><span class="meta">    blockHandlerClass = ExceptionUtil.class</span></span><br><span class="line"><span class="meta">)</span></span><br><span class="line"><span class="keyword">public</span> RestTemplate <span class="title function_">restTemplate</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RestTemplate</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>blockHandler</code> / <code>fallback</code> 对应方法必须是 <code>blockHandlerClass</code> / <code>fallbackClass</code> 中的 <strong>静态方法</strong>；参数、返回值与 <code>ClientHttpRequestInterceptor#intercept</code> 一致，并多一个 <code>BlockException</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> com.alibaba.cloud.sentinel.rest.SentinelClientHttpResponse;</span><br><span class="line"><span class="keyword">import</span> com.alibaba.csp.sentinel.slots.block.BlockException;</span><br><span class="line"><span class="keyword">import</span> org.springframework.http.HttpRequest;</span><br><span class="line"><span class="keyword">import</span> org.springframework.http.client.ClientHttpRequestExecution;</span><br><span class="line"><span class="keyword">import</span> org.springframework.http.client.ClientHttpResponse;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ExceptionUtil</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> ClientHttpResponse <span class="title function_">handleException</span><span class="params">(</span></span><br><span class="line"><span class="params">            HttpRequest request,</span></span><br><span class="line"><span class="params">            <span class="type">byte</span>[] body,</span></span><br><span class="line"><span class="params">            ClientHttpRequestExecution execution,</span></span><br><span class="line"><span class="params">            BlockException exception)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">SentinelClientHttpResponse</span>(<span class="string">&quot;blocked by sentinel&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>要点：</p><table><thead><tr><th>项</th><th>说明</th></tr></thead><tbody><tr><td>注解属性</td><td><code>blockHandler</code> + <code>blockHandlerClass</code>（限流）；<code>fallback</code> + <code>fallbackClass</code>（降级）；均可选</td></tr><tr><td>启动校验</td><td>声明了 handler 但方法不存在时，<strong>应用启动直接失败</strong></td></tr><tr><td>未配 handler</td><td>被限流 / 熔断时默认返回类似 <code>RestTemplate request block by sentinel</code></td></tr><tr><td>资源名粒度</td><td>① <code>GET:https://host:port/path</code>（含路径）② <code>GET:https://host:port</code>（仅主机端口）</td></tr></tbody></table><p>例：<code>GET https://www.taobao.com/test</code> 对应资源名可为 <code>GET:https://www.taobao.com/test</code> 或 <code>GET:https://www.taobao.com</code>。</p><h3 id="3-Spring-Cloud-Gateway（简述）">3. Spring Cloud Gateway（简述）</h3><p>网关限流需额外依赖（与普通业务服务的 Starter 组合使用）：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-sentinel<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-alibaba-sentinel-gateway<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-gateway<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>规则类型使用 <code>gw-flow</code> / <code>gw-api-group</code>（见第五节对照表）。熔断后的响应可通过 <code>spring.cloud.sentinel.scg.fallback.*</code> 配置（<code>mode</code>=<code>redirect</code> | <code>response</code> 等），详见 <a href="https://sca.aliyun.com/docs/2025.x/user-guide/sentinel/advanced-guide/">进阶指南</a> 与 <a href="https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81">Sentinel 网关限流</a>。</p><hr><h2 id="五、单台-Dashboard-Nacos-规则持久化">五、单台 Dashboard + Nacos 规则持久化</h2><p>官方 Dashboard <strong>不提供集群</strong>，生产更务实的做法是：把规则落到 <strong>Nacos 配置中心</strong>（Push 模式）。应用启动后从 Nacos 拉取规则并监听变更，重启不丢。单台 Dashboard 仅作可选监控面板。动态数据源说明见 <a href="https://sca.aliyun.com/docs/2025.x/user-guide/sentinel/advanced-guide/">SCA · Sentinel 进阶指南</a>。</p><p>前提：已按 <a href="/2026/07/14/sca-2025-nacos-install/" title="Spring Cloud Alibaba 2025.0.0.0 配套搭建 Nacos 3.0.3">Spring Cloud Alibaba 2025.0.0.0 配套搭建 Nacos 3.0.3</a> 跑通 Nacos <code>3.0.3</code>（含鉴权账号密码）。</p><h3 id="1-架构说明">1. 架构说明</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">Nacos Config  ─────────────────────►  业务应用（Sentinel Client 内存）</span><br><span class="line">（规则真源）                                   │</span><br><span class="line">     ▲                                         │ 控制台打开规则页时查询客户端（可选）</span><br><span class="line">     │ 人工维护配置（推荐）                      ▼</span><br><span class="line">Nacos 控制台                        Sentinel Dashboard（单台，可选）</span><br><span class="line">                                        监控、看板；开箱不写回 Nacos</span><br></pre></td></tr></table></figure><table><thead><tr><th>角色</th><th>职责</th></tr></thead><tbody><tr><td>Nacos</td><td>规则持久化与多实例一致下发（<strong>真源</strong>）</td></tr><tr><td>业务应用</td><td>通过 <code>sentinel-datasource-nacos</code> 订阅规则并写入本地内存生效</td></tr><tr><td>Sentinel Dashboard</td><td><strong>可选</strong>；集群拓扑、实时监控；打开规则页时向客户端拉取展示；开箱不写回 Nacos</td></tr></tbody></table><h4 id="同步方向（务必分清）">同步方向（务必分清）</h4><table><thead><tr><th>方向</th><th>是否自动</th><th>实际链路</th></tr></thead><tbody><tr><td>改 Nacos → 业务应用</td><td><strong>会</strong></td><td>Nacos 推送 / 监听 → 客户端内存，限流立刻生效</td></tr><tr><td>改 Nacos → Sentinel 控制台</td><td><strong>不是直推</strong></td><td>Nacos → 应用内存 →（控制台查客户端）→ 页面展示。控制台没有单独一份从 Nacos 同步来的规则库</td></tr><tr><td>改控制台 → Nacos</td><td><strong>不会</strong></td><td>开箱 Dashboard 只推到应用内存，<strong>不写回</strong> Nacos</td></tr><tr><td>改控制台 → 业务应用</td><td>会（临时）</td><td>写入客户端内存；重启后仍以 Nacos 为准，或下次 Nacos 发布后被覆盖</td></tr></tbody></table><p>一句话：<strong>真源永远是 Nacos，不是控制台。</strong><br>Nacos 改了会自动到应用，控制台打开规则页时通常也能查到；控制台改了不会进 Nacos。两边一起改容易互相覆盖，持久化启用后请统一在 Nacos 维护。</p><h4 id="Dashboard-是否必须启动？">Dashboard 是否必须启动？</h4><p><strong>不必须。</strong> 只保证限流 / 熔断生效时，可以不启 Sentinel Dashboard：</p><ul class="lvl-0"><li class="lvl-2"><p><code>sentinel-datasource-nacos</code> 在应用启动时从 Nacos 拉规则并监听变更，与 Dashboard <strong>无关</strong></p></li><li class="lvl-2"><p>不配 / 不启控制台时，流控、熔断照常工作</p></li><li class="lvl-2"><p><code>spring.cloud.sentinel.transport.dashboard</code> 也可去掉或先注释掉</p></li></ul><p>Dashboard 只是<strong>可选运维面板</strong>（实时 QPS、机器列表、簇点链路等）。需要临时看监控时再启一台，把 <code>transport.dashboard</code> 指过去即可；<strong>规则仍以 Nacos 为准</strong>。</p><p>% note tip %<br>若强依赖「控制台点几下就落库」，需二次开发 Dashboard（源码自带 Nacos 推拉示例，社区亦有改版包），本文不展开，优先 Nacos 直配。<br>% endnote %</p><h3 id="2-应用依赖">2. 应用依赖</h3><p>在已有 <code>spring-cloud-starter-alibaba-sentinel</code> 之外，再引入 Nacos 数据源（版本由 SCA BOM 管理）：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.csp<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>sentinel-datasource-nacos<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><blockquote><p>不必单独声明 <code>nacos-client</code> 版本；与同体系 Nacos <code>3.0.3</code> 对齐即可。若项目已引入 <code>spring-cloud-starter-alibaba-nacos-discovery</code> / <code>config</code>，账号密码等可复用。</p></blockquote><h3 id="3-application-yml-示例">3. <code>application.yml</code> 示例</h3><p>在第三节配置基础上增加 <code>datasource</code>（可并存多种 <code>rule-type</code>，<strong>每种对应一个 Nacos DataId</strong>）。下面一次挂上常用五类，按需删减即可：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><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="attr">server:</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">18082</span></span><br><span class="line"></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">application:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">demo-provider</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">sentinel:</span></span><br><span class="line">      <span class="comment"># 仅做监控时需要；纯 Nacos 规则持久化可不配 / 不启 Dashboard</span></span><br><span class="line">      <span class="attr">transport:</span></span><br><span class="line">        <span class="attr">dashboard:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span><span class="string">:8858</span></span><br><span class="line">      <span class="attr">eager:</span> <span class="literal">true</span></span><br><span class="line">      <span class="attr">datasource:</span></span><br><span class="line">        <span class="attr">flow:</span></span><br><span class="line">          <span class="attr">nacos:</span></span><br><span class="line">            <span class="attr">server-addr:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span><span class="string">:8848</span></span><br><span class="line">            <span class="attr">username:</span> <span class="string">nacos</span></span><br><span class="line">            <span class="attr">password:</span> <span class="string">YourStrongPassword</span></span><br><span class="line">            <span class="attr">namespace:</span>                   <span class="comment"># 留空即 public；与 Nacos 控制台所选命名空间一致</span></span><br><span class="line">            <span class="attr">data-id:</span> <span class="string">$&#123;spring.application.name&#125;-flow-rules</span></span><br><span class="line">            <span class="attr">group-id:</span> <span class="string">SENTINEL_GROUP</span></span><br><span class="line">            <span class="attr">data-type:</span> <span class="string">json</span></span><br><span class="line">            <span class="attr">rule-type:</span> <span class="string">flow</span></span><br><span class="line">        <span class="attr">degrade:</span></span><br><span class="line">          <span class="attr">nacos:</span></span><br><span class="line">            <span class="attr">server-addr:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span><span class="string">:8848</span></span><br><span class="line">            <span class="attr">username:</span> <span class="string">nacos</span></span><br><span class="line">            <span class="attr">password:</span> <span class="string">YourStrongPassword</span></span><br><span class="line">            <span class="attr">data-id:</span> <span class="string">$&#123;spring.application.name&#125;-degrade-rules</span></span><br><span class="line">            <span class="attr">group-id:</span> <span class="string">SENTINEL_GROUP</span></span><br><span class="line">            <span class="attr">data-type:</span> <span class="string">json</span></span><br><span class="line">            <span class="attr">rule-type:</span> <span class="string">degrade</span></span><br><span class="line">        <span class="attr">authority:</span></span><br><span class="line">          <span class="attr">nacos:</span></span><br><span class="line">            <span class="attr">server-addr:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span><span class="string">:8848</span></span><br><span class="line">            <span class="attr">username:</span> <span class="string">nacos</span></span><br><span class="line">            <span class="attr">password:</span> <span class="string">YourStrongPassword</span></span><br><span class="line">            <span class="attr">data-id:</span> <span class="string">$&#123;spring.application.name&#125;-authority-rules</span></span><br><span class="line">            <span class="attr">group-id:</span> <span class="string">SENTINEL_GROUP</span></span><br><span class="line">            <span class="attr">data-type:</span> <span class="string">json</span></span><br><span class="line">            <span class="attr">rule-type:</span> <span class="string">authority</span></span><br><span class="line">        <span class="attr">system:</span></span><br><span class="line">          <span class="attr">nacos:</span></span><br><span class="line">            <span class="attr">server-addr:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span><span class="string">:8848</span></span><br><span class="line">            <span class="attr">username:</span> <span class="string">nacos</span></span><br><span class="line">            <span class="attr">password:</span> <span class="string">YourStrongPassword</span></span><br><span class="line">            <span class="attr">data-id:</span> <span class="string">$&#123;spring.application.name&#125;-system-rules</span></span><br><span class="line">            <span class="attr">group-id:</span> <span class="string">SENTINEL_GROUP</span></span><br><span class="line">            <span class="attr">data-type:</span> <span class="string">json</span></span><br><span class="line">            <span class="attr">rule-type:</span> <span class="string">system</span></span><br><span class="line">        <span class="attr">param-flow:</span></span><br><span class="line">          <span class="attr">nacos:</span></span><br><span class="line">            <span class="attr">server-addr:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span><span class="string">:8848</span></span><br><span class="line">            <span class="attr">username:</span> <span class="string">nacos</span></span><br><span class="line">            <span class="attr">password:</span> <span class="string">YourStrongPassword</span></span><br><span class="line">            <span class="attr">data-id:</span> <span class="string">$&#123;spring.application.name&#125;-param-flow-rules</span></span><br><span class="line">            <span class="attr">group-id:</span> <span class="string">SENTINEL_GROUP</span></span><br><span class="line">            <span class="attr">data-type:</span> <span class="string">json</span></span><br><span class="line">            <span class="attr">rule-type:</span> <span class="string">param-flow</span></span><br></pre></td></tr></table></figure><p>常用 <code>rule-type</code> 与文档对照（Nacos 中均为对应实体的 <strong>JSON 数组</strong>；下一节给出五类示例）：</p><table><thead><tr><th><code>rule-type</code></th><th>含义</th><th>字段说明（Wiki）</th><th>实体类（1.8.9 源码，属性名即 JSON key）</th></tr></thead><tbody><tr><td><code>flow</code></td><td>流控</td><td><a href="https://github.com/alibaba/Sentinel/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8#%E6%B5%81%E9%87%8F%E6%8E%A7%E5%88%B6">如何使用 · 流量控制</a></td><td><a href="https://github.com/alibaba/Sentinel/blob/1.8.9/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/FlowRule.java"><code>FlowRule</code></a></td></tr><tr><td><code>degrade</code></td><td>熔断降级</td><td><a href="https://github.com/alibaba/Sentinel/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8#%E7%86%94%E6%96%AD%E9%99%8D%E7%BA%A7">如何使用 · 熔断降级</a></td><td><a href="https://github.com/alibaba/Sentinel/blob/1.8.9/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/degrade/DegradeRule.java"><code>DegradeRule</code></a></td></tr><tr><td><code>authority</code></td><td>来源黑白名单</td><td><a href="https://github.com/alibaba/Sentinel/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8#%E9%BB%91%E7%99%BD%E5%90%8D%E5%8D%95%E6%8E%A7%E5%88%B6authority-rule">如何使用 · 黑白名单控制</a></td><td><a href="https://github.com/alibaba/Sentinel/blob/1.8.9/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/block/authority/AuthorityRule.java"><code>AuthorityRule</code></a></td></tr><tr><td><code>system</code></td><td>系统自适应保护</td><td><a href="https://github.com/alibaba/Sentinel/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8#%E7%B3%BB%E7%BB%9F%E8%87%AA%E9%80%82%E5%BA%94%E4%BF%9D%E6%8A%A4">如何使用 · 系统自适应保护</a></td><td><a href="https://github.com/alibaba/Sentinel/blob/1.8.9/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/system/SystemRule.java"><code>SystemRule</code></a></td></tr><tr><td><code>param-flow</code></td><td>热点参数限流</td><td><a href="https://github.com/alibaba/Sentinel/wiki/%E7%83%AD%E7%82%B9%E5%8F%82%E6%95%B0%E9%99%90%E6%B5%81">热点参数限流</a></td><td><a href="https://github.com/alibaba/Sentinel/blob/1.8.9/sentinel-extension/sentinel-parameter-flow-control/src/main/java/com/alibaba/csp/sentinel/slots/block/flow/param/ParamFlowRule.java"><code>ParamFlowRule</code></a></td></tr><tr><td><code>gw-flow</code> / <code>gw-api-group</code></td><td>网关流控 / API 分组</td><td><a href="https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81">网关限流</a></td><td>需配合 <code>spring-cloud-alibaba-sentinel-gateway</code>，示例从略</td></tr></tbody></table><blockquote><p>SCA 的 <a href="https://sca.aliyun.com/docs/2025.x/user-guide/sentinel/advanced-guide/">进阶指南</a> 说明 <strong>datasource 怎么挂</strong>；字段有疑问时再对照上表 <code>*Rule.java</code>。</p></blockquote><h3 id="4-在-Nacos-中创建规则配置">4. 在 Nacos 中创建规则配置</h3><p>统一在 Nacos 控制台 → <strong>配置管理 → 配置列表</strong> 新建，<strong>Group</strong> 均为 <code>SENTINEL_GROUP</code>，<strong>配置格式</strong> 为 <code>JSON</code>：</p><table><thead><tr><th>Data ID</th><th>对应 <code>rule-type</code></th></tr></thead><tbody><tr><td><code>demo-provider-flow-rules</code></td><td><code>flow</code></td></tr><tr><td><code>demo-provider-degrade-rules</code></td><td><code>degrade</code></td></tr><tr><td><code>demo-provider-authority-rules</code></td><td><code>authority</code></td></tr><tr><td><code>demo-provider-system-rules</code></td><td><code>system</code></td></tr><tr><td><code>demo-provider-param-flow-rules</code></td><td><code>param-flow</code></td></tr></tbody></table><h4 id="4-1-流控-flow（demo-provider-flow-rules）">4.1 流控 <code>flow</code>（<code>demo-provider-flow-rules</code>）</h4><p>对资源 <code>hello</code> 限流 QPS = 2：</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;resource&quot;</span><span class="punctuation">:</span> <span class="string">&quot;hello&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;limitApp&quot;</span><span class="punctuation">:</span> <span class="string">&quot;default&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;grade&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;count&quot;</span><span class="punctuation">:</span> <span class="number">2</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;strategy&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;controlBehavior&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;clusterMode&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><table><thead><tr><th>字段</th><th>含义</th></tr></thead><tbody><tr><td><code>resource</code></td><td>资源名，与 <code>@SentinelResource(&quot;hello&quot;)</code> 或 URL 资源一致</td></tr><tr><td><code>grade</code></td><td><code>1</code> = QPS，<code>0</code> = 线程数</td></tr><tr><td><code>count</code></td><td>阈值</td></tr><tr><td><code>strategy</code></td><td><code>0</code> 直接；<code>1</code> 关联；<code>2</code> 链路</td></tr><tr><td><code>controlBehavior</code></td><td><code>0</code> 快速失败；<code>1</code> 预热；<code>2</code> 排队</td></tr><tr><td><code>clusterMode</code></td><td>单机流控保持 <code>false</code></td></tr></tbody></table><h4 id="4-2-熔断-degrade（demo-provider-degrade-rules）">4.2 熔断 <code>degrade</code>（<code>demo-provider-degrade-rules</code>）</h4><p>异常比例熔断（<code>grade: 1</code>，<code>count</code> 为 0~1）：</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;resource&quot;</span><span class="punctuation">:</span> <span class="string">&quot;hello&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;grade&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;count&quot;</span><span class="punctuation">:</span> <span class="number">0.5</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;timeWindow&quot;</span><span class="punctuation">:</span> <span class="number">10</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;minRequestAmount&quot;</span><span class="punctuation">:</span> <span class="number">5</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;statIntervalMs&quot;</span><span class="punctuation">:</span> <span class="number">1000</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><table><thead><tr><th>字段</th><th>含义</th></tr></thead><tbody><tr><td><code>grade</code></td><td><code>0</code> 慢调用比例；<code>1</code> 异常比例；<code>2</code> 异常数</td></tr><tr><td><code>count</code></td><td>阈值（慢调用时表示最大 RT 毫秒；异常比例为 0~1）</td></tr><tr><td><code>timeWindow</code></td><td>熔断时长（秒）</td></tr><tr><td><code>minRequestAmount</code></td><td>熔断触发的最小请求数</td></tr><tr><td><code>statIntervalMs</code></td><td>统计窗口（毫秒）</td></tr><tr><td><code>slowRatioThreshold</code></td><td>仅 <code>grade: 0</code> 时使用，慢调用比例阈值</td></tr></tbody></table><h4 id="4-3-授权-authority（demo-provider-authority-rules）">4.3 授权 <code>authority</code>（<code>demo-provider-authority-rules</code>）</h4><p>仅允许来源 <code>appA</code>、<code>appB</code> 访问资源 <code>hello</code>（白名单，<code>strategy: 0</code>）：</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;resource&quot;</span><span class="punctuation">:</span> <span class="string">&quot;hello&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;limitApp&quot;</span><span class="punctuation">:</span> <span class="string">&quot;appA,appB&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;strategy&quot;</span><span class="punctuation">:</span> <span class="number">0</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><table><thead><tr><th>字段</th><th>含义</th></tr></thead><tbody><tr><td><code>limitApp</code></td><td>来源名，多个用英文逗号分隔</td></tr><tr><td><code>strategy</code></td><td><code>0</code> 白名单；<code>1</code> 黑名单</td></tr></tbody></table><p>% note tip %<br>授权规则依赖请求「来源」标识。Web 场景需实现 <code>RequestOriginParser</code>（解析 Header / 参数等为 origin），否则 <code>limitApp</code> 对不上，规则不按预期生效。<br>% endnote %</p><h4 id="4-4-系统保护-system（demo-provider-system-rules）">4.4 系统保护 <code>system</code>（<code>demo-provider-system-rules</code>）</h4><p>入口级自适应保护（对全部入口生效，无单独 <code>resource</code>；未用到的指标保持 <code>-1</code> 表示不启用）：</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></pre></td><td class="code"><pre><span class="line"><span class="punctuation">[</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;highestSystemLoad&quot;</span><span class="punctuation">:</span> <span class="number">5.0</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;highestCpuUsage&quot;</span><span class="punctuation">:</span> <span class="number">0.8</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;avgRt&quot;</span><span class="punctuation">:</span> <span class="number">200</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;maxThread&quot;</span><span class="punctuation">:</span> <span class="number">100</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;qps&quot;</span><span class="punctuation">:</span> <span class="number">1000</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><table><thead><tr><th>字段</th><th>含义</th></tr></thead><tbody><tr><td><code>highestSystemLoad</code></td><td>系统 Load1 阈值（主要 Linux 有效）</td></tr><tr><td><code>highestCpuUsage</code></td><td>CPU 使用率阈值，范围 <code>[0, 1]</code></td></tr><tr><td><code>avgRt</code></td><td>所有入口平均 RT 阈值（毫秒）</td></tr><tr><td><code>maxThread</code></td><td>入口最大并发线程数</td></tr><tr><td><code>qps</code></td><td>所有入口总 QPS 阈值</td></tr></tbody></table><h4 id="4-5-热点参数-param-flow（demo-provider-param-flow-rules）">4.5 热点参数 <code>param-flow</code>（<code>demo-provider-param-flow-rules</code>）</h4><p>对资源 <code>hello</code> 的第 0 个参数默认 QPS = 5；参数值 <code>1001</code> 单独放开到 QPS = 10：</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><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="punctuation">[</span></span><br><span class="line">  <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;resource&quot;</span><span class="punctuation">:</span> <span class="string">&quot;hello&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;grade&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;paramIdx&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;count&quot;</span><span class="punctuation">:</span> <span class="number">5</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;controlBehavior&quot;</span><span class="punctuation">:</span> <span class="number">0</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;durationInSec&quot;</span><span class="punctuation">:</span> <span class="number">1</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;clusterMode&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;paramFlowItemList&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;object&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1001&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;classType&quot;</span><span class="punctuation">:</span> <span class="string">&quot;int&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;count&quot;</span><span class="punctuation">:</span> <span class="number">10</span></span><br><span class="line">      <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><table><thead><tr><th>字段</th><th>含义</th></tr></thead><tbody><tr><td><code>paramIdx</code></td><td>热点参数在 <code>args</code> 中的下标，从 <code>0</code> 起</td></tr><tr><td><code>count</code></td><td>该参数维度的默认阈值</td></tr><tr><td><code>grade</code></td><td><code>1</code> = QPS，<code>0</code> = 线程数</td></tr><tr><td><code>paramFlowItemList</code></td><td>例外项：某具体参数值使用单独阈值</td></tr><tr><td><code>object</code> / <code>classType</code></td><td>例外参数值及其类型（如 <code>int</code>、<code>long</code>、<code>java.lang.String</code>）</td></tr></tbody></table><p>% note tip %<br>热点规则必须把参数传入 Sentinel 埋点，例如 <code>SphU.entry(&quot;hello&quot;, EntryType.IN, 1, userId)</code>，<code>paramIdx: 0</code> 即对应第一个 <code>args</code>。仅写 <code>@SentinelResource(&quot;hello&quot;)</code>、调用时不传参，热点规则不会按参数生效。<br>% endnote %</p><h3 id="5-验证步骤">5. 验证步骤</h3><table><thead><tr><th>步骤</th><th>操作</th><th>预期</th></tr></thead><tbody><tr><td>1</td><td>发布 Nacos 配置后启动（或已启动）应用</td><td>日志无 datasource 加载失败；可压测 <code>/hello</code> 被限流（<strong>无需 Dashboard</strong>）</td></tr><tr><td>2</td><td>（可选）打开 Sentinel Dashboard</td><td>可见应用与资源；规则列表可能因数据源来源显示为「动态规则」侧生效</td></tr><tr><td>3</td><td>修改 Nacos 中 <code>count</code> 并发布</td><td>客户端秒级感知（监听推送），无需重启</td></tr><tr><td>4</td><td>重启应用（任意是否启 Dashboard）</td><td>规则仍从 Nacos 加载，限流行为不变</td></tr></tbody></table><div class="note warning"><p>若同时在 <strong>Dashboard 里手改规则</strong> 又在 <strong>Nacos 里改规则</strong>，客户端终态以最后一次写入内存的规则为准，容易踩坑。持久化启用后，请统一在 Nacos 维护。</p></div><h3 id="6-可选：Dashboard-侧双向推送（源码改造）">6. 可选：Dashboard 侧双向推送（源码改造）</h3><p>开箱 Dashboard <strong>不会</strong>把控制台改动写回 Nacos。下文 <strong>6.2～6.4</strong> 按推荐路线介绍 <strong>Dashboard 侧改造</strong>（V2 + Nacos Provider/Publisher）；若在网上看到「客户端 <code>WritableDataSource</code> 反写 Nacos」的做法，见本章末尾 <strong>6.5</strong> 作对照了解即可。</p><p>官方在 <strong>1.8.9</strong> 源码里只放了一套 <strong>流控规则</strong> 的 Dashboard 改造示例（不是完整生产方案），且位于 <strong><code>src/test</code></strong>，发布包 JAR 里看不到。</p><h4 id="6-1-代码到底在哪（为何在仓库里不好找）">6.1 代码到底在哪（为何在仓库里不好找）</h4><p>克隆并切到 tag 后路径为（浏览器可直接打开）：</p><table><thead><tr><th>文件</th><th>GitHub 路径（tag <code>1.8.9</code>）</th></tr></thead><tbody><tr><td>DataId / Group 常量</td><td><a href="https://github.com/alibaba/Sentinel/blob/1.8.9/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfigUtil.java"><code>.../rule/nacos/NacosConfigUtil.java</code></a></td></tr><tr><td>Nacos <code>ConfigService</code> Bean</td><td><a href="https://github.com/alibaba/Sentinel/blob/1.8.9/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/NacosConfig.java"><code>.../rule/nacos/NacosConfig.java</code></a></td></tr><tr><td>从 Nacos 拉流控</td><td><a href="https://github.com/alibaba/Sentinel/blob/1.8.9/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/FlowRuleNacosProvider.java"><code>.../rule/nacos/FlowRuleNacosProvider.java</code></a></td></tr><tr><td>写回流控到 Nacos</td><td><a href="https://github.com/alibaba/Sentinel/blob/1.8.9/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/FlowRuleNacosPublisher.java"><code>.../rule/nacos/FlowRuleNacosPublisher.java</code></a></td></tr></tbody></table><p>对应本地目录：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/</span><br><span class="line">├── NacosConfig.java</span><br><span class="line">├── NacosConfigUtil.java</span><br><span class="line">├── FlowRuleNacosProvider.java</span><br><span class="line">└── FlowRuleNacosPublisher.java</span><br></pre></td></tr></table></figure><p>找不到时常见原因：</p><ol><li class="lvl-3"><p>只下了 <code>sentinel-dashboard-1.8.9.jar</code>（不含 <code>src/test</code>）</p></li><li class="lvl-3"><p>在 <code>src/main</code> 里搜（示例默认不在 main）</p></li><li class="lvl-3"><p>看了错误分支 / 未切到 <code>1.8.9</code></p></li><li class="lvl-3"><p>以为还有 degrade / authority 等示例——<strong>没有</strong>；官方 test 样例 <strong>只有 flow</strong>，其它规则类型需自行照抄扩展</p></li></ol><p>官方样例约定与本文一致性较好：</p><ul class="lvl-0"><li class="lvl-2"><p>Group：<code>SENTINEL_GROUP</code>（见 <code>NacosConfigUtil.GROUP_ID</code>）</p></li><li class="lvl-2"><p>DataId：<code>&#123;app&#125;-flow-rules</code>（见 <code>FLOW_DATA_ID_POSTFIX = &quot;-flow-rules&quot;</code>），与上文 <code>demo-provider-flow-rules</code> 一致</p></li></ul><h4 id="6-2-改造步骤（以流控双向同步为例）">6.2 改造步骤（以流控双向同步为例）</h4><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">git <span class="built_in">clone</span> https://github.com/alibaba/Sentinel.git</span><br><span class="line"><span class="built_in">cd</span> Sentinel</span><br><span class="line">git checkout 1.8.9</span><br><span class="line"><span class="built_in">cd</span> sentinel-dashboard</span><br></pre></td></tr></table></figure><p><strong>步骤 1：放开 Nacos 依赖（去掉 test scope）</strong></p><p>编辑 <code>sentinel-dashboard/pom.xml</code>，将：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.csp<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>sentinel-datasource-nacos<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">scope</span>&gt;</span>test<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>改为（删除 <code>&lt;scope&gt;test&lt;/scope&gt;</code>，必要时升到与 Nacos 3.x 兼容的客户端版本）：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.csp<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>sentinel-datasource-nacos<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p><strong>步骤 2：把 test 样例拷到 main</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="built_in">mkdir</span> -p src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos</span><br><span class="line"><span class="built_in">cp</span> src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/*.java \</span><br><span class="line">   src/main/java/com/alibaba/csp/sentinel/dashboard/rule/nacos/</span><br></pre></td></tr></table></figure><p><strong>步骤 3：改 <code>NacosConfig</code>，接上 Nacos 3.x 地址与鉴权</strong></p><p>官方默认是 <code>ConfigFactory.createConfigService(&quot;localhost&quot;)</code>，对 Nacos 3.x（含鉴权、端口 <code>8848</code>）不够用。建议改成可配置，例如：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> ConfigService <span class="title function_">nacosConfigService</span><span class="params">()</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    <span class="type">Properties</span> <span class="variable">properties</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Properties</span>();</span><br><span class="line">    properties.put(<span class="string">&quot;serverAddr&quot;</span>, <span class="string">&quot;127.0.0.1:8848&quot;</span>);</span><br><span class="line">    properties.put(<span class="string">&quot;namespace&quot;</span>, <span class="string">&quot;&quot;</span>);          <span class="comment">// public 留空；其它命名空间填 namespaceId</span></span><br><span class="line">    properties.put(<span class="string">&quot;username&quot;</span>, <span class="string">&quot;nacos&quot;</span>);</span><br><span class="line">    properties.put(<span class="string">&quot;password&quot;</span>, <span class="string">&quot;YourStrongPassword&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> ConfigFactory.createConfigService(properties);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>（也可用 <code>application.properties</code> + <code>@Value</code> 注入，避免把密码写死在代码里。）</p><p><strong>步骤 4（建议）：发布时转成客户端可直接消费的 <code>FlowRule</code> JSON</strong></p><p>官方 encoder 直接 <code>JSON.toJSONString(List&lt;FlowRuleEntity&gt;)</code>，会带上 <code>app</code> / <code>gmtCreate</code> 等控制台字段。客户端 <code>rule-type: flow</code> 一般能忽略多余字段，但更干净的做法是发布前 <code>toRule()</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> Converter&lt;List&lt;FlowRuleEntity&gt;, String&gt; <span class="title function_">flowRuleEntityEncoder</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> list -&gt; JSON.toJSONString(</span><br><span class="line">        list.stream().map(FlowRuleEntity::toRule).collect(Collectors.toList())</span><br><span class="line">    );</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>decoder 仍可 <code>JSON.parseArray(s, FlowRuleEntity.class)</code>（字段重合部分能填上；打开规则页时 Controller 还会补 <code>app</code>）。</p><p><strong>步骤 5：让 <code>FlowControllerV2</code> 使用 Nacos 的 Provider / Publisher</strong></p><p>编辑 <code>com.alibaba.csp.sentinel.dashboard.controller.v2.FlowControllerV2.java</code>。该类通过 <code>@Qualifier</code> 注入规则读写 Bean；默认走内存实现，需 <strong>只改两个 Qualifier 名</strong>，指向步骤 2 拷到 main 的 Nacos 实现（<code>@Component(&quot;flowRuleNacosProvider&quot;)</code> / <code>@Component(&quot;flowRuleNacosPublisher&quot;)</code>）：</p><p>修改前（默认，读写内存，不写 Nacos）：</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">@Autowired</span></span><br><span class="line"><span class="meta">@Qualifier(&quot;flowRuleDefaultProvider&quot;)</span></span><br><span class="line"><span class="keyword">private</span> DynamicRuleProvider&lt;List&lt;FlowRuleEntity&gt;&gt; ruleProvider;</span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="meta">@Qualifier(&quot;flowRuleDefaultPublisher&quot;)</span></span><br><span class="line"><span class="keyword">private</span> DynamicRulePublisher&lt;List&lt;FlowRuleEntity&gt;&gt; rulePublisher;</span><br></pre></td></tr></table></figure><p>修改后（读写 Nacos）——<strong>仅替换 Qualifier 中的 Bean 名</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">@Autowired</span></span><br><span class="line"><span class="meta">@Qualifier(&quot;flowRuleNacosProvider&quot;)</span>      <span class="comment">// default → nacos</span></span><br><span class="line"><span class="keyword">private</span> DynamicRuleProvider&lt;List&lt;FlowRuleEntity&gt;&gt; ruleProvider;</span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="meta">@Qualifier(&quot;flowRuleNacosPublisher&quot;)</span>     <span class="comment">// default → nacos</span></span><br><span class="line"><span class="keyword">private</span> DynamicRulePublisher&lt;List&lt;FlowRuleEntity&gt;&gt; rulePublisher;</span><br></pre></td></tr></table></figure><p>即：<code>flowRuleDefaultProvider</code> → <code>flowRuleNacosProvider</code>，<code>flowRuleDefaultPublisher</code> → <code>flowRuleNacosPublisher</code>。字段类型与其它代码不用动。</p><div class="note warning"><p>必须走 <strong>V2</strong> 接口（<code>/v2/flow/**</code>）。V1（<code>/v1/flow/**</code>）仍是推客户端内存，改完 Provider 也不写 Nacos。</p></div><p><strong>步骤 6：前端菜单切到 V2 流控页</strong></p><p>1.8.9 默认侧边栏「流控规则」指向 <strong>V1</strong>：</p><figure class="highlight html"><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">&lt;!-- sidebar.html --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">ui-sref</span>=<span class="string">&quot;dashboard.flowV1(&#123;app: entry.app&#125;)&quot;</span>&gt;</span>流控规则<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br></pre></td></tr></table></figure><p>改为（注意：V2 的 state 名是 <code>dashboard.flow</code>，URL 为 <code>/v2/flow/:app</code>）：</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">ui-sref</span>=<span class="string">&quot;dashboard.flow(&#123;app: entry.app&#125;)&quot;</span>&gt;</span>流控规则<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br></pre></td></tr></table></figure><p>文件：<code>src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html</code>。</p><p>簇点链路里「加流控」走的是 <code>identity.js</code> + <code>FlowServiceV1</code>，同样要改：</p><ul class="lvl-0"><li class="lvl-2"><p><code>FlowServiceV1</code> → <code>FlowServiceV2</code></p></li><li class="lvl-2"><p>跳转路径 <code>/dashboard/flow/</code> → <code>/dashboard/v2/flow/</code></p></li></ul><p>文件：<code>src/main/webapp/resources/app/scripts/controllers/identity.js</code>。</p><p><strong>步骤 7：打包替换官方 JAR</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"># 在仓库根目录或 dashboard 模块按官方方式打包</span></span><br><span class="line">mvn -pl sentinel-dashboard -am clean package -DskipTests</span><br><span class="line"><span class="comment"># 产物一般在 sentinel-dashboard/target/sentinel-dashboard.jar</span></span><br><span class="line">java -Dserver.port=8858 -jar target/sentinel-dashboard.jar</span><br></pre></td></tr></table></figure><h4 id="6-3-改造后如何验证">6.3 改造后如何验证</h4><table><thead><tr><th>步骤</th><th>预期</th></tr></thead><tbody><tr><td>控制台打开应用 →「流控规则」（V2）新增规则并保存</td><td>Nacos 出现 / 更新 <code>demo-provider-flow-rules</code>（Group=<code>SENTINEL_GROUP</code>）</td></tr><tr><td>应用已配置 <code>rule-type: flow</code> 数据源</td><td>客户端自动收到新规则并限流</td></tr><tr><td>在 Nacos 改 <code>count</code> 后刷新控制台 V2 流控页</td><td>能读到最新配置</td></tr></tbody></table><h4 id="6-4-能力边界">6.4 能力边界</h4><ul class="lvl-0"><li class="lvl-2"><p>官方样例 <strong>只覆盖 flow</strong>；熔断 / 授权 / 系统 / 热点需自行增加成对的 Provider/Publisher，并改对应 Controller（它们默认也不是 V2 + DynamicRule 模式）。</p></li><li class="lvl-2"><p>运维成本明显高于「应用直连 Nacos、控制台只做监控」。中小型环境通常 <strong>不必</strong> 做本节改造。</p></li></ul><h3 id="7-附录：客户端-WritableDataSource-写回-Nacos（了解即可）">7. 附录：客户端 WritableDataSource 写回 Nacos（了解即可）</h3><p>网上另一种常见做法是 <strong>不改 Dashboard</strong>，而在客户端注册 <strong>写数据源</strong>：Dashboard（默认 V1）把规则推到客户端内存后，Sentinel transport 模块回调 <code>WritableDataSource.write()</code>，由客户端把规则 <strong>反写</strong> 到 Nacos。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">Dashboard（V1 默认）</span><br><span class="line">    │  API 推送到客户端内存</span><br><span class="line">    ▼</span><br><span class="line">WritableDataSource.write()  →  Nacos publishConfig</span><br><span class="line">    ▲</span><br><span class="line">    │  Nacos 监听（spring.cloud.sentinel.datasource）</span><br><span class="line">客户端内存 ←────────────────┘</span><br></pre></td></tr></table></figure><p>官方 <a href="https://github.com/alibaba/Sentinel/blob/master/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FileDataSourceInit.java"><code>FileDataSourceInit</code></a> 演示的就是这个钩子——只不过写的是 <strong>本地文件</strong>，不是 Nacos：</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="comment">// 读：注册到 RuleManager（Push 模式常规做法）</span></span><br><span class="line">ReadableDataSource&lt;String, List&lt;FlowRule&gt;&gt; ds = <span class="keyword">new</span> <span class="title class_">NacosDataSource</span>&lt;&gt;(serverAddr, groupId, dataId, parser);</span><br><span class="line">FlowRuleManager.register2Property(ds.getProperty());</span><br><span class="line"></span><br><span class="line"><span class="comment">// 写：Dashboard 推送后回调；社区常自实现 NacosWritableDataSource</span></span><br><span class="line">WritableDataSource&lt;List&lt;FlowRule&gt;&gt; wds = <span class="keyword">new</span> <span class="title class_">NacosWritableDataSource</span>&lt;&gt;(serverAddr, groupId, dataId, JSON::toJSONString);</span><br><span class="line">WritableDataSourceRegistry.registerFlowDataSource(wds);</span><br></pre></td></tr></table></figure><p>注意：<code>sentinel-datasource-nacos</code> <strong>1.8.9 官方只有 <code>NacosDataSource</code>（只读）</strong>，没有 <code>NacosWritableDataSource</code>；网上教程里的写 Nacos 实现多为 <strong>社区自写</strong>（在 <code>write()</code> 里调 <code>configService.publishConfig()</code>），或通过 <code>InitFunc</code> / <code>@PostConstruct</code> 注册到 <code>WritableDataSourceRegistry</code>。</p><p>与上文 <strong>6.2 Dashboard 改造</strong> 对比：</p><table><thead><tr><th></th><th>客户端 WritableDataSource 反写</th><th>Dashboard V2 + Nacos Provider/Publisher</th></tr></thead><tbody><tr><td>改动范围</td><td>每个业务应用</td><td>只改 Dashboard</td></tr><tr><td>能否用默认 V1 控制台</td><td>能</td><td>需切 V2 页面 + 改后端</td></tr><tr><td>官方 Push 模式态度</td><td><strong>不推荐</strong> 客户端反写配置中心</td><td><strong>推荐</strong></td></tr><tr><td>规则维度</td><td>V1 按 <strong>机器</strong>（ip:port）推送</td><td>V2 按 <strong>应用</strong>（app）读写</td></tr><tr><td>多实例</td><td>每台实例各写一次 Nacos，易覆盖/冲突</td><td>Dashboard 统一写一份 <code>&#123;app&#125;-flow-rules</code></td></tr></tbody></table><div class="note warning"><p>官方 <a href="https://github.com/alibaba/Sentinel/wiki/%E5%9C%A8%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E4%B8%AD%E4%BD%BF%E7%94%A8-Sentinel">在生产环境中使用 Sentinel</a> 明确：Push 模式正确链路是 <strong>控制台 → 配置中心 → 客户端读数据源 → 内存</strong>，而不是客户端收到 Dashboard 推送后再写 Nacos（客户端已监听同一 DataId 时，可能 <strong>重复更新</strong>；多实例还会 <strong>争抢写</strong>）。<code>WritableDataSourceRegistry</code> 的设计场景是 Pull 模式 + <strong>本地文件</strong> 持久化。</p></div><p>适用场景：<strong>单机联调、不想改 Dashboard 源码</strong> 时可了解此路；生产多实例仍建议走 <strong>6.2 Dashboard 改造</strong>，或更简单——<strong>只在 Nacos 维护规则</strong>（上文第五节 Push 模式），Dashboard 仅做监控。</p><hr><h2 id="六、联调检查清单">六、联调检查清单</h2><table><thead><tr><th>检查项</th><th>预期结果</th></tr></thead><tbody><tr><td>Nacos 规则配置</td><td>对应 DataId / Group 已发布且 JSON 合法</td></tr><tr><td>规则持久化 / 生效</td><td>改 Nacos 后限流生效；重启应用后仍在（<strong>可不启 Dashboard</strong>）</td></tr><tr><td>Sentinel Dashboard（可选）</td><td>若已启动：<code>http://127.0.0.1:8858</code> 可登录；触发流量后可见应用</td></tr><tr><td>客户端版本</td><td><code>sentinel 1.8.9</code>（由 SCA BOM 引入）</td></tr></tbody></table><p>常见问题：</p><ol><li class="lvl-3"><p><strong>控制台无应用</strong>：需真实流量触发一次资源；检查 <code>spring.cloud.sentinel.transport.dashboard</code> 指向与 Dashboard 实际端口一致。</p></li><li class="lvl-3"><p><strong>端口冲突</strong>：与 Nacos 3.x 同机时，勿再占用 <code>8080</code>；本文默认使用 <code>8858</code>。</p></li><li class="lvl-3"><p><strong>Nacos 规则不生效</strong>：核对 <code>data-id</code> / <code>group-id</code> / <code>namespace</code> / 账号密码；<code>rule-type</code> 与配置内容类型一致；看应用日志是否有 datasource 转换失败。</p></li><li class="lvl-3"><p><strong>控制台改了规则，重启又没了</strong>：未接 Nacos，或未把 Dashboard 改成写回 Nacos；请以 Nacos 为准。</p></li><li class="lvl-3"><p><strong>版本混用</strong>：按 SCA 官方矩阵固定 <strong>Sentinel 1.8.9</strong> 最省事。</p></li></ol><hr><h2 id="七、参考链接">七、参考链接</h2><ul class="lvl-0"><li class="lvl-2"><p><a href="https://sca.aliyun.com/docs/2025.x/overview/version-explain/">Spring Cloud Alibaba 版本发布说明（2025.x）</a></p></li><li class="lvl-2"><p><a href="https://sca.aliyun.com/docs/2025.x/user-guide/sentinel/quick-start/">SCA · Sentinel 快速开始</a></p></li><li class="lvl-2"><p><a href="https://sca.aliyun.com/docs/2025.x/user-guide/sentinel/advanced-guide/">SCA · Sentinel 进阶指南（客户端支持 / 动态数据源）</a></p></li><li class="lvl-2"><p><a href="https://github.com/alibaba/Sentinel/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8">Sentinel Wiki · 如何使用（各规则字段）</a></p></li><li class="lvl-2"><p><a href="https://github.com/alibaba/Sentinel/wiki/%E7%83%AD%E7%82%B9%E5%8F%82%E6%95%B0%E9%99%90%E6%B5%81">Sentinel Wiki · 热点参数限流</a></p></li><li class="lvl-2"><p><a href="https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81">Sentinel Wiki · 网关限流</a></p></li><li class="lvl-2"><p><a href="https://github.com/alibaba/Sentinel/wiki/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%99%E6%89%A9%E5%B1%95">Sentinel · 动态规则扩展</a></p></li><li class="lvl-2"><p><a href="https://github.com/alibaba/Sentinel/wiki/%E5%9C%A8%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E4%B8%AD%E4%BD%BF%E7%94%A8-Sentinel">Sentinel · 在生产环境中使用 Sentinel（Push / Pull / WritableDataSource）</a></p></li><li class="lvl-2"><p><a href="https://github.com/alibaba/Sentinel/releases/tag/1.8.9">Sentinel v1.8.9 Release</a></p></li><li class="lvl-2"><p><a href="https://github.com/alibaba/Sentinel/wiki/Dashboard">Sentinel Wiki · Dashboard</a></p></li><li class="lvl-2"><p><a href="https://github.com/alibaba/Sentinel/tree/1.8.9/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos">Sentinel Dashboard · Nacos 规则样例（1.8.9 / src/test）</a></p></li><li class="lvl-2"><a href="/2026/07/14/sca-2025-nacos-install/" title="Spring Cloud Alibaba 2025.0.0.0 配套搭建 Nacos 3.0.3">Spring Cloud Alibaba 2025.0.0.0 配套搭建 Nacos 3.0.3</a></li></ul>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/07/14/sca-2025-sentinel-install/</id>
    <link href="https://blog.hanqunfeng.com/2026/07/14/sca-2025-sentinel-install/"/>
    <published>2026-07-14T08:44:58.000Z</published>
    <summary>
      <![CDATA[<!--
 **加粗**
 *斜体*
 ***加粗并斜体***
 ~~删除线~~
 ==突出显示==
 `突出显示(推荐)`
 ++下划线++
 ~下标~
 ^上标^
 脚注，参考文献[^1]，然后在文档最下方要添加这个1对应的内容，如：[^1]: My reference.
 图片设置宽度和高度(通过uPic上传后，需要将upic修改过为blog，用于添加水印) ![](https://upic-oss.oss-cn-beijing.aliyuncs.com/blog/innodb_buffer_pool.png =900x600)

 +++ **点击折叠**
 这是被隐藏的内容
 +++

::: tips success warning danger
这里是容器内的内容
:::

% note info % success warning danger
这里是容器内的内容
% endnote %

引用本地其它文章连接{}
 大括号开始% post_link 文件名称(不包含.md) %大括号结束
 -->
<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">
<p>依据 <a href="https://sca.aliyun.com/docs/2025.x/overview/version-explain/">Spring Cloud Alibaba 版本发布说明</a>，搭建与 <strong>Spring Cloud Alibaba 2025.0.0.0</strong> 对应的 <strong>Sentinel Dashboard 1.8.9</strong>。</p>
</li>
<li class="lvl-2">
<p>本文侧重 Dashboard <strong>单机</strong>搭建（JAR / Docker / systemd），并补充应用侧接入、<strong>OpenFeign / RestTemplate / Gateway</strong> 适配，以及 Nacos 规则持久化。</p>
</li>
<li class="lvl-2">
<p>生产推荐：规则持久化到 <strong>Nacos</strong>（Push 模式）；<strong>Dashboard 可选</strong>（仅监控时需要），限流生效不依赖控制台。</p>
</li>
<li class="lvl-2">
<p>同版本配套的 Nacos Server 见 <a href="/2026/07/14/sca-2025-nacos-install/" title="Spring Cloud Alibaba 2025.0.0.0 配套搭建 Nacos 3.0.3">Spring Cloud Alibaba 2025.0.0.0 配套搭建 Nacos 3.0.3</a>。</p>
</li>
</ul>]]>
    </summary>
    <title>Spring Cloud Alibaba 2025.0.0.0 配套搭建 Sentinel Dashboard 1.8.9</title>
    <updated>2026-07-17T09:10:00.842Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="微服务" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    <category term="spring-cloud-alibaba" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/%E5%BE%AE%E6%9C%8D%E5%8A%A1/spring-cloud-alibaba/"/>
    <category term="spring-cloud-alibaba" scheme="https://blog.hanqunfeng.com/tags/spring-cloud-alibaba/"/>
    <category term="nacos" scheme="https://blog.hanqunfeng.com/tags/nacos/"/>
    <content>
      <![CDATA[<!-- **加粗** *斜体* ***加粗并斜体*** ~~删除线~~ ==突出显示== `突出显示(推荐)` ++下划线++ ~下标~ ^上标^ 脚注，参考文献[^1]，然后在文档最下方要添加这个1对应的内容，如：[^1]: My reference. 图片设置宽度和高度(通过uPic上传后，需要将upic修改过为blog，用于添加水印) ![](https://upic-oss.oss-cn-beijing.aliyuncs.com/blog/innodb_buffer_pool.png =900x600) +++ **点击折叠** 这是被隐藏的内容 +++::: tips success warning danger这里是容器内的内容:::% note info % success warning danger这里是容器内的内容% endnote %引用本地其它文章连接{} 大括号开始% post_link 文件名称(不包含.md) %大括号结束 --><h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2"><p>依据 <a href="https://sca.aliyun.com/docs/2025.x/overview/version-explain/">Spring Cloud Alibaba 版本发布说明</a>，搭建与 <strong>Spring Cloud Alibaba 2025.0.0.0</strong> 对应的 <strong>Nacos Server 3.0.3</strong>。</p></li><li class="lvl-2"><p>覆盖服务端单机搭建（二进制包 + Docker）、<strong>MySQL 外置持久化</strong>、<strong>多节点集群</strong>，并补充 Spring Boot 应用侧的最小接入配置。</p></li><li class="lvl-2"><p>同版本配套的 Sentinel Dashboard 见 <a href="/2026/07/14/sca-2025-sentinel-install/" title="Spring Cloud Alibaba 2025.0.0.0 配套搭建 Sentinel Dashboard 1.8.9">Spring Cloud Alibaba 2025.0.0.0 配套搭建 Sentinel Dashboard 1.8.9</a>。</p></li></ul><span id="more"></span><h2 id="一、版本对照">一、版本对照</h2><h3 id="1-Spring-Cloud-Alibaba-2025-0-x-适配关系">1. Spring Cloud Alibaba 2025.0.x 适配关系</h3><p>摘自官方 <a href="https://sca.aliyun.com/docs/2025.x/overview/version-explain/">版本发布说明</a>：</p><table><thead><tr><th>Spring Cloud Alibaba Version</th><th>Spring Cloud Version</th><th>Spring Boot Version</th></tr></thead><tbody><tr><td>2025.0.0.0</td><td>2025.0.0</td><td>3.5.0</td></tr></tbody></table><p>同页「组件版本关系」中，与 <code>2025.0.0.0</code> 对应的 Nacos 版本为：</p><table><thead><tr><th>Spring Cloud Alibaba Version</th><th>Nacos Version</th></tr></thead><tbody><tr><td>2025.0.0.0</td><td>3.0.3</td></tr></tbody></table><blockquote><p>对比：同属 2025.x 的 <code>2025.1.0.0</code> 适配 Spring Boot 4.0 / Spring Cloud 2025.1，组件为 Nacos <code>3.1.1</code>。若走 Boot 4 路线请勿混用本文 Nacos 版本。</p></blockquote><h3 id="2-环境准备">2. 环境准备</h3><table><thead><tr><th>项目</th><th>要求说明</th></tr></thead><tbody><tr><td>Nacos 3.0.3</td><td>官方要求 <strong>64 位 JDK 17+</strong>；建议机器至少 2C4G</td></tr><tr><td>MySQL（持久化 / 集群推荐）</td><td><strong>5.6.5+</strong>（建议 8.0）；集群生产强烈建议外置 MySQL，勿用嵌入式 Derby</td></tr><tr><td>Docker（可选）</td><td>Docker 20+ / Docker Compose v2</td></tr><tr><td>集群节点</td><td>建议 <strong>≥ 3</strong> 奇数台，防止脑裂</td></tr></tbody></table><p>端口规划：</p><table><thead><tr><th>用途</th><th>端口</th><th>说明</th></tr></thead><tbody><tr><td>Nacos 控制台</td><td><code>8080</code></td><td>Nacos 3.x 控制台入口</td></tr><tr><td>Nacos 主端口</td><td><code>8848</code></td><td>客户端注册 / 配置</td></tr><tr><td>客户端 gRPC</td><td><code>9848</code></td><td>客户端通信（主端口 +1000，需与 8848 一并放行）</td></tr><tr><td>服务端间 gRPC</td><td><code>9849</code></td><td>集群节点间通信（主端口 +1001）</td></tr><tr><td>Raft</td><td><code>7848</code></td><td>集群选主 / 一致性（主端口 -1000）</td></tr></tbody></table><hr><h2 id="二、搭建-Nacos-3-0-3">二、搭建 Nacos 3.0.3</h2><p>官方文档：<a href="https://nacos.io/docs/v3.0/quickstart/quick-start/">Nacos 快速开始</a>、<a href="https://nacos.io/docs/v3.0/quickstart/quick-start-docker/">Docker 快速开始</a>。</p><div class="note warning"><p>Nacos 3.x 相对 2.x 有几处常见差异，搭建前先记住：</p><ol><li class="lvl-3">控制台地址为 <code>http://127.0.0.1:8080/index.html</code>（不再是 <code>http://ip:8848/nacos</code>）。</li><li class="lvl-3">控制台鉴权默认开启；启动前必须配置 JWT 密钥与服务端身份识别参数。</li><li class="lvl-3">首次打开控制台需要<strong>初始化管理员用户 <code>nacos</code> 的密码</strong>（不再提供默认 <code>nacos/nacos</code>）。</li><li class="lvl-3">Spring Cloud Alibaba <code>2025.0.x</code> 若使用 <code>public</code> / 空命名空间，需使用 Nacos Server 3.x，与本文版本一致。</li></ol></div><h3 id="1-方式一：二进制包单机启动（推荐先理解流程）">1. 方式一：二进制包单机启动（推荐先理解流程）</h3><h4 id="（1）下载">（1）下载</h4><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"><span class="comment"># 官网下载页：https://nacos.io/download/nacos-server/</span></span><br><span class="line">wget https://download.nacos.io/nacos-server/nacos-server-3.0.3.zip</span><br><span class="line">unzip nacos-server-3.0.3.zip</span><br><span class="line"></span><br><span class="line"><span class="comment"># GitHub Release：https://github.com/alibaba/nacos/releases/tag/3.0.3</span></span><br><span class="line">wget https://github.com/alibaba/nacos/releases/download/3.0.3/nacos-server-3.0.3.tar.gz</span><br><span class="line">tar -zxvf nacos-server-3.0.3.tar.gz</span><br><span class="line"></span><br><span class="line"><span class="built_in">cd</span> nacos</span><br></pre></td></tr></table></figure><h4 id="（2）生成鉴权相关配置">（2）生成鉴权相关配置</h4><p>三个参数均<strong>无官方默认值</strong>，需自行生成并固定保存（集群节点必须一致）：</p><table><thead><tr><th>配置项 / 环境变量</th><th>作用</th></tr></thead><tbody><tr><td><code>nacos.core.auth.plugin.nacos.token.secret.key</code> / <code>NACOS_AUTH_TOKEN</code></td><td>签发 accessToken 的 JWT 密钥（Base64，原始长度建议 ≥ 32）</td></tr><tr><td><code>nacos.core.auth.server.identity.key</code> / <code>NACOS_AUTH_IDENTITY_KEY</code></td><td>服务端身份识别 Key</td></tr><tr><td><code>nacos.core.auth.server.identity.value</code> / <code>NACOS_AUTH_IDENTITY_VALUE</code></td><td>服务端身份识别 Value</td></tr></tbody></table><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><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># JWT 密钥：先生成 ≥32 字符明文，再 Base64</span></span><br><span class="line">openssl rand -<span class="built_in">base64</span> 32</span><br><span class="line"><span class="comment"># 3kphyB8N/h/7NHvoXdvM7hwfCsnpBXK+1nlruWb2gUQ=</span></span><br><span class="line">openssl rand -<span class="built_in">base64</span> 48</span><br><span class="line"><span class="comment"># PZhCxx3Rj5Z3GZ59tBkxSElxHCKPMTEtInHiJSVqUnz435/H2iwY3buSEX0DFQ6j</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># identity key / value 可用任意自定义字符串，例如：</span></span><br><span class="line"><span class="built_in">echo</span> -n <span class="string">&#x27;nacos_identity_key&#x27;</span> | <span class="built_in">base64</span></span><br><span class="line"><span class="comment"># bmFjb3NfaWRlbnRpdHlfa2V5</span></span><br><span class="line"><span class="built_in">echo</span> -n <span class="string">&#x27;nacos_identity_value&#x27;</span> | <span class="built_in">base64</span></span><br><span class="line"><span class="comment"># bmFjb3NfaWRlbnRpdHlfdmFsdWU=</span></span><br></pre></td></tr></table></figure><p>也可在首次执行 <code>startup.sh -m standalone</code> 时按提示交互填入，<br>填入后会写入 <code>conf/application.properties</code>。<br>但建议直接修改配置文件，因为 base64 生成的编码可能含有特殊字符导致配置失败。</p><p>手动写入示例（请替换为你自己的值）：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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="comment">### conf/application.properties 片段</span></span><br><span class="line"><span class="comment"># 使用“内置的 Nacos 原生鉴权体系”，默认 nacos，还支持 ldap</span></span><br><span class="line"><span class="attr">nacos.core.auth.system.type</span>=<span class="string">nacos</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"># 总开关，是否开启 Nacos 服务端 API 的鉴权系统，默认为 false（当前值）：表示关闭鉴权。</span></span><br><span class="line"><span class="comment"># 此时任何人无需用户名密码即可通过 Open API 访问、修改 Nacos 上的配置和服务列表。这在生产环境是极不安全的。</span></span><br><span class="line"><span class="attr">nacos.core.auth.enabled</span>=<span class="string">true</span></span><br><span class="line"><span class="comment"># 是否开启 Nacos 管理 API 的鉴权，默认为true</span></span><br><span class="line"><span class="attr">nacos.core.auth.admin.enabled</span>=<span class="string">true</span></span><br><span class="line"><span class="comment"># 是否开启 Nacos 控制台（Web UI）的鉴权，默认为true</span></span><br><span class="line"><span class="attr">nacos.core.auth.console.enabled</span>=<span class="string">true</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="comment"># 当 nacos.core.auth.enabled=true 时生效</span></span><br><span class="line"><span class="comment"># 如果请求的 Header 中包含 key=nacos_identity_key 且 value=nacos_identity_value，Nacos 会认为这是一个受信任的内部服务器请求（例如通过 Nginx 代理转发的请求，或者 Nacos 集群节点间的通信），从而跳过常规的用户 Token 鉴权。</span></span><br><span class="line"><span class="comment"># 在生产环境中，必须修改这两个默认值，防止被恶意利用绕过鉴权。</span></span><br><span class="line"><span class="attr">nacos.core.auth.server.identity.key</span>=<span class="string">替换为你的identityKey</span></span><br><span class="line"><span class="attr">nacos.core.auth.server.identity.value</span>=<span class="string">替换为你的identityValue</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"># 用于生成 Token 的密钥（Base64 编码的字符串）</span></span><br><span class="line"><span class="comment"># Nacos 使用 JWT (JSON Web Token) 机制，该密钥用于签名和验证 Token。</span></span><br><span class="line"><span class="comment"># 在生产环境中，必须将其设置为一个复杂的、唯一的 Base64 字符串，并确保所有 Nacos 节点使用相同的密钥（集群模式下）</span></span><br><span class="line"><span class="attr">nacos.core.auth.plugin.nacos.token.secret.key</span>=<span class="string">替换为你的Base64密钥</span></span><br></pre></td></tr></table></figure><h4 id="（3）修改端口（可选）">（3）修改端口（可选）</h4><p>Nacos 3.x 已把 <strong>Server API</strong> 与 <strong>控制台</strong> 拆成两个独立 HTTP 端口。改端口时请使用下面专用配置，<strong>不要再写 <code>server.port</code></strong>（它是 Spring Boot 全局 HTTP 端口，容易让 Server 与控制台抢同一端口而启动失败）。参见 <a href="https://nacos.io/docs/latest/manual/admin/system-configurations/">系统参数</a>。</p><table><thead><tr><th>配置项</th><th>默认值</th><th>说明</th></tr></thead><tbody><tr><td><code>nacos.server.main.port</code></td><td><code>8848</code></td><td>Server 主端口（HTTP OpenAPI / Admin）</td></tr><tr><td><code>nacos.console.port</code></td><td><code>8080</code></td><td>控制台端口</td></tr><tr><td>gRPC <code>主端口 + 1000</code></td><td><code>9848</code></td><td>客户端 gRPC（<strong>随主端口偏移，不能单独随意指定</strong>）</td></tr><tr><td>gRPC <code>主端口 + 1001</code></td><td><code>9849</code></td><td>服务端间 gRPC</td></tr><tr><td>Raft <code>主端口 - 1000</code></td><td><code>7848</code></td><td>集群 Raft（单机可忽略）</td></tr></tbody></table><p><code>conf/application.properties</code> 示例（把主端口改成 <code>8849</code>、控制台改成 <code>8081</code>）：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">nacos.server.main.port</span>=<span class="string">8849</span></span><br><span class="line"><span class="attr">nacos.console.port</span>=<span class="string">8081</span></span><br></pre></td></tr></table></figure><p>改完后实际端口变为：</p><table><thead><tr><th>用途</th><th>新端口</th></tr></thead><tbody><tr><td>控制台</td><td><code>8081</code> → <code>http://127.0.0.1:8081/index.html</code></td></tr><tr><td>Server 主端口</td><td><code>8849</code></td></tr><tr><td>客户端 gRPC</td><td><code>9849</code>（<code>8849 + 1000</code>）</td></tr></tbody></table><p>客户端 <code>spring.cloud.nacos.server-addr</code> 只填主端口即可，例如 <code>127.0.0.1:8849</code>；gRPC 端口由客户端按同样偏移规则自动推算。</p><p>防火墙 / 安全组至少放行：<strong>控制台端口、主端口、客户端 gRPC（主端口+1000）</strong>。</p><h4 id="（4）启动-停止">（4）启动 / 停止</h4><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"># Linux / macOS 单机模式</span></span><br><span class="line">sh bin/startup.sh -m standalone</span><br><span class="line"><span class="comment"># Ubuntu 若 [[ 报错，改用：</span></span><br><span class="line"><span class="comment"># bash bin/startup.sh -m standalone</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 指定jdk环境，也可以将 JAVA_HOME 添加到 bin/startup.sh 中</span></span><br><span class="line">JAVA_HOME=/usr/local/jvm/jdk17  \</span><br><span class="line">  sh bin/startup.sh -m standalone</span><br><span class="line"></span><br><span class="line"><span class="comment"># 查看启动日志</span></span><br><span class="line"><span class="built_in">tail</span> -f logs/start.out</span><br></pre></td></tr></table></figure><p>看到类似日志表示成功：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Nacos started successfully in stand alone mode. use embedded storage</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">sh bin/shutdown.sh</span><br></pre></td></tr></table></figure><h4 id="（5）验证">（5）验证</h4><ol><li class="lvl-3"><p>浏览器打开：<code>http://127.0.0.1:8080/index.html</code>（若改过控制台端口则替换），按提示初始化 <code>nacos</code> 管理员密码。</p></li><li class="lvl-3"><p>或用 API <strong>首次</strong>初始化管理员密码（密码为空则会随机生成，务必保存）：</p></li></ol><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">curl -X POST <span class="string">&#x27;http://127.0.0.1:8848/nacos/v3/auth/user/admin&#x27;</span> \</span><br><span class="line">  -d <span class="string">&#x27;password=YourStrongPassword&#x27;</span></span><br></pre></td></tr></table></figure><div class="note warning"><p>该接口<strong>只能在尚未创建管理员用户时调用一次</strong>。成功后再次调用会返回类似：</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span><span class="attr">&quot;code&quot;</span><span class="punctuation">:</span><span class="number">409</span><span class="punctuation">,</span><span class="attr">&quot;message&quot;</span><span class="punctuation">:</span><span class="string">&quot;have admin user cannot use it.&quot;</span><span class="punctuation">,</span><span class="attr">&quot;data&quot;</span><span class="punctuation">:</span><span class="literal"><span class="keyword">null</span></span><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>说明管理员 <code>nacos</code> 已存在，此时应改用登录接口拿 <code>accessToken</code>，而不是继续调 <code>/admin</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">curl -s -X POST <span class="string">&#x27;http://127.0.0.1:8848/nacos/v3/auth/user/login&#x27;</span> \</span><br><span class="line">  -d <span class="string">&#x27;username=nacos&#x27;</span> \</span><br><span class="line">  -d <span class="string">&#x27;password=当初初始化时设置的密码&#x27;</span></span><br></pre></td></tr></table></figure></div><ol start="3"><li class="lvl-3"><p>服务注册 / 发现快速探测（v3 Client API）。</p></li></ol><p>若已开启客户端鉴权（<code>nacos.core.auth.enabled=true</code>，报 <code>401 User not found</code> / <code>403 Forbidden</code> 时就是这种情况），须先登录拿 <code>accessToken</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 登录（密码为你初始化的 nacos 管理员密码）</span></span><br><span class="line">TOKEN=$(curl -s -X POST <span class="string">&#x27;http://127.0.0.1:8848/nacos/v3/auth/user/login&#x27;</span> \</span><br><span class="line">  -d <span class="string">&#x27;username=nacos&#x27;</span> \</span><br><span class="line">  -d <span class="string">&#x27;password=YourStrongPassword&#x27;</span> | sed -n <span class="string">&#x27;s/.*&quot;accessToken&quot;:&quot;\([^&quot;]*\)&quot;.*/\1/p&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;TOKEN=<span class="variable">$TOKEN</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 注册（Header 或 query 携带 accessToken 均可）</span></span><br><span class="line">curl -X POST <span class="string">&quot;http://127.0.0.1:8848/nacos/v3/client/ns/instance?serviceName=quickstart.test.service&amp;ip=127.0.0.1&amp;port=8080&amp;accessToken=<span class="variable">$&#123;TOKEN&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 发现</span></span><br><span class="line">curl -X GET <span class="string">&quot;http://127.0.0.1:8848/nacos/v3/client/ns/instance/list?serviceName=quickstart.test.service&amp;accessToken=<span class="variable">$&#123;TOKEN&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><p>未开启客户端鉴权时，可省略登录，直接调用无 <code>accessToken</code> 的 URL（官方快速开始示例即为此种情况）。</p><h3 id="2-方式二：Docker-单机（嵌入式-Derby）">2. 方式二：Docker 单机（嵌入式 Derby）</h3><p>官方镜像：<code>nacos/nacos-server:v3.0.3</code>。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="built_in">export</span> NACOS_AUTH_TOKEN=<span class="string">&quot;<span class="subst">$(openssl rand -base64 48)</span>&quot;</span></span><br><span class="line"><span class="built_in">export</span> NACOS_AUTH_IDENTITY_KEY=<span class="string">&quot;nacos_identity_key&quot;</span></span><br><span class="line"><span class="built_in">export</span> NACOS_AUTH_IDENTITY_VALUE=<span class="string">&quot;nacos_identity_value&quot;</span></span><br><span class="line"></span><br><span class="line">docker pull nacos/nacos-server:v3.0.3</span><br><span class="line"></span><br><span class="line">docker run -d \</span><br><span class="line">  --name nacos-standalone \</span><br><span class="line">  -e MODE=standalone \</span><br><span class="line">  -e NACOS_AUTH_TOKEN=<span class="string">&quot;<span class="variable">$&#123;NACOS_AUTH_TOKEN&#125;</span>&quot;</span> \</span><br><span class="line">  -e NACOS_AUTH_IDENTITY_KEY=<span class="string">&quot;<span class="variable">$&#123;NACOS_AUTH_IDENTITY_KEY&#125;</span>&quot;</span> \</span><br><span class="line">  -e NACOS_AUTH_IDENTITY_VALUE=<span class="string">&quot;<span class="variable">$&#123;NACOS_AUTH_IDENTITY_VALUE&#125;</span>&quot;</span> \</span><br><span class="line">  -e JVM_XMS=512m \</span><br><span class="line">  -e JVM_XMX=512m \</span><br><span class="line">  -p 8080:8080 \</span><br><span class="line">  -p 8848:8848 \</span><br><span class="line">  -p 9848:9848 \</span><br><span class="line">  nacos/nacos-server:v3.0.3</span><br></pre></td></tr></table></figure><p>若要改容器内端口，使用镜像环境变量（对应配置见上一节）：</p><table><thead><tr><th>环境变量</th><th>对应配置</th><th>默认</th></tr></thead><tbody><tr><td><code>NACOS_APPLICATION_PORT</code></td><td><code>nacos.server.main.port</code></td><td><code>8848</code></td></tr><tr><td><code>NACOS_CONSOLE_PORT</code></td><td><code>nacos.console.port</code></td><td><code>8080</code></td></tr></tbody></table><p>示例：主端口 <code>8849</code>、控制台 <code>8081</code>（gRPC 随之变为 <code>9849</code>，映射要一起改）：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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">docker run -d \</span><br><span class="line">  --name nacos-standalone \</span><br><span class="line">  -e MODE=standalone \</span><br><span class="line">  -e NACOS_APPLICATION_PORT=8849 \</span><br><span class="line">  -e NACOS_CONSOLE_PORT=8081 \</span><br><span class="line">  -e NACOS_AUTH_TOKEN=<span class="string">&quot;<span class="variable">$&#123;NACOS_AUTH_TOKEN&#125;</span>&quot;</span> \</span><br><span class="line">  -e NACOS_AUTH_IDENTITY_KEY=<span class="string">&quot;<span class="variable">$&#123;NACOS_AUTH_IDENTITY_KEY&#125;</span>&quot;</span> \</span><br><span class="line">  -e NACOS_AUTH_IDENTITY_VALUE=<span class="string">&quot;<span class="variable">$&#123;NACOS_AUTH_IDENTITY_VALUE&#125;</span>&quot;</span> \</span><br><span class="line">  -e JVM_XMS=512m \</span><br><span class="line">  -e JVM_XMX=512m \</span><br><span class="line">  -p 8081:8081 \</span><br><span class="line">  -p 8849:8849 \</span><br><span class="line">  -p 9849:9849 \</span><br><span class="line">  nacos/nacos-server:v3.0.3</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 logs -f nacos-standalone</span><br></pre></td></tr></table></figure><p>出现 <code>Nacos started successfully ...</code> 后，访问 <code>http://127.0.0.1:8080/index.html</code> 初始化管理员密码。</p><p>% note info %<br>低配机器务必限制 JVM，否则容易 OOM / 卡死。生产请改用外置 MySQL，详见下文「三、MySQL 持久化」。</p><p>% endnote %</p><h3 id="3-Docker-Compose-最小示例">3. Docker Compose 最小示例</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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"># docker-compose-nacos.yml</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">nacos:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">nacos/nacos-server:v3.0.3</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">nacos</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">MODE:</span> <span class="string">standalone</span></span><br><span class="line">      <span class="comment"># 如需改端口，取消注释并同步改 ports 映射与 gRPC（主端口+1000）</span></span><br><span class="line">      <span class="comment"># NACOS_APPLICATION_PORT: 8849</span></span><br><span class="line">      <span class="comment"># NACOS_CONSOLE_PORT: 8081</span></span><br><span class="line">      <span class="attr">NACOS_AUTH_TOKEN:</span> <span class="string">$&#123;NACOS_AUTH_TOKEN&#125;</span></span><br><span class="line">      <span class="attr">NACOS_AUTH_IDENTITY_KEY:</span> <span class="string">$&#123;NACOS_AUTH_IDENTITY_KEY&#125;</span></span><br><span class="line">      <span class="attr">NACOS_AUTH_IDENTITY_VALUE:</span> <span class="string">$&#123;NACOS_AUTH_IDENTITY_VALUE&#125;</span></span><br><span class="line">      <span class="attr">JVM_XMS:</span> <span class="string">512m</span></span><br><span class="line">      <span class="attr">JVM_XMX:</span> <span class="string">512m</span></span><br><span class="line">      <span class="attr">TZ:</span> <span class="string">Asia/Shanghai</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8080:8080&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8848:8848&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;9848:9848&quot;</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br></pre></td></tr></table></figure><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="built_in">export</span> NACOS_AUTH_TOKEN=<span class="string">&quot;<span class="subst">$(openssl rand -base64 32)</span>&quot;</span></span><br><span class="line"><span class="built_in">export</span> NACOS_AUTH_IDENTITY_KEY=<span class="string">&quot;nacos_identity_key&quot;</span></span><br><span class="line"><span class="built_in">export</span> NACOS_AUTH_IDENTITY_VALUE=<span class="string">&quot;nacos_identity_value&quot;</span></span><br><span class="line"></span><br><span class="line">docker compose -f docker-compose-nacos.yml up -d</span><br></pre></td></tr></table></figure><hr><h2 id="三、MySQL-持久化">三、MySQL 持久化</h2><p>单机快速体验可用嵌入式 Derby；<strong>配置中心数据、用户权限等需要跨重启保活，或后续要做集群时，必须切换外置 MySQL</strong>。官方部署说明：<a href="https://nacos.io/docs/v3.0/manual/admin/deployment/deployment-standalone/">单机模式部署</a>、<a href="https://nacos.io/docs/v3.0/manual/admin/deployment/deployment-cluster/">集群模式部署</a>。</p><h3 id="1-准备数据库">1. 准备数据库</h3><ol><li class="lvl-3"><p>MySQL <strong>5.6.5+</strong>（建议 8.0），字符集建议 <code>utf8mb4</code>。</p></li><li class="lvl-3"><p>创建库与账号（密码请自行替换）：</p></li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> DATABASE nacos <span class="keyword">DEFAULT</span> <span class="keyword">CHARACTER SET</span> utf8mb4 <span class="keyword">COLLATE</span> utf8mb4_unicode_ci;</span><br><span class="line"></span><br><span class="line"><span class="keyword">CREATE</span> <span class="keyword">USER</span> <span class="string">&#x27;nacos&#x27;</span>@<span class="string">&#x27;%&#x27;</span> IDENTIFIED <span class="keyword">BY</span> <span class="string">&#x27;Nacos_Db_Passw0rd&#x27;</span>;</span><br><span class="line"><span class="keyword">GRANT</span> <span class="keyword">ALL</span> PRIVILEGES <span class="keyword">ON</span> nacos.<span class="operator">*</span> <span class="keyword">TO</span> <span class="string">&#x27;nacos&#x27;</span>@<span class="string">&#x27;%&#x27;</span>;</span><br><span class="line">FLUSH PRIVILEGES;</span><br></pre></td></tr></table></figure><ol start="3"><li class="lvl-3"><p>导入官方表结构（发行包内 <code>conf/mysql-schema.sql</code>，或从 <a href="https://github.com/alibaba/nacos/blob/3.0.3/distribution/conf/mysql-schema.sql">Nacos 源码 conf</a> 下载同版本脚本）：</p></li></ol><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"># 在 nacos 解压目录执行</span></span><br><span class="line">mysql -h127.0.0.1 -P3306 -unacos -p nacos &lt; conf/mysql-schema.sql</span><br></pre></td></tr></table></figure><div class="note warning"><p>请使用与 <strong>Nacos 3.0.3</strong> 同版本的 <code>mysql-schema.sql</code>，勿混用 2.x 脚本。初始化只需执行一次；多节点集群共享同一库。</p></div><h3 id="2-二进制包：改配置后单机启动">2. 二进制包：改配置后单机启动</h3><p>编辑 <code>conf/application.properties</code>，取消 / 补充 MySQL 相关项（示例）：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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="comment">### 使用 MySQL 数据源（与 3.0.3 发行包 conf/application.properties 一致）</span></span><br><span class="line"><span class="attr">spring.sql.init.platform</span>=<span class="string">mysql</span></span><br><span class="line"></span><br><span class="line"><span class="attr">db.num</span>=<span class="string">1</span></span><br><span class="line"><span class="attr">db.url.0</span>=<span class="string">jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&amp;connectTimeout=1000&amp;socketTimeout=3000&amp;autoReconnect=true&amp;useUnicode=true&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true</span></span><br><span class="line"><span class="attr">db.user</span>=<span class="string">nacos</span></span><br><span class="line"><span class="attr">db.password</span>=<span class="string">Nacos_Db_Passw0rd</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">### 鉴权（与第二节一致，必须已配置）</span></span><br><span class="line"><span class="attr">nacos.core.auth.enabled</span>=<span class="string">true</span></span><br><span class="line"><span class="attr">nacos.core.auth.system.type</span>=<span class="string">nacos</span></span><br><span class="line"><span class="attr">nacos.core.auth.plugin.nacos.token.secret.key</span>=<span class="string">替换为你的Base64密钥</span></span><br><span class="line"><span class="attr">nacos.core.auth.server.identity.key</span>=<span class="string">替换为你的identityKey</span></span><br><span class="line"><span class="attr">nacos.core.auth.server.identity.value</span>=<span class="string">替换为你的identityValue</span></span><br></pre></td></tr></table></figure><p>启动方式与单机 Derby 相同：</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">sh bin/startup.sh -m standalone</span><br></pre></td></tr></table></figure><p>成功时日志类似：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Nacos started successfully in stand alone mode. use external storage</span><br></pre></td></tr></table></figure><p>对比 Derby 时的 <code>use embedded storage</code>，若仍显示 embedded，说明 MySQL 配置未生效。</p><h3 id="3-Docker：单机-外置-Compose-内置-MySQL">3. Docker：单机 + 外置 / Compose 内置 MySQL</h3><h4 id="（1）连接已有-MySQL">（1）连接已有 MySQL</h4><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><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">docker run -d \</span><br><span class="line">  --name nacos-standalone-mysql \</span><br><span class="line">  -e MODE=standalone \</span><br><span class="line">  -e SPRING_DATASOURCE_PLATFORM=mysql \</span><br><span class="line">  -e MYSQL_SERVICE_HOST=192.168.1.10 \</span><br><span class="line">  -e MYSQL_SERVICE_PORT=3306 \</span><br><span class="line">  -e MYSQL_SERVICE_DB_NAME=nacos \</span><br><span class="line">  -e MYSQL_SERVICE_USER=nacos \</span><br><span class="line">  -e MYSQL_SERVICE_PASSWORD=Nacos_Db_Passw0rd \</span><br><span class="line">  -e MYSQL_SERVICE_DB_PARAM=<span class="string">&#x27;characterEncoding=utf8&amp;connectTimeout=1000&amp;socketTimeout=3000&amp;autoReconnect=true&amp;useUnicode=true&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true&#x27;</span> \</span><br><span class="line">  -e NACOS_AUTH_TOKEN=<span class="string">&quot;<span class="variable">$&#123;NACOS_AUTH_TOKEN&#125;</span>&quot;</span> \</span><br><span class="line">  -e NACOS_AUTH_IDENTITY_KEY=<span class="string">&quot;<span class="variable">$&#123;NACOS_AUTH_IDENTITY_KEY&#125;</span>&quot;</span> \</span><br><span class="line">  -e NACOS_AUTH_IDENTITY_VALUE=<span class="string">&quot;<span class="variable">$&#123;NACOS_AUTH_IDENTITY_VALUE&#125;</span>&quot;</span> \</span><br><span class="line">  -e JVM_XMS=512m \</span><br><span class="line">  -e JVM_XMX=512m \</span><br><span class="line">  -p 8080:8080 \</span><br><span class="line">  -p 8848:8848 \</span><br><span class="line">  -p 9848:9848 \</span><br><span class="line">  nacos/nacos-server:v3.0.3</span><br></pre></td></tr></table></figure><table><thead><tr><th>环境变量</th><th>说明</th></tr></thead><tbody><tr><td><code>SPRING_DATASOURCE_PLATFORM</code></td><td>设为 <code>mysql</code> 启用外置库</td></tr><tr><td><code>MYSQL_SERVICE_HOST</code> / <code>PORT</code> / <code>DB_NAME</code></td><td>库地址</td></tr><tr><td><code>MYSQL_SERVICE_USER</code> / <code>PASSWORD</code></td><td>账号密码（勿含逗号 <code>,</code>）</td></tr><tr><td><code>MYSQL_SERVICE_DB_PARAM</code></td><td>JDBC 附加参数</td></tr></tbody></table><p>容器启动前请已在目标库执行过 <code>mysql-schema.sql</code>。</p><h4 id="（2）Docker-Compose-一键（Nacos-MySQL）">（2）Docker Compose 一键（Nacos + MySQL）</h4><p>也可直接参考 <a href="https://github.com/nacos-group/nacos-docker">nacos-docker</a> 的 <code>example/standalone-mysql.yaml</code>（版本在 <code>.env</code> 固定为 <code>v3.0.3</code>，并先跑 <code>mysql-init.sh</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><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="comment"># docker-compose-nacos-mysql.yml</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">mysql:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">mysql:8.0.30</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">nacos-mysql</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">MYSQL_ROOT_PASSWORD:</span> <span class="string">root</span></span><br><span class="line">      <span class="attr">MYSQL_DATABASE:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">MYSQL_USER:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">MYSQL_PASSWORD:</span> <span class="string">Nacos_Db_Passw0rd</span></span><br><span class="line">      <span class="attr">TZ:</span> <span class="string">Asia/Shanghai</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="comment"># 将 mysql-schema.sql 放到 ./mysql-init/ 下，首次启动自动导入</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./mysql-init:/docker-entrypoint-initdb.d</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">nacos-mysql-data:/var/lib/mysql</span></span><br><span class="line">    <span class="attr">command:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">--character-set-server=utf8mb4</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">--collation-server=utf8mb4_unicode_ci</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;3306:3306&quot;</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;mysqladmin&quot;</span>, <span class="string">&quot;ping&quot;</span>, <span class="string">&quot;-h&quot;</span>, <span class="string">&quot;localhost&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">5s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">10s</span></span><br><span class="line">      <span class="attr">retries:</span> <span class="number">10</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">nacos:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">nacos/nacos-server:v3.0.3</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">nacos</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="attr">mysql:</span></span><br><span class="line">        <span class="attr">condition:</span> <span class="string">service_healthy</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">MODE:</span> <span class="string">standalone</span></span><br><span class="line">      <span class="attr">SPRING_DATASOURCE_PLATFORM:</span> <span class="string">mysql</span></span><br><span class="line">      <span class="attr">MYSQL_SERVICE_HOST:</span> <span class="string">mysql</span></span><br><span class="line">      <span class="attr">MYSQL_SERVICE_PORT:</span> <span class="number">3306</span></span><br><span class="line">      <span class="attr">MYSQL_SERVICE_DB_NAME:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">MYSQL_SERVICE_USER:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">MYSQL_SERVICE_PASSWORD:</span> <span class="string">Nacos_Db_Passw0rd</span></span><br><span class="line">      <span class="attr">MYSQL_SERVICE_DB_PARAM:</span> <span class="string">characterEncoding=utf8&amp;connectTimeout=1000&amp;socketTimeout=3000&amp;autoReconnect=true&amp;useUnicode=true&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true</span></span><br><span class="line">      <span class="attr">NACOS_AUTH_TOKEN:</span> <span class="string">$&#123;NACOS_AUTH_TOKEN&#125;</span></span><br><span class="line">      <span class="attr">NACOS_AUTH_IDENTITY_KEY:</span> <span class="string">$&#123;NACOS_AUTH_IDENTITY_KEY&#125;</span></span><br><span class="line">      <span class="attr">NACOS_AUTH_IDENTITY_VALUE:</span> <span class="string">$&#123;NACOS_AUTH_IDENTITY_VALUE&#125;</span></span><br><span class="line">      <span class="attr">JVM_XMS:</span> <span class="string">512m</span></span><br><span class="line">      <span class="attr">JVM_XMX:</span> <span class="string">512m</span></span><br><span class="line">      <span class="attr">TZ:</span> <span class="string">Asia/Shanghai</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8080:8080&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8848:8848&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;9848:9848&quot;</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">nacos-mysql-data:</span></span><br></pre></td></tr></table></figure><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="built_in">mkdir</span> -p mysql-init</span><br><span class="line"><span class="comment"># 从发行包拷贝同版本脚本</span></span><br><span class="line"><span class="built_in">cp</span> /path/to/nacos/conf/mysql-schema.sql mysql-init/</span><br><span class="line"></span><br><span class="line"><span class="built_in">export</span> NACOS_AUTH_TOKEN=<span class="string">&quot;<span class="subst">$(openssl rand -base64 32)</span>&quot;</span></span><br><span class="line"><span class="built_in">export</span> NACOS_AUTH_IDENTITY_KEY=<span class="string">&quot;nacos_identity_key&quot;</span></span><br><span class="line"><span class="built_in">export</span> NACOS_AUTH_IDENTITY_VALUE=<span class="string">&quot;nacos_identity_value&quot;</span></span><br><span class="line"></span><br><span class="line">docker compose -f docker-compose-nacos-mysql.yml up -d</span><br></pre></td></tr></table></figure><p>常见问题：</p><ol><li class="lvl-3"><p><strong><code>Public Key Retrieval is not allowed</code></strong>：JDBC URL / <code>MYSQL_SERVICE_DB_PARAM</code> 追加 <code>allowPublicKeyRetrieval=true</code>。</p></li><li class="lvl-3"><p><strong><code>db.num is null</code> / 连不上库</strong>：确认 <code>spring.sql.init.platform=mysql</code>（或环境变量 <code>SPRING_DATASOURCE_PLATFORM=mysql</code>）及 <code>db.*</code> 完整。</p></li><li class="lvl-3"><p><strong>重启后数据仍丢</strong>：可能仍跑在 Derby，或 MySQL volume 未挂载成功。</p></li></ol><hr><h2 id="四、集群搭建">四、集群搭建</h2><p>生产推荐：<strong>≥ 3 个 Nacos 节点 + 外置 MySQL（建议高可用）+ 域名 / 内网 SLB 对外暴露主端口与客户端 gRPC</strong>。直连任意单节点 IP 也可工作，但节点故障后客户端需改配置，运维成本更高。架构说明见 <a href="https://nacos.io/docs/v3.0/manual/admin/deployment/deployment-cluster/">集群模式部署</a>。</p><div class="note warning"><p>集群模式下：</p><ol><li class="lvl-3"><strong>必须共享同一套外置 MySQL</strong>（嵌入式 Derby 虽可用 <code>-p embedded</code> 靠 Raft 组逻辑集群，排障成本高，官方不推荐生产）。</li><li class="lvl-3">所有节点的 <strong>鉴权三件套必须完全一致</strong>（<code>token.secret.key</code> / <code>identity.key</code> / <code>identity.value</code>）。</li><li class="lvl-3">节点间需放行：主端口、客户端 gRPC（+1000）、服务端 gRPC（+1001）、Raft（-1000）。</li><li class="lvl-3">启动命令<strong>不要</strong>再带 <code>-m standalone</code>。</li></ol></div><h3 id="1-二进制包三节点示例">1. 二进制包三节点示例</h3><p>假设三台机器（或同机不同端口，演示时可改；生产请每机一进程）：</p><table><thead><tr><th>节点</th><th>IP</th><th>主端口</th></tr></thead><tbody><tr><td>nacos-1</td><td><code>192.168.1.11</code></td><td><code>8848</code></td></tr><tr><td>nacos-2</td><td><code>192.168.1.12</code></td><td><code>8848</code></td></tr><tr><td>nacos-3</td><td><code>192.168.1.13</code></td><td><code>8848</code></td></tr></tbody></table><h4 id="（1）各节点统一-application-properties">（1）各节点统一 <code>application.properties</code></h4><p>在上一节 MySQL 配置基础上，保证三台鉴权参数相同，并按需指定本机地址（多网卡时尤其重要）：</p><figure class="highlight properties"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><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="comment">### 多网卡时可固定对外 IP，避免注册成错误网卡</span></span><br><span class="line"><span class="comment"># nacos.inetutils.ip-address=192.168.1.11</span></span><br><span class="line"></span><br><span class="line"><span class="attr">spring.sql.init.platform</span>=<span class="string">mysql</span></span><br><span class="line"><span class="attr">db.num</span>=<span class="string">1</span></span><br><span class="line"><span class="attr">db.url.0</span>=<span class="string">jdbc:mysql://192.168.1.10:3306/nacos?characterEncoding=utf8&amp;connectTimeout=1000&amp;socketTimeout=3000&amp;autoReconnect=true&amp;useUnicode=true&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true</span></span><br><span class="line"><span class="attr">db.user</span>=<span class="string">nacos</span></span><br><span class="line"><span class="attr">db.password</span>=<span class="string">Nacos_Db_Passw0rd</span></span><br><span class="line"></span><br><span class="line"><span class="attr">nacos.core.auth.enabled</span>=<span class="string">true</span></span><br><span class="line"><span class="attr">nacos.core.auth.console.enabled</span>=<span class="string">true</span></span><br><span class="line"><span class="attr">nacos.core.auth.system.type</span>=<span class="string">nacos</span></span><br><span class="line"><span class="attr">nacos.core.auth.plugin.nacos.token.secret.key</span>=<span class="string">替换为你的Base64密钥</span></span><br><span class="line"><span class="attr">nacos.core.auth.server.identity.key</span>=<span class="string">替换为你的identityKey</span></span><br><span class="line"><span class="attr">nacos.core.auth.server.identity.value</span>=<span class="string">替换为你的identityValue</span></span><br></pre></td></tr></table></figure><h4 id="（2）各节点相同的-conf-cluster-conf">（2）各节点相同的 <code>conf/cluster.conf</code></h4><p>三台机器内容一致，每行 <code>ip:主端口</code>：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><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"># conf/cluster.conf</span><br><span class="line">192.168.1.11:8848</span><br><span class="line">192.168.1.12:8848</span><br><span class="line">192.168.1.13:8848</span><br></pre></td></tr></table></figure><h4 id="（3）启动与验证">（3）启动与验证</h4><p>每台分别执行（<strong>集群模式，无 <code>-m standalone</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></pre></td><td class="code"><pre><span class="line">sh bin/startup.sh</span><br><span class="line"><span class="comment"># Ubuntu：bash bin/startup.sh</span></span><br></pre></td></tr></table></figure><p>成功日志类似：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Nacos started successfully in cluster mode. use external storage</span><br></pre></td></tr></table></figure><p>任选一台控制台（或经 SLB）打开：<code>http://任意节点控制台端口/index.html</code>。管理员密码只需初始化一次（数据在 MySQL）。</p><p>控制台「集群管理」应能看到 3 个节点；也可用健康检查 / 成员列表 API（需先登录拿 token，方式同第二节）。</p><p>客户端 <code>server-addr</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></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="comment"># 推荐：VIP / 域名（内网 SLB 同时转发 8848 与 9848）</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">nacos.example.internal:8848</span></span><br><span class="line">      <span class="comment"># 或直连多地址（逗号分隔）</span></span><br><span class="line">      <span class="comment"># server-addr: 192.168.1.11:8848,192.168.1.12:8848,192.168.1.13:8848</span></span><br></pre></td></tr></table></figure><p>% note tip %<br>若前面挂了四层负载或 Nginx，<strong>8848 与 9848 都要转发且保持「主端口 +1000 = gRPC」的偏移</strong>。只转 8848、不转 gRPC，或把 gRPC 配成 HTTP/HTTP2，会出现注册失败或频繁超时。完整示例见下文「Nginx 反向代理」。</p><p>% endnote %</p><h3 id="2-Docker-Compose-集群示例（hostname-模式）">2. Docker Compose 集群示例（hostname 模式）</h3><p>官方示例：<a href="https://github.com/nacos-group/nacos-docker/blob/master/example/cluster-hostname.yaml">nacos-docker <code>example/cluster-hostname.yaml</code></a>。核心环境变量：</p><table><thead><tr><th>环境变量</th><th>示例</th><th>说明</th></tr></thead><tbody><tr><td><code>MODE</code></td><td><code>cluster</code>（镜像默认）</td><td>集群模式</td></tr><tr><td><code>PREFER_HOST_MODE</code></td><td><code>hostname</code></td><td>节点互相用 hostname 发现</td></tr><tr><td><code>NACOS_SERVERS</code></td><td><code>nacos1:8848 nacos2:8848 nacos3:8848</code></td><td>空格分隔的成员列表</td></tr><tr><td><code>SPRING_DATASOURCE_PLATFORM</code></td><td><code>mysql</code></td><td>外置库</td></tr><tr><td><code>NACOS_AUTH_*</code></td><td>与单机相同</td><td><strong>三节点必须相同</strong></td></tr></tbody></table><p>自写精简示例如下（生产请将 MySQL 换为外部高可用库，并收紧密码）：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># docker-compose-nacos-cluster.yml</span></span><br><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">mysql:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">mysql:8.0.30</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">nacos-mysql</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">MYSQL_ROOT_PASSWORD:</span> <span class="string">root</span></span><br><span class="line">      <span class="attr">MYSQL_DATABASE:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">MYSQL_USER:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">MYSQL_PASSWORD:</span> <span class="string">Nacos_Db_Passw0rd</span></span><br><span class="line">    <span class="attr">volumes:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">./mysql-init:/docker-entrypoint-initdb.d</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">nacos-mysql-data:/var/lib/mysql</span></span><br><span class="line">    <span class="attr">healthcheck:</span></span><br><span class="line">      <span class="attr">test:</span> [<span class="string">&quot;CMD&quot;</span>, <span class="string">&quot;mysqladmin&quot;</span>, <span class="string">&quot;ping&quot;</span>, <span class="string">&quot;-h&quot;</span>, <span class="string">&quot;localhost&quot;</span>]</span><br><span class="line">      <span class="attr">interval:</span> <span class="string">5s</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">10s</span></span><br><span class="line">      <span class="attr">retries:</span> <span class="number">10</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">nacos1:</span> <span class="string">&amp;nacos-node</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">nacos/nacos-server:v3.0.3</span></span><br><span class="line">    <span class="attr">hostname:</span> <span class="string">nacos1</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">nacos1</span></span><br><span class="line">    <span class="attr">depends_on:</span></span><br><span class="line">      <span class="attr">mysql:</span></span><br><span class="line">        <span class="attr">condition:</span> <span class="string">service_healthy</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">PREFER_HOST_MODE:</span> <span class="string">hostname</span></span><br><span class="line">      <span class="attr">NACOS_SERVERS:</span> <span class="string">nacos1:8848</span> <span class="string">nacos2:8848</span> <span class="string">nacos3:8848</span></span><br><span class="line">      <span class="attr">SPRING_DATASOURCE_PLATFORM:</span> <span class="string">mysql</span></span><br><span class="line">      <span class="attr">MYSQL_SERVICE_HOST:</span> <span class="string">mysql</span></span><br><span class="line">      <span class="attr">MYSQL_SERVICE_PORT:</span> <span class="number">3306</span></span><br><span class="line">      <span class="attr">MYSQL_SERVICE_DB_NAME:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">MYSQL_SERVICE_USER:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">MYSQL_SERVICE_PASSWORD:</span> <span class="string">Nacos_Db_Passw0rd</span></span><br><span class="line">      <span class="attr">MYSQL_SERVICE_DB_PARAM:</span> <span class="string">characterEncoding=utf8&amp;connectTimeout=1000&amp;socketTimeout=3000&amp;autoReconnect=true&amp;useSSL=false&amp;allowPublicKeyRetrieval=true</span></span><br><span class="line">      <span class="attr">NACOS_AUTH_TOKEN:</span> <span class="string">$&#123;NACOS_AUTH_TOKEN&#125;</span></span><br><span class="line">      <span class="attr">NACOS_AUTH_IDENTITY_KEY:</span> <span class="string">$&#123;NACOS_AUTH_IDENTITY_KEY&#125;</span></span><br><span class="line">      <span class="attr">NACOS_AUTH_IDENTITY_VALUE:</span> <span class="string">$&#123;NACOS_AUTH_IDENTITY_VALUE&#125;</span></span><br><span class="line">      <span class="attr">JVM_XMS:</span> <span class="string">512m</span></span><br><span class="line">      <span class="attr">JVM_XMX:</span> <span class="string">512m</span></span><br><span class="line">      <span class="attr">TZ:</span> <span class="string">Asia/Shanghai</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8080:8080&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8848:8848&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;9848:9848&quot;</span></span><br><span class="line">    <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">nacos2:</span></span><br><span class="line">    <span class="string">&lt;&lt;:</span> <span class="string">*nacos-node</span></span><br><span class="line">    <span class="attr">hostname:</span> <span class="string">nacos2</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">nacos2</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8081:8080&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8849:8848&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;9849:9848&quot;</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">nacos3:</span></span><br><span class="line">    <span class="string">&lt;&lt;:</span> <span class="string">*nacos-node</span></span><br><span class="line">    <span class="attr">hostname:</span> <span class="string">nacos3</span></span><br><span class="line">    <span class="attr">container_name:</span> <span class="string">nacos3</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8082:8080&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;8850:8848&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;9850:9848&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">volumes:</span></span><br><span class="line">  <span class="attr">nacos-mysql-data:</span></span><br></pre></td></tr></table></figure><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="built_in">mkdir</span> -p mysql-init</span><br><span class="line"><span class="built_in">cp</span> /path/to/nacos/conf/mysql-schema.sql mysql-init/</span><br><span class="line"></span><br><span class="line"><span class="built_in">export</span> NACOS_AUTH_TOKEN=<span class="string">&quot;<span class="subst">$(openssl rand -base64 32)</span>&quot;</span></span><br><span class="line"><span class="built_in">export</span> NACOS_AUTH_IDENTITY_KEY=<span class="string">&quot;nacos_identity_key&quot;</span></span><br><span class="line"><span class="built_in">export</span> NACOS_AUTH_IDENTITY_VALUE=<span class="string">&quot;nacos_identity_value&quot;</span></span><br><span class="line"></span><br><span class="line">docker compose -f docker-compose-nacos-cluster.yml up -d</span><br><span class="line">docker compose -f docker-compose-nacos-cluster.yml logs -f nacos1</span><br></pre></td></tr></table></figure><p>本机快速验证时，应用可临时连 <code>127.0.0.1:8848</code>（或逗号列出 <code>8848,8849,8850</code>）。真实环境仍建议用域名 / SLB / Nginx 统一入口。</p><h3 id="3-Nginx-反向代理示例">3. Nginx 反向代理示例</h3><p>官方说明：<a href="https://nacos.io/docs/v3.0/manual/admin/deployment/deployment-overview/">部署架构概览</a>、<a href="https://nacos.io/docs/latest/guide/admin/nginx-cluster-load-balance-guide/">Nginx 负载均衡指南</a>。</p><p>对外建议暴露：</p><table><thead><tr><th>对外端口</th><th>协议</th><th>后端</th><th>说明</th></tr></thead><tbody><tr><td><code>8848</code></td><td>HTTP</td><td>各节点 <code>8848</code></td><td>OpenAPI / 鉴权 login 等</td></tr><tr><td><code>9848</code></td><td><strong>TCP（stream）</strong></td><td>各节点 <code>9848</code></td><td>客户端 gRPC 长连接（<strong>= 8848 + 1000</strong>）</td></tr><tr><td><code>8080</code></td><td>HTTP</td><td>各节点 <code>8080</code></td><td>Nacos 3.x 控制台</td></tr></tbody></table><p><strong>不要</strong>把 gRPC 配成 <code>http</code> / <code>http2</code> 反代，否则连接会被掐断，表现为服务反复上下线。节点间端口 <code>9849</code>、<code>7848</code> 仅内网互通，勿对公网开放。</p><p>先确认 Nginx 带 <code>stream</code> 模块：</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">nginx -V 2&gt;&amp;1 | <span class="built_in">tr</span> <span class="string">&#x27; &#x27;</span> <span class="string">&#x27;\n&#x27;</span> | grep -E <span class="string">&#x27;stream&#x27;</span></span><br><span class="line"><span class="comment"># 应能看到 --with-stream</span></span><br></pre></td></tr></table></figure><h4 id="（1）推荐：保持默认端口偏移（客户端无感）">（1）推荐：保持默认端口偏移（客户端无感）</h4><p><code>server-addr</code> 填 <code>nacos.example.com:8848</code> 时，客户端会自动连 <code>nacos.example.com:9848</code>，因此 Nginx <strong>对外监听端口也必须满足同一偏移</strong>：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># /etc/nginx/nginx.conf 片段（stream 必须与 http 同级，不能写在 http &#123;&#125; 里）</span></span><br><span class="line"></span><br><span class="line"><span class="attribute">worker_processes</span> auto;</span><br><span class="line"></span><br><span class="line"><span class="section">events</span> &#123;</span><br><span class="line">    <span class="attribute">worker_connections</span> <span class="number">10240</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># ---------- gRPC：四层 TCP 反代 ----------</span></span><br><span class="line"><span class="section">stream</span> &#123;</span><br><span class="line">    <span class="section">upstream</span> nacos_grpc &#123;</span><br><span class="line">        <span class="comment"># 可选：按客户端 IP 粘滞，减少长连接漂移</span></span><br><span class="line">        <span class="attribute">hash</span> <span class="variable">$remote_addr</span> consistent;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.11:9848</span> max_fails=<span class="number">3</span> fail_timeout=<span class="number">30s</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.12:9848</span> max_fails=<span class="number">3</span> fail_timeout=<span class="number">30s</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.13:9848</span> max_fails=<span class="number">3</span> fail_timeout=<span class="number">30s</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="section">server</span> &#123;</span><br><span class="line">        <span class="attribute">listen</span> <span class="number">9848</span>;</span><br><span class="line">        <span class="attribute">proxy_pass</span> nacos_grpc;</span><br><span class="line">        <span class="attribute">proxy_connect_timeout</span> <span class="number">10s</span>;</span><br><span class="line">        <span class="comment"># gRPC 长连接，超时宜设大一些</span></span><br><span class="line">        <span class="attribute">proxy_timeout</span> <span class="number">300s</span>;</span><br><span class="line">        <span class="attribute">proxy_next_upstream</span> <span class="literal">on</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"># ---------- HTTP：API + 控制台 ----------</span></span><br><span class="line"><span class="section">http</span> &#123;</span><br><span class="line">    <span class="attribute">include</span>       mime.types;</span><br><span class="line">    <span class="attribute">default_type</span>  application/octet-stream;</span><br><span class="line">    <span class="attribute">sendfile</span>      <span class="literal">on</span>;</span><br><span class="line">    <span class="attribute">keepalive_timeout</span> <span class="number">65</span>;</span><br><span class="line">    <span class="comment"># 配置内容可能较大</span></span><br><span class="line">    <span class="attribute">client_max_body_size</span> <span class="number">20m</span>;</span><br><span class="line"></span><br><span class="line">    <span class="section">upstream</span> nacos_api &#123;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.11:8848</span> max_fails=<span class="number">3</span> fail_timeout=<span class="number">30s</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.12:8848</span> max_fails=<span class="number">3</span> fail_timeout=<span class="number">30s</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.13:8848</span> max_fails=<span class="number">3</span> fail_timeout=<span class="number">30s</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="section">upstream</span> nacos_console &#123;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.11:8080</span> max_fails=<span class="number">3</span> fail_timeout=<span class="number">30s</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.12:8080</span> max_fails=<span class="number">3</span> fail_timeout=<span class="number">30s</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.13:8080</span> max_fails=<span class="number">3</span> fail_timeout=<span class="number">30s</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 客户端 / OpenAPI：http://nacos.example.com:8848</span></span><br><span class="line">    <span class="section">server</span> &#123;</span><br><span class="line">        <span class="attribute">listen</span> <span class="number">8848</span>;</span><br><span class="line">        <span class="attribute">server_name</span> nacos.example.com;</span><br><span class="line"></span><br><span class="line">        <span class="section">location</span> / &#123;</span><br><span class="line">            <span class="attribute">proxy_pass</span> http://nacos_api;</span><br><span class="line">            <span class="attribute">proxy_http_version</span> <span class="number">1</span>.<span class="number">1</span>;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> X-Forwarded-Proto <span class="variable">$scheme</span>;</span><br><span class="line">            <span class="attribute">proxy_connect_timeout</span> <span class="number">5s</span>;</span><br><span class="line">            <span class="attribute">proxy_read_timeout</span> <span class="number">60s</span>;</span><br><span class="line">            <span class="attribute">proxy_send_timeout</span> <span class="number">60s</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="section">location</span> = /nacos/v3/console/health/readiness &#123;</span><br><span class="line">            <span class="attribute">proxy_pass</span> http://nacos_api;</span><br><span class="line">            <span class="attribute">access_log</span> <span class="literal">off</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"># 控制台：http://nacos-console.example.com:8080/index.html</span></span><br><span class="line">    <span class="section">server</span> &#123;</span><br><span class="line">        <span class="attribute">listen</span> <span class="number">8080</span>;</span><br><span class="line">        <span class="attribute">server_name</span> nacos-console.example.com;</span><br><span class="line"></span><br><span class="line">        <span class="section">location</span> / &#123;</span><br><span class="line">            <span class="attribute">proxy_pass</span> http://nacos_console;</span><br><span class="line">            <span class="attribute">proxy_http_version</span> <span class="number">1</span>.<span class="number">1</span>;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> X-Forwarded-Proto <span class="variable">$scheme</span>;</span><br><span class="line">            <span class="comment"># 控制台前端资源 / 接口</span></span><br><span class="line">            <span class="attribute">proxy_connect_timeout</span> <span class="number">5s</span>;</span><br><span class="line">            <span class="attribute">proxy_read_timeout</span> <span class="number">60s</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><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">nginx -t &amp;&amp; nginx -s reload</span><br></pre></td></tr></table></figure><p>应用侧只填 Nginx 入口：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="string">nacos.example.com:8848</span></span><br><span class="line">      <span class="attr">username:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">password:</span> <span class="string">YourStrongPassword</span></span><br></pre></td></tr></table></figure><p>浏览器访问控制台：<code>http://nacos-console.example.com:8080/index.html</code>。</p><h4 id="（2）自定义对外端口时：仍须保持-1000-偏移">（2）自定义对外端口时：仍须保持 +1000 偏移</h4><p>若希望对外用 <code>7847</code>（HTTP）而不想占用 <code>8848</code>，则 gRPC 必须对外监听 <code>8847</code>（= <code>7847 + 1000</code>），客户端才能算出正确端口：</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="section">stream</span> &#123;</span><br><span class="line">    <span class="section">upstream</span> nacos_grpc &#123;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.11:9848</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.12:9848</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.13:9848</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="section">server</span> &#123;</span><br><span class="line">        <span class="attribute">listen</span> <span class="number">8847</span>;          <span class="comment"># = 对外主端口 + 1000</span></span><br><span class="line">        <span class="attribute">proxy_pass</span> nacos_grpc;</span><br><span class="line">        <span class="attribute">proxy_timeout</span> <span class="number">300s</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="section">http</span> &#123;</span><br><span class="line">    <span class="section">upstream</span> nacos_api &#123;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.11:8848</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.12:8848</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.13:8848</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="section">upstream</span> nacos_console &#123;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.11:8080</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.12:8080</span>;</span><br><span class="line">        <span class="attribute">server</span> <span class="number">192.168.1.13:8080</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 对外主端口 7847：可同时按路径拆 API 与控制台（官方指南同思路）</span></span><br><span class="line">    <span class="section">server</span> &#123;</span><br><span class="line">        <span class="attribute">listen</span> <span class="number">7847</span>;</span><br><span class="line">        <span class="attribute">server_name</span> nacos.example.com;</span><br><span class="line"></span><br><span class="line">        <span class="section">location</span> /nacos/ &#123;</span><br><span class="line">            <span class="attribute">proxy_pass</span> http://nacos_api/nacos/;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment"># Nacos 3.x 控制台路径无 /nacos 前缀</span></span><br><span class="line">        <span class="section">location</span> / &#123;</span><br><span class="line">            <span class="attribute">proxy_pass</span> http://nacos_console;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">            <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</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><p>此时客户端：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring.cloud.nacos.server-addr:</span> <span class="string">nacos.example.com:7847</span></span><br></pre></td></tr></table></figure><p>控制台：<code>http://nacos.example.com:7847/index.html</code>。</p><h4 id="（3）HTTPS-控制台（可选）">（3）HTTPS 控制台（可选）</h4><p>仅给控制台上证书即可；客户端注册发现一般仍走内网明文 <code>8848/9848</code>。证书终止在 Nginx，后端继续 <code>http://nacos_console</code>。</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="section">server</span> &#123;</span><br><span class="line">    <span class="attribute">listen</span> <span class="number">443</span> ssl http2;</span><br><span class="line">    <span class="attribute">server_name</span> nacos-console.example.com;</span><br><span class="line"></span><br><span class="line">    <span class="attribute">ssl_certificate</span>     /etc/nginx/certs/nacos-console.crt;</span><br><span class="line">    <span class="attribute">ssl_certificate_key</span> /etc/nginx/certs/nacos-console.key;</span><br><span class="line"></span><br><span class="line">    <span class="section">location</span> / &#123;</span><br><span class="line">        <span class="attribute">proxy_pass</span> http://nacos_console;</span><br><span class="line">        <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>;</span><br><span class="line">        <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">        <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>;</span><br><span class="line">        <span class="attribute">proxy_set_header</span> X-Forwarded-Proto https;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="（4）Nginx-相关注意点">（4）Nginx 相关注意点</h4><ol><li class="lvl-3"><p><strong><code>stream</code> 与 <code>http</code> 同级</strong>：写在 <code>http &#123;&#125;</code> 内会直接报错。</p></li><li class="lvl-3"><p><strong>偏移不能破</strong>：对外 <code>主端口</code> 与 <code>主端口+1000</code> 必须成对出现。</p></li><li class="lvl-3"><p><strong>单机也可用同一套配置</strong>：<code>upstream</code> 里只留一个 <code>server</code> 即可。</p></li><li class="lvl-3"><p><strong>扩容</strong>：新增节点后改 Nginx <code>upstream</code> 再 <code>nginx -s reload</code>，应用无需改 <code>server-addr</code>。</p></li><li class="lvl-3"><p><strong>安全</strong>：生产尽量内网暴露 <code>8848/9848</code>；控制台可单独域名 + HTTPS + IP 白名单。</p></li></ol><h3 id="4-集群常见问题">4. 集群常见问题</h3><ol><li class="lvl-3"><p><strong>节点无法互相发现</strong>：检查 <code>cluster.conf</code> / <code>NACOS_SERVERS</code> 是否互通；防火墙是否放行 <code>8848/9848/9849/7848</code>。</p></li><li class="lvl-3"><p><strong>鉴权不一致导致节点异常</strong>：三件套任一不一致都会出问题，务必三台拷贝同一份配置。</p></li><li class="lvl-3"><p><strong>多网卡注册成内网错误 IP</strong>：设置 <code>nacos.inetutils.ip-address</code> 或 Docker 的 <code>NACOS_SERVER_IP</code>。</p></li><li class="lvl-3"><p><strong>只用 2 节点</strong>：偶数节点在半数故障时易无法选主，生产请用奇数台（≥3）。</p></li><li class="lvl-3"><p><strong>Nginx 后客户端连不上 / 反复掉线</strong>：检查是否遗漏 <code>9848</code> 的 <code>stream</code> TCP 转发，或误用了 HTTP2；对外端口是否满足「主端口 +1000」。</p></li></ol><hr><h2 id="五、应用侧快速接入（SCA-2025-0-0-0）">五、应用侧快速接入（SCA 2025.0.0.0）</h2><p>官方指南：<a href="https://sca.aliyun.com/docs/2025.x/user-guide/nacos/quick-start/">SCA · Nacos 快速开始</a>。</p><h3 id="1-BOM-与依赖">1. BOM 与依赖</h3><p>父 POM / 依赖管理：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependencyManagement</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-dependencies<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.5.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-dependencies<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">version</span>&gt;</span>2025.0.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-alibaba-dependencies<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">version</span>&gt;</span>2025.0.0.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencyManagement</span>&gt;</span></span><br></pre></td></tr></table></figure><p>业务模块按需引入（版本由 BOM 管理）：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 注册发现 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-nacos-discovery<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- 配置中心 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>com.alibaba.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-alibaba-nacos-config<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">&lt;!-- 服务间调用负载均衡（消费端常用） --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.cloud<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-cloud-starter-loadbalancer<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><blockquote><p>Starter 会传递依赖 <code>nacos-client:3.0.3</code>，与上文服务端版本对齐。</p></blockquote><h3 id="2-application-yml-示例">2. <code>application.yml</code> 示例</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">server:</span></span><br><span class="line">  <span class="attr">port:</span> <span class="number">18082</span></span><br><span class="line"></span><br><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">application:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">demo-provider</span></span><br><span class="line">  <span class="attr">config:</span></span><br><span class="line">    <span class="attr">import:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">optional:nacos:demo-provider.properties?refreshEnabled=true</span></span><br><span class="line">  <span class="attr">cloud:</span></span><br><span class="line">    <span class="attr">nacos:</span></span><br><span class="line">      <span class="attr">server-addr:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span><span class="string">:8848</span></span><br><span class="line">      <span class="attr">username:</span> <span class="string">nacos</span></span><br><span class="line">      <span class="attr">password:</span> <span class="string">YourStrongPassword</span></span><br><span class="line">      <span class="attr">discovery:</span></span><br><span class="line">        <span class="attr">server-addr:</span> <span class="string">$&#123;spring.cloud.nacos.server-addr&#125;</span></span><br><span class="line">      <span class="attr">config:</span></span><br><span class="line">        <span class="attr">server-addr:</span> <span class="string">$&#123;spring.cloud.nacos.server-addr&#125;</span></span><br><span class="line">        <span class="attr">file-extension:</span> <span class="string">properties</span></span><br></pre></td></tr></table></figure><p>% note tip %<br><code>2025.0.x</code> 起建议统一使用 <code>spring.config.import</code> 拉取 Nacos 配置；<code>shared-configs</code> / <code>extension-configs</code> 等旧方式已废弃。更靠后的 <code>2025.1.x</code> 会正式废弃 Bootstrap，接入时直接写在 <code>application.yml</code> 即可。</p><p>% endnote %</p><h3 id="3-代码最小示例">3. 代码最小示例</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="meta">@EnableDiscoveryClient</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ProviderApplication</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(ProviderApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><hr><h2 id="六、联调检查清单">六、联调检查清单</h2><table><thead><tr><th>检查项</th><th>预期结果</th></tr></thead><tbody><tr><td>Nacos 进程 / 容器</td><td>单机：<code>stand alone</code>；集群：<code>cluster mode</code>；外置库：<code>use external storage</code></td></tr><tr><td>Nacos 控制台</td><td><code>http://127.0.0.1:8080/index.html</code>（或 Nginx / 域名）可登录</td></tr><tr><td>MySQL</td><td><code>config_info</code> 等表有数据；重启 Nacos 后配置不丢</td></tr><tr><td>集群节点</td><td>控制台「集群管理」可见全部节点且状态正常</td></tr><tr><td><code>8848</code> / <code>9848</code>（及集群 <code>9849</code> / <code>7848</code>）</td><td>防火墙或安全组已放行</td></tr><tr><td>Nginx 反代</td><td><code>8848</code> HTTP + <code>9848</code> TCP（stream）成对；<code>nginx -V</code> 含 <code>--with-stream</code></td></tr><tr><td>应用注册</td><td>Nacos「服务管理」可见实例，且 healthy</td></tr><tr><td>客户端版本</td><td><code>nacos-client 3.0.3</code>（由 SCA BOM 引入）</td></tr></tbody></table><p>常见问题：</p><ol><li class="lvl-3"><p><strong>应用连不上 Nacos</strong>：除了 <code>8848</code>，还必须打通 <code>9848</code>（gRPC）；经 Nginx / SLB 时两条链路都要转，且保持主端口 +1000 偏移。</p></li><li class="lvl-3"><p><strong>控制台 404 / 打不开</strong>：Nacos 3.x 用 <code>8080</code>，不要再用 <code>8848/nacos</code>。</p></li><li class="lvl-3"><p><strong>外网 IP 打不开控制台</strong>：本机 <code>curl</code> 正常且监听 <code>*:8080</code> 时，多半是云安全组未放行控制台端口；与 <code>nacos.inetutils.ip-address</code> 无关。</p></li><li class="lvl-3"><p><strong>重启后配置丢失</strong>：仍在用 Derby，或未导入 / 未指向 MySQL。</p></li><li class="lvl-3"><p><strong>集群某节点起不来</strong>：核对 <code>cluster.conf</code>、鉴权三件套一致性、数据库连通性。</p></li><li class="lvl-3"><p><strong>经 Nginx 后服务反复上下线</strong>：常因 gRPC 用了 HTTP/HTTP2 反代；应改为 <code>stream</code> TCP，并把 <code>proxy_timeout</code> 调大。</p></li><li class="lvl-3"><p><strong>版本混用</strong>：按 SCA 官方矩阵固定 <strong>Nacos 3.0.3</strong> 最省事。</p></li></ol><hr><h2 id="七、参考链接">七、参考链接</h2><ul class="lvl-0"><li class="lvl-2"><p><a href="https://sca.aliyun.com/docs/2025.x/overview/version-explain/">Spring Cloud Alibaba 版本发布说明（2025.x）</a></p></li><li class="lvl-2"><p><a href="https://sca.aliyun.com/docs/2025.x/user-guide/nacos/quick-start/">SCA · Nacos 快速开始</a></p></li><li class="lvl-2"><p><a href="https://nacos.io/docs/v3.0/quickstart/quick-start/">Nacos 3.0 快速开始</a></p></li><li class="lvl-2"><p><a href="https://nacos.io/docs/v3.0/quickstart/quick-start-docker/">Nacos Docker 快速开始</a></p></li><li class="lvl-2"><p><a href="https://nacos.io/docs/v3.0/manual/admin/deployment/deployment-standalone/">Nacos 单机模式部署</a></p></li><li class="lvl-2"><p><a href="https://nacos.io/docs/v3.0/manual/admin/deployment/deployment-cluster/">Nacos 集群模式部署</a></p></li><li class="lvl-2"><p><a href="https://nacos.io/docs/v3.0/manual/admin/deployment/deployment-overview/">Nacos 部署架构概览（含 VIP/Nginx 注意点）</a></p></li><li class="lvl-2"><p><a href="https://nacos.io/docs/latest/guide/admin/nginx-cluster-load-balance-guide/">Nacos 集群 Nginx 负载均衡指南</a></p></li><li class="lvl-2"><p><a href="https://nacos.io/docs/v3.0/manual/admin/auth/">Nacos 权限校验（3.0）</a></p></li><li class="lvl-2"><p><a href="https://nacos.io/docs/latest/manual/admin/system-configurations/">Nacos 系统参数（含端口）</a></p></li><li class="lvl-2"><p><a href="https://github.com/alibaba/nacos/blob/3.0.3/distribution/conf/mysql-schema.sql">mysql-schema.sql（3.0.3）</a></p></li><li class="lvl-2"><p><a href="https://github.com/nacos-group/nacos-docker">nacos-docker</a></p></li><li class="lvl-2"><a href="/2026/07/14/sca-2025-sentinel-install/" title="Spring Cloud Alibaba 2025.0.0.0 配套搭建 Sentinel Dashboard 1.8.9">Spring Cloud Alibaba 2025.0.0.0 配套搭建 Sentinel Dashboard 1.8.9</a></li></ul>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/07/14/sca-2025-nacos-install/</id>
    <link href="https://blog.hanqunfeng.com/2026/07/14/sca-2025-nacos-install/"/>
    <published>2026-07-14T08:43:58.000Z</published>
    <summary>
      <![CDATA[<!--
 **加粗**
 *斜体*
 ***加粗并斜体***
 ~~删除线~~
 ==突出显示==
 `突出显示(推荐)`
 ++下划线++
 ~下标~
 ^上标^
 脚注，参考文献[^1]，然后在文档最下方要添加这个1对应的内容，如：[^1]: My reference.
 图片设置宽度和高度(通过uPic上传后，需要将upic修改过为blog，用于添加水印) ![](https://upic-oss.oss-cn-beijing.aliyuncs.com/blog/innodb_buffer_pool.png =900x600)

 +++ **点击折叠**
 这是被隐藏的内容
 +++

::: tips success warning danger
这里是容器内的内容
:::

% note info % success warning danger
这里是容器内的内容
% endnote %

引用本地其它文章连接{}
 大括号开始% post_link 文件名称(不包含.md) %大括号结束
 -->
<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">
<p>依据 <a href="https://sca.aliyun.com/docs/2025.x/overview/version-explain/">Spring Cloud Alibaba 版本发布说明</a>，搭建与 <strong>Spring Cloud Alibaba 2025.0.0.0</strong> 对应的 <strong>Nacos Server 3.0.3</strong>。</p>
</li>
<li class="lvl-2">
<p>覆盖服务端单机搭建（二进制包 + Docker）、<strong>MySQL 外置持久化</strong>、<strong>多节点集群</strong>，并补充 Spring Boot 应用侧的最小接入配置。</p>
</li>
<li class="lvl-2">
<p>同版本配套的 Sentinel Dashboard 见 <a href="/2026/07/14/sca-2025-sentinel-install/" title="Spring Cloud Alibaba 2025.0.0.0 配套搭建 Sentinel Dashboard 1.8.9">Spring Cloud Alibaba 2025.0.0.0 配套搭建 Sentinel Dashboard 1.8.9</a>。</p>
</li>
</ul>]]>
    </summary>
    <title>Spring Cloud Alibaba 2025.0.0.0 配套搭建 Nacos 3.0.3</title>
    <updated>2026-07-17T09:09:04.585Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="npm" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/npm/"/>
    <category term="nodejs" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/npm/nodejs/"/>
    <category term="github" scheme="https://blog.hanqunfeng.com/tags/github/"/>
    <category term="npm" scheme="https://blog.hanqunfeng.com/tags/npm/"/>
    <category term="oidc" scheme="https://blog.hanqunfeng.com/tags/oidc/"/>
    <category term="github-actions" scheme="https://blog.hanqunfeng.com/tags/github-actions/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><p>本文介绍如何使用 npm Trusted Publishing，在 GitHub Actions 中通过 OpenID Connect（OIDC）发布 npm 包。整个发布过程不需要创建或保存长期有效的 <code>NPM_TOKEN</code>，并可通过指定仓库、Workflow 和 GitHub Environment 限制发布来源。</p><p>本文将依次完成以下工作：</p><ul class="lvl-0"><li class="lvl-2"><p>解释为什么 npm 自动发布应从长期 Token 迁移到 OIDC。</p></li><li class="lvl-2"><p>介绍 OIDC 和 npm Trusted Publishing 的工作原理。</p></li><li class="lvl-2"><p>在 <a href="http://npmjs.com">npmjs.com</a> 中为包添加 GitHub Actions Trusted Publisher。</p></li><li class="lvl-2"><p>在 GitHub 中配置 Environment、审批规则和最小权限。</p></li><li class="lvl-2"><p>编写一个完整、安全且不依赖 <code>NPM_TOKEN</code> 的发布 Workflow。</p></li><li class="lvl-2"><p>介绍私有依赖、首次发布、npm v12、Provenance 和常见错误。</p></li></ul><span id="more"></span><h2 id="一、为什么要使用-OIDC-发布-npm-包">一、为什么要使用 OIDC 发布 npm 包</h2><h3 id="1-1-npm-正在限制-2FA-bypass-Token">1.1 npm 正在限制 2FA-bypass Token</h3><p>GitHub 在 2026 年 7 月 8 日发布的 <a href="https://github.blog/changelog/2026-07-08-npm-install-time-security-and-gat-bypass2fa-deprecation/">npm install-time security and GAT bypass2fa deprecation</a> 中宣布：</p><ol><li class="lvl-3"><p>npm v12 已正式发布，并成为 <code>latest</code> 版本。</p></li><li class="lvl-3"><p>预计从 2026 年 8 月上旬开始，配置为绕过双因素认证的 Granular Access Token（GAT）将不能再绕过账户、包和组织管理操作的 2FA。</p></li><li class="lvl-3"><p>预计从 2027 年 1 月前后开始，2FA-bypass Token 将不能再直接发布 npm 包。</p></li><li class="lvl-3"><p>自动发布应迁移到 Trusted Publishing（OIDC），或者改用需要人工 2FA 审批的 Staged Publishing。</p></li></ol><p>过去常见的自动发布方式，是在 npm 中创建一个允许发布且绕过 2FA 的 Token，再将它保存为 GitHub Repository Secret：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">publish</span></span><br><span class="line">  <span class="attr">env:</span></span><br><span class="line">    <span class="attr">NODE_AUTH_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.NPM_TOKEN</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><p>这种方案依赖一个长期凭证。只要 Token 没有过期或被撤销，拿到它的人就可能在允许的权限范围内持续操作。Token 还可能因为日志输出、错误配置、第三方 Action、开发者电脑或仓库 Secret 管理不当而泄漏。</p><p>随着 2FA-bypass Token 的直接发布能力被逐步取消，继续围绕长期发布 Token 建设自动发布流程不仅风险更高，也不是可持续方案。</p><h3 id="1-2-OIDC-解决了什么问题">1.2 OIDC 解决了什么问题</h3><p>OIDC 发布不需要预先生成一个长期 npm 发布 Token。每次 Workflow 运行时：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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">GitHub Actions Job</span><br><span class="line">  → 向 GitHub OIDC Provider 请求短期 JWT</span><br><span class="line">  → npm 校验 JWT 的签名和身份声明</span><br><span class="line">  → npm 核对仓库、Workflow、Environment 等可信条件</span><br><span class="line">  → 本次 npm publish 获得短期发布权限</span><br><span class="line">  → Job 结束后凭证失效</span><br></pre></td></tr></table></figure><p>与长期 Token 相比，OIDC 的主要优势是：</p><ul class="lvl-0"><li class="lvl-2"><p><strong>没有长期发布密钥</strong>：GitHub Secrets 中不再需要保存 <code>NPM_TOKEN</code>。</p></li><li class="lvl-2"><p><strong>凭证生命周期短</strong>：身份令牌按 Job 动态生成，不能作为长期凭证反复使用。</p></li><li class="lvl-2"><p><strong>身份限制更精确</strong>：npm 可以只信任指定 GitHub 用户或组织、仓库、Workflow 文件和 Environment。</p></li><li class="lvl-2"><p><strong>不需要人工轮换 Token</strong>：减少 Token 创建、保存、更新和撤销的维护工作。</p></li><li class="lvl-2"><p><strong>降低泄漏影响</strong>：仓库里没有可直接复制走并长期使用的发布 Token。</p></li><li class="lvl-2"><p><strong>自动生成 Provenance</strong>：满足条件时，npm 会自动发布来源证明，让使用者可以确认包从哪个仓库和 Workflow 构建而来。</p></li></ul><p>需要注意，OIDC 并不是“关闭认证”。它是将认证方式从“持有一个长期秘密”改为“由 GitHub 在运行时证明当前 Workflow 的身份”。</p><h2 id="二、前置条件和限制">二、前置条件和限制</h2><p>根据 <a href="https://docs.npmjs.com/trusted-publishers/">npm Trusted Publishing 官方文档</a>，使用 GitHub Actions 发布时需要满足以下条件：</p><ul class="lvl-0"><li class="lvl-2"><p>Node.js <code>&gt;= 22.14.0</code>。</p></li><li class="lvl-2"><p>npm CLI <code>&gt;= 11.5.1</code>。</p></li><li class="lvl-2"><p>使用 GitHub-hosted Runner，当前不支持 self-hosted Runner。</p></li><li class="lvl-2"><p>npm 包已经存在，并且当前 npm 用户拥有该包的管理权限。</p></li><li class="lvl-2"><p>Workflow 文件位于仓库的 <code>.github/workflows/</code> 目录。</p></li><li class="lvl-2"><p><code>package.json</code> 中的 <code>repository.url</code> 必须与发布来源的 GitHub 仓库准确对应。</p></li><li class="lvl-2"><p>一个 npm 包同一时间只能配置一个 Trusted Publisher。</p></li></ul><p>本文使用：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">Node.js 24</span><br><span class="line">npm 12</span><br><span class="line">ubuntu-latest</span><br><span class="line">.github/workflows/publish.yml</span><br><span class="line">GitHub Environment: npm</span><br></pre></td></tr></table></figure><h3 id="2-1-新包需要先完成首次发布">2.1 新包需要先完成首次发布</h3><p>Trusted Publisher 是在已有 npm 包的 Settings 页面中配置的，因此全新的包通常需要先由维护者交互式发布一次：</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">npm login</span><br><span class="line">npm publish --access public</span><br></pre></td></tr></table></figure><p>首次发布时按照 npm 提示完成 2FA。包创建成功后，再配置 Trusted Publisher，后续版本即可完全交给 GitHub Actions 和 OIDC。</p><p>不要为了首次发布而创建长期的 2FA-bypass Token。</p><h3 id="2-2-检查-package-json">2.2 检查 package.json</h3><p>一个公开作用域包可以使用如下配置：</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;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;@your-scope/your-package&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1.0.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;repository&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;git&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;url&quot;</span><span class="punctuation">:</span> <span class="string">&quot;git+https://github.com/OWNER/REPOSITORY.git&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;publishConfig&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;access&quot;</span><span class="punctuation">:</span> <span class="string">&quot;public&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;registry&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://registry.npmjs.org/&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>需要重点检查：</p><ul class="lvl-0"><li class="lvl-2"><p><code>name</code> 必须是准备配置 Trusted Publisher 的 npm 包名。</p></li><li class="lvl-2"><p><code>version</code> 每次发布都必须递增，npm 不允许覆盖已有版本。</p></li><li class="lvl-2"><p><code>repository.url</code> 中的 <code>OWNER/REPOSITORY</code> 必须与实际 GitHub 仓库匹配。</p></li><li class="lvl-2"><p>公开作用域包建议设置 <code>publishConfig.access</code> 为 <code>public</code>，这样无需每次添加 <code>--access public</code>。</p></li><li class="lvl-2"><p><code>publishConfig.registry</code> 可以防止包被误发到其他 Registry。</p></li></ul><h2 id="三、在-npm-中配置-Trusted-Publisher">三、在 npm 中配置 Trusted Publisher</h2><h3 id="3-1-通过-npmjs-com-配置">3.1 通过 <a href="http://npmjs.com">npmjs.com</a> 配置</h3><p>登录 <a href="https://www.npmjs.com/">npmjs.com</a>，进入要发布的包，然后打开：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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">Packages</span><br><span class="line">  → 选择包</span><br><span class="line">  → Settings</span><br><span class="line">  → Trusted publishing</span><br><span class="line">  → Select your publisher</span><br><span class="line">  → GitHub Actions</span><br></pre></td></tr></table></figure><p>填写以下字段：</p><table><thead><tr><th>字段</th><th>示例</th><th>说明</th></tr></thead><tbody><tr><td>Organization or user</td><td><code>hanqunfeng</code></td><td>GitHub 仓库所属用户或组织，不包含 <code>@</code></td></tr><tr><td>Repository</td><td><code>my-package</code></td><td>只填写仓库名，不填写完整 URL</td></tr><tr><td>Workflow filename</td><td><code>publish.yml</code></td><td>只填写文件名，不能填写 <code>.github/workflows/publish.yml</code></td></tr><tr><td>Environment name</td><td><code>npm</code></td><td>可选；填写后，GitHub Job 必须使用同名 Environment</td></tr><tr><td>Allowed actions</td><td><code>npm publish</code></td><td>允许 Workflow 直接发布</td></tr></tbody></table><p>所有字段都区分大小写，应与 GitHub 中的值完全一致。</p><p>如果希望发布前必须由维护者进行 2FA 审批，可以只允许 <code>npm stage publish</code>。本文介绍自动直接发布，因此选择 <code>npm publish</code>。</p><p>保存配置时 npm 不会验证仓库和 Workflow 是否真实存在。字段写错通常要等到第一次发布时才会出现 <code>ENEEDAUTH</code>，因此应在保存前逐项核对。</p><h3 id="3-2-Environment-是否必须填写">3.2 Environment 是否必须填写</h3><p>Environment 是可选项，但生产发布建议配置。填写 <code>npm</code> 后，npm 不仅检查仓库和 Workflow，还会检查 OIDC Token 中的 Environment 身份。</p><p>这意味着下面两处必须完全一致：</p><figure class="highlight text"><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">npm Trusted Publisher Environment name: npm</span><br><span class="line">GitHub Actions Job environment: npm</span><br></pre></td></tr></table></figure><p>如果 npm 中填写了 Environment，但 Workflow 没有声明 <code>environment: npm</code>，发布认证会失败。</p><h3 id="3-3-使用-npm-CLI-配置">3.3 使用 npm CLI 配置</h3><p>也可以使用 <code>npm trust</code> 命令创建信任关系：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">npm trust github @your-scope/your-package \</span><br><span class="line">  --repo OWNER/REPOSITORY \</span><br><span class="line">  --file publish.yml \</span><br><span class="line">  --<span class="built_in">env</span> npm \</span><br><span class="line">  --allow-publish</span><br></pre></td></tr></table></figure><p>先使用 <code>--dry-run</code> 检查参数：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">npm trust github @your-scope/your-package \</span><br><span class="line">  --repo OWNER/REPOSITORY \</span><br><span class="line">  --file publish.yml \</span><br><span class="line">  --<span class="built_in">env</span> npm \</span><br><span class="line">  --allow-publish \</span><br><span class="line">  --dry-run</span><br></pre></td></tr></table></figure><p>这个命令与网页配置的作用相同。它需要当前终端已经登录 npm，并拥有目标包的管理权限。</p><h2 id="四、在-GitHub-中配置发布环境">四、在 GitHub 中配置发布环境</h2><h3 id="4-1-创建-Environment">4.1 创建 Environment</h3><p>打开 GitHub 仓库，进入：</p><figure class="highlight text"><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">Settings</span><br><span class="line">  → Environments</span><br><span class="line">  → New environment</span><br></pre></td></tr></table></figure><p>Environment 名称填写：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm</span><br></pre></td></tr></table></figure><p>名称应与 npm Trusted Publisher 中的 <code>Environment name</code> 完全一致。</p><h3 id="4-2-配置部署保护规则">4.2 配置部署保护规则</h3><p>可以根据仓库安全要求配置：</p><ul class="lvl-0"><li class="lvl-2"><p>Required reviewers：发布前必须由指定维护者批准。</p></li><li class="lvl-2"><p>Deployment branches and tags：只允许受保护分支或指定 Tag 部署。</p></li><li class="lvl-2"><p>Prevent self-review：触发发布的人不能批准自己的发布。</p></li><li class="lvl-2"><p>Wait timer：批准后等待一段时间再开始发布。</p></li></ul><p>如果使用 <code>v*</code> Tag 触发发布，建议只允许符合版本规则的 Tag，例如：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">v*</span><br></pre></td></tr></table></figure><p>GitHub 不同套餐和仓库可见性支持的 Environment 保护能力可能不同，应以仓库 Settings 页面实际提供的选项为准。</p><h3 id="4-3-不需要创建-NPM-TOKEN">4.3 不需要创建 NPM_TOKEN</h3><p>OIDC 发布不需要在下面的位置添加 npm 发布 Token：</p><figure class="highlight text"><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">Settings</span><br><span class="line">  → Secrets and variables</span><br><span class="line">  → Actions</span><br></pre></td></tr></table></figure><p>如果旧 Workflow 中存在 <code>NPM_TOKEN</code>：</p><ol><li class="lvl-3"><p>先保留 Secret，完成一次 OIDC 发布验证。</p></li><li class="lvl-3"><p>确认 Workflow 没有读取该 Secret。</p></li><li class="lvl-3"><p>删除旧 Workflow 中的 <code>NODE_AUTH_TOKEN: $&#123;&#123; secrets.NPM_TOKEN &#125;&#125;</code>。</p></li><li class="lvl-3"><p>最后在 GitHub 和 npm 中撤销不再使用的发布 Token。</p></li></ol><p>不要在尚未验证 OIDC 可用前先撤销旧发布方式，否则可能中断发布。</p><h3 id="4-4-保护发布-Tag">4.4 保护发布 Tag</h3><p>仅仅使用 <code>push.tags: [&quot;v*&quot;]</code> 不代表任何人都应该能创建发布 Tag。组织仓库可以通过 GitHub Rulesets 限制谁能创建或更新匹配 <code>v*</code> 的 Tag：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><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">Settings</span><br><span class="line">  → Rules</span><br><span class="line">  → Rulesets</span><br><span class="line">  → New ruleset</span><br><span class="line">  → New tag ruleset</span><br></pre></td></tr></table></figure><p>Tag 保护与 Environment 审批配合使用，可以避免普通写权限成员通过创建 Tag 直接触发生产发布。</p><h2 id="五、编写-GitHub-Actions-Workflow">五、编写 GitHub Actions Workflow</h2><p>在项目中创建 <code>.github/workflows/publish.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><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></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Publish</span> <span class="string">to</span> <span class="string">npm</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">tags:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;v*&quot;</span></span><br><span class="line">  <span class="attr">workflow_dispatch:</span></span><br><span class="line"></span><br><span class="line"><span class="attr">concurrency:</span></span><br><span class="line">  <span class="attr">group:</span> <span class="string">npm-publish</span></span><br><span class="line">  <span class="attr">cancel-in-progress:</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">publish:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">Publish</span> <span class="string">package</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">environment:</span> <span class="string">npm</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">      <span class="attr">id-token:</span> <span class="string">write</span></span><br><span class="line"></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Checkout</span> <span class="string">repository</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Setup</span> <span class="string">Node.js</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">node-version:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line">          <span class="attr">registry-url:</span> <span class="string">&quot;https://registry.npmjs.org/&quot;</span></span><br><span class="line">          <span class="attr">package-manager-cache:</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Show</span> <span class="string">runtime</span> <span class="string">versions</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          node --version</span></span><br><span class="line"><span class="string">          npm --version</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Verify</span> <span class="string">tag</span> <span class="string">matches</span> <span class="string">package</span> <span class="string">version</span></span><br><span class="line">        <span class="attr">if:</span> <span class="string">github.event_name</span> <span class="string">==</span> <span class="string">&#x27;push&#x27;</span></span><br><span class="line">        <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">          PACKAGE_VERSION=&quot;$(node -p &quot;require(&#x27;./package.json&#x27;).version&quot;)&quot;</span></span><br><span class="line"><span class="string">          if [[ &quot;$&#123;GITHUB_REF_NAME&#125;&quot; != &quot;v$&#123;PACKAGE_VERSION&#125;&quot; ]]; then</span></span><br><span class="line"><span class="string">            echo &quot;Tag $&#123;GITHUB_REF_NAME&#125; does not match package version v$&#123;PACKAGE_VERSION&#125;&quot;</span></span><br><span class="line"><span class="string">            exit 1</span></span><br><span class="line"><span class="string">          fi</span></span><br><span class="line"><span class="string"></span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dependencies</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Run</span> <span class="string">tests</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span> <span class="string">--if-present</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">package</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npm</span> <span class="string">run</span> <span class="string">build</span> <span class="string">--if-present</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Preview</span> <span class="string">package</span> <span class="string">contents</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npm</span> <span class="string">pack</span> <span class="string">--dry-run</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Publish</span> <span class="string">package</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npm</span> <span class="string">publish</span></span><br></pre></td></tr></table></figure><p>提交 Workflow：</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">git add .github/workflows/publish.yml</span><br><span class="line">git commit -m <span class="string">&quot;ci: publish npm package with OIDC&quot;</span></span><br><span class="line">git push</span><br></pre></td></tr></table></figure><p>如果通过 HTTPS 使用 GitHub Personal Access Token（classic）执行 <code>git push</code>，该 Token 除了仓库写入权限外，还必须勾选 <code>workflow</code> scope（Update GitHub Action workflows）；否则无法新增或修改 <code>.github/workflows/</code> 下的 Workflow 文件。</p><h3 id="5-1-id-token-write-是关键权限">5.1 id-token: write 是关键权限</h3><p>下面的权限允许当前 Job 向 GitHub OIDC Provider 请求 JWT：</p><figure class="highlight yaml"><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="attr">permissions:</span></span><br><span class="line">  <span class="attr">id-token:</span> <span class="string">write</span></span><br></pre></td></tr></table></figure><p>根据 <a href="https://docs.github.com/en/actions/reference/security/oidc">GitHub OIDC 参考文档</a>，<code>id-token: write</code> 只允许请求 OIDC Token，本身不会直接授予修改仓库或 npm 包的权限。npm 仍然会验证 Token，并根据 Trusted Publisher 配置决定是否允许发布。</p><p><code>actions/checkout</code> 读取仓库代码还需要：</p><figure class="highlight yaml"><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="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br></pre></td></tr></table></figure><p>将权限放在 <code>publish</code> Job 内，可以避免同一 Workflow 的其他 Job 获得请求 OIDC Token 的能力。</p><h3 id="5-2-为什么没有-NODE-AUTH-TOKEN">5.2 为什么没有 NODE_AUTH_TOKEN</h3><p>OIDC Workflow 中不应再写：</p><figure class="highlight yaml"><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="attr">env:</span></span><br><span class="line">  <span class="attr">NODE_AUTH_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.NPM_TOKEN</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><p>npm CLI 会识别 GitHub Actions OIDC 环境，在执行 <code>npm publish</code> 时完成身份交换。如果仍提供一个具有发布权限的长期 Token，可能会掩盖 Trusted Publisher 配置错误，让 Workflow 看似迁移成功，实际仍然依赖旧 Token。</p><h3 id="5-3-为什么关闭发布任务中的依赖缓存">5.3 为什么关闭发布任务中的依赖缓存</h3><p>示例使用：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">package-manager-cache:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>发布任务应优先保证构建来源清晰、可重复。缓存可以放在普通 CI 中加速测试，但 npm 官方示例建议发布构建不要使用依赖缓存，以减少旧缓存或受污染缓存进入发布产物的风险。</p><h3 id="5-4-为什么使用-Environment">5.4 为什么使用 Environment</h3><p>下面的配置有两个作用：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">environment:</span> <span class="string">npm</span></span><br></pre></td></tr></table></figure><ol><li class="lvl-3"><p>让 OIDC Token 包含 <code>npm</code> Environment 身份，以匹配 npm Trusted Publisher。</p></li><li class="lvl-3"><p>应用 GitHub Environment 中配置的审批、Tag 限制和等待规则。</p></li></ol><p>如果不准备使用 Environment，应同时删除：</p><ul class="lvl-0"><li class="lvl-2"><p>npm Trusted Publisher 中的 Environment name。</p></li><li class="lvl-2"><p>Workflow 中的 <code>environment: npm</code>。</p></li></ul><h3 id="5-5-为什么校验-Tag-和-package-json-版本">5.5 为什么校验 Tag 和 package.json 版本</h3><p><code>v1.2.3</code> Tag 应发布 <code>package.json</code> 中的 <code>1.2.3</code>。如果二者不一致，可能出现：</p><ul class="lvl-0"><li class="lvl-2"><p>Tag 表示的版本与 npm 实际版本不同。</p></li><li class="lvl-2"><p>重复发布一个已经存在的版本。</p></li><li class="lvl-2"><p>Release Notes 与真实包版本错位。</p></li></ul><p>Workflow 在安装依赖前进行校验，可以更早终止错误发布。</p><h2 id="六、执行一次发布">六、执行一次发布</h2><h3 id="6-1-更新版本并创建-Tag">6.1 更新版本并创建 Tag</h3><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></pre></td><td class="code"><pre><span class="line">npm version patch -m <span class="string">&quot;chore(release): %s&quot;</span></span><br><span class="line">git push</span><br><span class="line">git push --tags</span><br></pre></td></tr></table></figure><p><code>npm version patch</code> 会：</p><ol><li class="lvl-3"><p>更新 <code>package.json</code> 和 <code>package-lock.json</code> 中的版本。</p></li><li class="lvl-3"><p>创建一个 Git Commit。</p></li><li class="lvl-3"><p>创建形如 <code>v1.0.1</code> 的 Git Tag。</p></li></ol><p>Tag 推送到 GitHub 后，<code>publish.yml</code> 开始运行。如果 Environment 配置了 Required reviewers，Job 会在发布前等待审批。</p><h3 id="6-2-验证发布结果">6.2 验证发布结果</h3><p>在 GitHub 中检查：</p><figure class="highlight text"><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">Repository</span><br><span class="line">  → Actions</span><br><span class="line">  → Publish to npm</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><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">npm view @your-scope/your-package version</span><br><span class="line">npm view @your-scope/your-package dist-tags</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">npm install @your-scope/your-package@1.0.1</span><br></pre></td></tr></table></figure><h2 id="七、Provenance-来源证明">七、Provenance 来源证明</h2><p>使用 GitHub Actions Trusted Publishing 时，npm 会在满足条件的情况下自动生成 Provenance，不需要添加：</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">npm publish --provenance</span><br></pre></td></tr></table></figure><p>自动生成需要同时满足：</p><ul class="lvl-0"><li class="lvl-2"><p>通过 OIDC Trusted Publishing 发布。</p></li><li class="lvl-2"><p>从公开 GitHub 仓库发布。</p></li><li class="lvl-2"><p>发布的是公开 npm 包。</p></li></ul><p>Provenance 为包提供可验证的构建来源，包括源代码仓库和执行发布的 Workflow。使用者可以在 npm 包页面查看来源信息，从而降低包被伪造或发布链路不透明的风险。</p><p>私有 GitHub 仓库当前不会生成 Provenance，即使最终发布的是公开 npm 包，但 OIDC Trusted Publishing 本身仍然可以使用。</p><h2 id="八、npm-v12-对发布-Workflow-的影响">八、npm v12 对发布 Workflow 的影响</h2><p>npm v12 除了推动发布认证迁移，还默认收紧了安装阶段的供应链安全策略：</p><ul class="lvl-0"><li class="lvl-2"><p>依赖的 <code>preinstall</code>、<code>install</code>、<code>postinstall</code> 生命周期脚本和隐式 <code>node-gyp</code> 构建默认不再自动执行。</p></li><li class="lvl-2"><p>Git 依赖默认不允许解析。</p></li><li class="lvl-2"><p>HTTPS Tarball 等远程 URL 依赖默认不允许解析。</p></li></ul><p>升级 npm v12 后，应在开发环境中审查被阻止的依赖脚本：</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">npm approve-scripts --allow-scripts-pending</span><br></pre></td></tr></table></figure><p>根据审查结果批准确实需要执行脚本的依赖，并将生成的 <code>allowScripts</code> 配置提交到 <code>package.json</code>。不要在 CI 中直接允许所有依赖脚本，否则会绕过 npm v12 新增的安全边界。</p><p>在正式启用 npm v12 前，应本地运行：</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">npm ci</span><br><span class="line">npm <span class="built_in">test</span></span><br><span class="line">npm run build --if-present</span><br><span class="line">npm pack --dry-run</span><br></pre></td></tr></table></figure><p>确认原生模块、代码生成工具和构建依赖没有因为 install script 被阻止而失效。</p><h2 id="九、项目使用私有依赖时的配置">九、项目使用私有依赖时的配置</h2><p>Trusted Publishing 只负责 <code>npm publish</code> 或 <code>npm stage publish</code>，不会让 <code>npm ci</code> 自动获得私有包读取权限。</p><p>如果项目依赖私有 npm 包，需要创建一个只读 Granular Access Token，并仅在安装步骤中使用：</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></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dependencies</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">  <span class="attr">env:</span></span><br><span class="line">    <span class="attr">NODE_AUTH_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.NPM_READ_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Publish</span> <span class="string">package</span> <span class="string">with</span> <span class="string">OIDC</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">npm</span> <span class="string">publish</span></span><br></pre></td></tr></table></figure><p>注意：</p><ul class="lvl-0"><li class="lvl-2"><p><code>NPM_READ_TOKEN</code> 只授予读取必要私有包的权限。</p></li><li class="lvl-2"><p>不要为它启用发布权限或 2FA bypass。</p></li><li class="lvl-2"><p>不要在 <code>npm publish</code> 步骤中传递该 Token。</p></li><li class="lvl-2"><p>项目中的 <code>.npmrc</code> 只能引用环境变量，不能写入真实 Token。</p></li></ul><p>例如：</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">//registry.npmjs.org/:<span class="attr">_authToken</span>=<span class="variable">$&#123;NODE_AUTH_TOKEN&#125;</span></span><br></pre></td></tr></table></figure><h2 id="十、进一步加固发布流程">十、进一步加固发布流程</h2><h3 id="10-1-OIDC-验证成功后禁止传统-Token-发布">10.1 OIDC 验证成功后禁止传统 Token 发布</h3><p>完成一次 OIDC 发布验证后，进入 npm 包设置：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><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">Package</span><br><span class="line">  → Settings</span><br><span class="line">  → Publishing access</span><br><span class="line">  → Require two-factor authentication and disallow tokens</span><br></pre></td></tr></table></figure><p>该选项会阻止传统 Token 发布，但不会阻止 Trusted Publisher 使用 OIDC。</p><p>推荐迁移顺序：</p><ol><li class="lvl-3"><p>添加 Trusted Publisher。</p></li><li class="lvl-3"><p>提交不包含 <code>NPM_TOKEN</code> 的 Workflow。</p></li><li class="lvl-3"><p>成功发布一个新版本。</p></li><li class="lvl-3"><p>禁止传统 Token 发布。</p></li><li class="lvl-3"><p>撤销旧的自动发布 Token。</p></li><li class="lvl-3"><p>删除 GitHub 中不再使用的 <code>NPM_TOKEN</code> Secret。</p></li></ol><h3 id="10-2-使用-Staged-Publishing">10.2 使用 Staged Publishing</h3><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">npm stage publish</span><br></pre></td></tr></table></figure><p>然后由维护者通过 <a href="http://npmjs.com">npmjs.com</a> 或 CLI 完成 2FA 审批，版本才会公开。此时在 Trusted Publisher 中只允许 <code>npm stage publish</code>，不要允许直接 <code>npm publish</code>。</p><h3 id="10-3-固定第三方-Action">10.3 固定第三方 Action</h3><p>示例为了便于阅读使用：</p><figure class="highlight yaml"><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="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line"><span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br></pre></td></tr></table></figure><p>对高安全要求的发布链路，可以将 Action 固定到审核过的完整 Commit SHA，并由 Dependabot 或 Renovate 提交升级 PR，避免可变 Tag 被上游修改。</p><h3 id="10-4-将测试和发布拆分">10.4 将测试和发布拆分</h3><p>复杂项目可以先运行没有 <code>id-token: write</code> 的测试 Job，全部通过后再运行发布 Job。只有最后一个 Job 拥有 OIDC 权限：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><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="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">node-version:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">publish:</span></span><br><span class="line">    <span class="attr">needs:</span> <span class="string">test</span></span><br><span class="line">    <span class="attr">environment:</span> <span class="string">npm</span></span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">      <span class="attr">id-token:</span> <span class="string">write</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">node-version:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line">          <span class="attr">registry-url:</span> <span class="string">&quot;https://registry.npmjs.org/&quot;</span></span><br><span class="line">          <span class="attr">package-manager-cache:</span> <span class="literal">false</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">run</span> <span class="string">build</span> <span class="string">--if-present</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">publish</span></span><br></pre></td></tr></table></figure><p>如果测试 Job 生成构建 Artifact，发布 Job 下载 Artifact 后应校验来源和内容，避免“测试一份代码、发布另一份代码”。</p><h2 id="十一、常见问题排查">十一、常见问题排查</h2><h3 id="11-1-npm-ERR-code-ENEEDAUTH">11.1 npm ERR! code ENEEDAUTH</h3><p>依次检查：</p><ol><li class="lvl-3"><p>Workflow 是否包含 <code>id-token: write</code>。</p></li><li class="lvl-3"><p>是否使用 GitHub-hosted Runner。</p></li><li class="lvl-3"><p>Node.js 是否 <code>&gt;= 22.14.0</code>。</p></li><li class="lvl-3"><p>npm 是否 <code>&gt;= 11.5.1</code>。</p></li><li class="lvl-3"><p>npm 中的 Organization、Repository 是否与当前仓库一致。</p></li><li class="lvl-3"><p>npm 中只填写了 <code>publish.yml</code>，而不是完整路径。</p></li><li class="lvl-3"><p>Workflow 文件名的大小写和扩展名是否完全一致。</p></li><li class="lvl-3"><p>npm 中配置了 Environment 时，Job 是否声明同名 <code>environment</code>。</p></li><li class="lvl-3"><p><code>package.json</code> 的 <code>repository.url</code> 是否指向当前 GitHub 仓库。</p></li><li class="lvl-4"><p><code>npm publish</code> 步骤是否意外传入了一个无效的 <code>NODE_AUTH_TOKEN</code>。</p></li></ol><h3 id="11-2-npm-whoami-失败">11.2 npm whoami 失败</h3><p>这是预期行为。OIDC 身份交换只在 <code>npm publish</code> 或 <code>npm stage publish</code> 时发生：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm <span class="built_in">whoami</span></span><br></pre></td></tr></table></figure><p>不能用于验证当前 Workflow 的 OIDC 发布身份。</p><h3 id="11-3-使用-Reusable-Workflow-后认证失败">11.3 使用 Reusable Workflow 后认证失败</h3><p>如果使用 <code>workflow_call</code>，npm 可能校验调用方 Workflow 的文件名，而不是实际包含 <code>npm publish</code> 的被调用 Workflow。父、子 Workflow 还都需要正确传递 <code>id-token: write</code> 权限。</p><p>遇到此问题时，应确认 npm Trusted Publisher 中登记的是实际参与 OIDC 身份声明的 Workflow 文件名。对于关键发布流程，直接在已登记的顶层 Workflow 中执行发布通常更容易审计。</p><h3 id="11-4-手动触发可以发布，Tag-触发失败">11.4 手动触发可以发布，Tag 触发失败</h3><p>检查：</p><ul class="lvl-0"><li class="lvl-2"><p>Tag 是否满足 <code>v*</code>。</p></li><li class="lvl-2"><p>Tag 是否指向包含 <code>publish.yml</code> 的提交。</p></li><li class="lvl-2"><p>Tag 与 <code>package.json</code> 版本是否一致。</p></li><li class="lvl-2"><p>Environment 的 Deployment branches and tags 是否允许该 Tag。</p></li><li class="lvl-2"><p>Tag Ruleset 是否允许当前用户或自动化创建该 Tag。</p></li></ul><h3 id="11-5-npm-中找不到-Trusted-publishing">11.5 npm 中找不到 Trusted publishing</h3><p>常见原因包括：</p><ul class="lvl-0"><li class="lvl-2"><p>包还没有完成首次发布。</p></li><li class="lvl-2"><p>当前 npm 账号不是包维护者。</p></li><li class="lvl-2"><p>登录了错误的 npm 账号或组织。</p></li><li class="lvl-2"><p>当前账号没有修改包设置的权限。</p></li></ul><h2 id="十二、完整迁移检查清单">十二、完整迁移检查清单</h2><p>发布前：</p><ul class="lvl-0"><li class="lvl-2"><p><input type="checkbox" id="checkbox0"><label for="checkbox0">npm 包已经存在。</label></p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox1"><label for="checkbox1">Node.js </label><code>&gt;= 22.14.0</code>，npm <code>&gt;= 11.5.1</code>。</p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox2"><label for="checkbox2"></label><code>package.json</code> 的包名、版本、Registry 和 <code>repository.url</code> 正确。</p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox3"><label for="checkbox3">npm Trusted Publisher 的仓库、Workflow 和 Environment 配置正确。</label></p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox4"><label for="checkbox4">GitHub 中已创建同名 Environment。</label></p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox5"><label for="checkbox5">Workflow 使用 GitHub-hosted Runner。</label></p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox6"><label for="checkbox6">发布 Job 只有 </label><code>contents: read</code> 和 <code>id-token: write</code> 等必要权限。</p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox7"><label for="checkbox7">Workflow 的发布步骤没有使用 </label><code>NPM_TOKEN</code>。</p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox8"><label for="checkbox8">Tag 与 </label><code>package.json</code> 版本一致。</p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox9"><label for="checkbox9">npm v12 所需的依赖 install script 已经完成审查。</label></p></li></ul><p>首次 OIDC 发布成功后：</p><ul class="lvl-0"><li class="lvl-2"><p><input type="checkbox" id="checkbox10"><label for="checkbox10">在 npm 包页面确认新版本和 Provenance。</label></p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox11"><label for="checkbox11">将 Publishing access 改为要求 2FA 并禁止传统 Token。</label></p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox12"><label for="checkbox12">撤销旧 npm 发布 Token。</label></p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox13"><label for="checkbox13">删除 GitHub 中不再使用的 </label><code>NPM_TOKEN</code>。</p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox14"><label for="checkbox14">保留私有依赖所需的只读 Token，并限制其作用范围。</label></p></li><li class="lvl-2"><p><input type="checkbox" id="checkbox15"><label for="checkbox15">定期审计 Trusted Publisher、Environment 审批人和 Tag Ruleset。</label></p></li></ul><h2 id="总结">总结</h2><p>npm 对 2FA-bypass Token 的限制意味着，长期发布 Token 不再适合作为 GitHub Actions 自动发布的基础。Trusted Publishing 使用 GitHub OIDC 在运行时证明 Workflow 身份，不需要保存长期发布密钥，并且可以将权限限制到指定仓库、Workflow 和 Environment。</p><p>一套推荐的 npm 发布链路是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">受保护的版本 Tag</span><br><span class="line">  → GitHub Actions 测试和构建</span><br><span class="line">  → GitHub Environment 审批</span><br><span class="line">  → GitHub 签发短期 OIDC Token</span><br><span class="line">  → npm 验证 Trusted Publisher</span><br><span class="line">  → npm publish</span><br><span class="line">  → 自动生成 Provenance</span><br></pre></td></tr></table></figure><p>完成迁移并验证成功后，再禁止传统 Token 发布并撤销旧 Token，才能真正移除长期凭证带来的供应链风险。</p><h2 id="真实案例">真实案例</h2><p>为了更好的便于读者理解，可以参考本人的开源项目 <a href="https://github.com/hanqunfeng/claude-trace">claude-trace</a></p><ul class="lvl-0"><li class="lvl-2"><p><a href="https://github.com/hanqunfeng/claude-trace/blob/main/.github/workflows/publish.yml">publish.yml</a>：使用 GitHub Actions 和 OIDC 自动发布 npm 包的完整 Workflow。</p></li><li class="lvl-2"><p><a href="https://github.com/hanqunfeng/claude-trace/blob/main/scripts/publish-oidc.sh">publish-oidc.sh</a>：发布前检查版本、构建产物和 OIDC 环境的辅助脚本。</p></li><li class="lvl-2"><p><a href="https://github.com/hanqunfeng/claude-trace/blob/main/doc/publishing/oidc.md">oidc.md</a>：项目中配置、使用及排查 OIDC 发布流程的说明文档。</p></li></ul><h2 id="参考资料">参考资料</h2><ul class="lvl-0"><li class="lvl-2"><p><a href="https://github.blog/changelog/2026-07-08-npm-install-time-security-and-gat-bypass2fa-deprecation/">npm install-time security and GAT bypass2fa deprecation</a></p></li><li class="lvl-2"><p><a href="https://docs.npmjs.com/trusted-publishers/">Trusted publishing for npm packages</a></p></li><li class="lvl-2"><p><a href="https://docs.npmjs.com/using-private-packages-in-a-ci-cd-workflow/">Using private packages in a CI/CD workflow</a></p></li><li class="lvl-2"><p><a href="https://docs.npmjs.com/generating-provenance-statements/">Generating provenance statements</a></p></li><li class="lvl-2"><p><a href="https://docs.github.com/en/actions/concepts/security/openid-connect">GitHub Actions OpenID Connect</a></p></li><li class="lvl-2"><p><a href="https://docs.github.com/en/actions/reference/security/oidc">OpenID Connect reference</a></p></li></ul>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/07/11/npm-oidc-trusted-publishing/</id>
    <link href="https://blog.hanqunfeng.com/2026/07/11/npm-oidc-trusted-publishing/"/>
    <published>2026-07-11T14:51:00.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<p>本文介绍如何使用 npm Trusted Publishing，在 GitHub Actions 中通过 OpenID Connect（OIDC）发布 npm 包。整个发布过程不需要创建或保存长期有效的 <code>NPM_TOKEN</code>，并可通过指定仓库、Workflow 和 GitHub Environment 限制发布来源。</p>
<p>本文将依次完成以下工作：</p>
<ul class="lvl-0">
<li class="lvl-2">
<p>解释为什么 npm 自动发布应从长期 Token 迁移到 OIDC。</p>
</li>
<li class="lvl-2">
<p>介绍 OIDC 和 npm Trusted Publishing 的工作原理。</p>
</li>
<li class="lvl-2">
<p>在 <a href="http://npmjs.com">npmjs.com</a> 中为包添加 GitHub Actions Trusted Publisher。</p>
</li>
<li class="lvl-2">
<p>在 GitHub 中配置 Environment、审批规则和最小权限。</p>
</li>
<li class="lvl-2">
<p>编写一个完整、安全且不依赖 <code>NPM_TOKEN</code> 的发布 Workflow。</p>
</li>
<li class="lvl-2">
<p>介绍私有依赖、首次发布、npm v12、Provenance 和常见错误。</p>
</li>
</ul>]]>
    </summary>
    <title>使用 GitHub Actions 和 OIDC 安全发布 npm 包</title>
    <updated>2026-07-11T09:17:50.546Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="git" scheme="https://blog.hanqunfeng.com/tags/git/"/>
    <category term="github" scheme="https://blog.hanqunfeng.com/tags/github/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><p>本文面向第一次接触 GitHub Actions 的开发者，从基本概念、YAML 语法和变量表达式开始，逐步介绍 CI、测试矩阵、缓存、构建产物、发布、OIDC、安全和排错。</p><blockquote><p>文档中的示例需要放在仓库的 <code>.github/workflows/</code> 目录中，例如 <code>.github/workflows/ci.yml</code>。<br>GitHub Actions 会执行代码和第三方 Action，复制示例前应根据项目实际情况调整权限、Node.js 版本和命令。</p></blockquote><span id="more"></span><h2 id="一、GitHub-Actions-是什么">一、GitHub Actions 是什么</h2><p>GitHub Actions 是 GitHub 内置的自动化平台。代码推送、Pull Request、标签、Release、Issue、定时任务或人工点击按钮，都可以触发自动任务。</p><p>常见用途：</p><ul class="lvl-0"><li class="lvl-2"><p>每次提交后运行类型检查、格式检查和单元测试。</p></li><li class="lvl-2"><p>在 Linux、Windows、macOS 或不同 Node.js 版本上测试。</p></li><li class="lvl-2"><p>构建前端、后端、桌面应用或 Docker 镜像。</p></li><li class="lvl-2"><p>将测试报告、安装包等保存为 Artifact。</p></li><li class="lvl-2"><p>发布 npm 包、GitHub Release、Docker 镜像。</p></li><li class="lvl-2"><p>部署到云服务器、Kubernetes、静态网站或 GitHub Pages。</p></li><li class="lvl-2"><p>自动添加标签、回复 Issue、检查依赖和执行安全扫描。</p></li></ul><p>基本执行过程：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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">GitHub 事件</span><br><span class="line">  └─ Workflow 工作流</span><br><span class="line">      ├─ Job A</span><br><span class="line">      │   ├─ Step 1</span><br><span class="line">      │   ├─ Step 2</span><br><span class="line">      │   └─ Step 3</span><br><span class="line">      └─ Job B</span><br><span class="line">          ├─ Step 1</span><br><span class="line">          └─ Step 2</span><br></pre></td></tr></table></figure><p>工作流文件本身属于仓库代码，应像源代码一样接受评审。</p><hr><h2 id="二、最小可运行示例">二、最小可运行示例</h2><p>创建 <code>.github/workflows/hello.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></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Hello</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">say-hello:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Print</span> <span class="string">message</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;Hello, GitHub Actions!&quot;</span></span><br></pre></td></tr></table></figure><p>含义：</p><ol><li class="lvl-3"><p><code>name: Hello</code>：Actions 页面显示的工作流名称。</p></li><li class="lvl-3"><p><code>on.push</code>：任意分支收到 push 时触发。</p></li><li class="lvl-3"><p><code>jobs</code>：定义工作流中的任务。</p></li><li class="lvl-3"><p><code>say-hello</code>：Job 的内部 ID，可以自定义。</p></li><li class="lvl-3"><p><code>runs-on</code>：Job 在 GitHub 托管的 Ubuntu Runner 中运行。</p></li><li class="lvl-3"><p><code>steps</code>：Job 内按顺序执行的步骤。</p></li><li class="lvl-3"><p><code>run</code>：在 Runner 的 Shell 中执行命令。</p></li></ol><p>推送该文件后，可以在仓库的 <strong>Actions</strong> 页面查看运行记录和日志。</p><hr><h2 id="三、Workflow、Job、Step、Action-和-Runner">三、Workflow、Job、Step、Action 和 Runner</h2><h3 id="3-1-Workflow">3.1 Workflow</h3><p>Workflow 是一个 YAML 文件，描述什么时候运行以及运行什么。文件只能位于：</p><figure class="highlight text"><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">.github/workflows/*.yml</span><br><span class="line">.github/workflows/*.yaml</span><br></pre></td></tr></table></figure><p>子目录中的 YAML 不会被当作 Workflow：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">.github/workflows/release/npm.yml  # 不会作为工作流加载</span><br></pre></td></tr></table></figure><h3 id="3-2-Job">3.2 Job</h3><p>Job 是一组在同一 Runner 中执行的步骤。同一个 Job 的 Step：</p><ul class="lvl-0"><li class="lvl-2"><p>共享工作目录。</p></li><li class="lvl-2"><p>共享该 Job 中创建的文件。</p></li><li class="lvl-2"><p>默认按声明顺序执行。</p></li><li class="lvl-2"><p>任一步失败后，后续普通步骤默认跳过。</p></li></ul><p>不同 Job：</p><ul class="lvl-0"><li class="lvl-2"><p>默认并行执行。</p></li><li class="lvl-2"><p>通常运行在不同的全新 Runner 中。</p></li><li class="lvl-2"><p>不会自动共享文件或环境变量。</p></li><li class="lvl-2"><p>需要通过 <code>needs</code> 设置顺序，通过 Artifact 共享文件。</p></li></ul><h3 id="3-3-Step">3.3 Step</h3><p>Step 是 Job 中的单个操作，有两种主要形式：</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 执行 Shell 命令</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Run</span> <span class="string">tests</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 调用已有 Action</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Checkout</span> <span class="string">source</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br></pre></td></tr></table></figure><p>一个 Step 不能同时使用 <code>run</code> 和 <code>uses</code>。</p><h3 id="3-4-Action">3.4 Action</h3><p>Action 是可复用的自动化组件，封装了检出代码、安装运行时、缓存依赖、上传产物等常见操作。在 Workflow 中通过 <code>uses</code> 引用，格式为 <code>owner/repository@ref</code>。</p><p>引用格式：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">uses:</span> <span class="string">&lt;owner&gt;/&lt;repo&gt;@&lt;ref&gt;</span></span><br><span class="line"><span class="comment"># 其中：</span></span><br><span class="line"><span class="comment"># &lt;owner&gt;：GitHub 用户或组织</span></span><br><span class="line"><span class="comment"># &lt;repo&gt;：Action 仓库名</span></span><br><span class="line"><span class="comment"># &lt;ref&gt;：Git 引用（Git ref）</span></span><br></pre></td></tr></table></figure><p>其中 <code>ref</code> 可以是标签、分支或完整 commit SHA：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">uses:</span> <span class="string">actions/checkout@v5</span>        <span class="comment"># 指定浮动的主版本标签</span></span><br><span class="line"><span class="attr">uses:</span> <span class="string">actions/checkout@v5.0.0</span>    <span class="comment"># 指定固定版本</span></span><br><span class="line"><span class="attr">uses:</span> <span class="string">owner/action@main</span>          <span class="comment"># 指定分支名称，一般不推荐，因为分支上的代码可能随时变化</span></span><br><span class="line"><span class="attr">uses:</span> <span class="string">owner/action@8f4b7f...完整的40位SHA</span> <span class="comment"># 指定 commit SHA，最安全，完全固定，不会受到 Tag 被移动或重新发布的影响</span></span><br></pre></td></tr></table></figure><p><code>uses: actions/checkout@v5</code> 中 <code>v5</code> 是一个浮动的主版本标签，它并不是固定版本，而是会随着 <code>v5</code> 系列的小版本升级而更新，即永远指向该主版本的最新版本，例如下面 <code>v5 ==&gt; v5.2.3</code>。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">v5</span><br><span class="line"> ├── v5.0.0</span><br><span class="line"> ├── v5.0.1</span><br><span class="line"> ├── v5.1.0</span><br><span class="line"> └── v5.2.3</span><br></pre></td></tr></table></figure><p>这意味着我愿意接受所有 v5 系列的更新，但不会自动升级到 v6。这样既能获得 Bug 修复和安全更新，又避免了跨主版本升级带来的兼容性风险。</p><table><thead><tr><th>写法</th><th>是否推荐</th><th>原因</th></tr></thead><tbody><tr><td><code>@v5</code></td><td>✅ 推荐</td><td>自动获取同一主版本内的修复和安全更新。</td></tr><tr><td><code>@v5.0.0</code></td><td>✅ 推荐（需要严格可重复构建时）</td><td>固定版本，行为稳定。</td></tr><tr><td><code>@&lt;commit-sha&gt;</code></td><td>✅ 强烈推荐（高安全场景）</td><td>防止 Tag 被篡改或移动，供应链安全最佳实践。</td></tr><tr><td><code>@main</code></td><td>❌ 不推荐</td><td>分支代码会变化，工作流行为不可预测。</td></tr><tr><td><code>@branch</code></td><td>❌ 不推荐</td><td>同上。</td></tr></tbody></table><h3 id="3-5-去哪里找-Action">3.5 去哪里找 Action</h3><p>编写 Workflow 时，通常从以下两个渠道查找 Action。</p><h4 id="官方-Action-仓库">官方 Action 仓库</h4><p>GitHub 在 <a href="https://github.com/actions">actions</a> 组织下维护了几乎所有官方 Action，是首选参考来源。常见 Action 如下：</p><table><thead><tr><th style="text-align:left">Action</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td style="text-align:left"><code>actions/checkout</code></td><td style="text-align:left">检出代码</td></tr><tr><td style="text-align:left"><code>actions/setup-node</code></td><td style="text-align:left">安装 Node.js</td></tr><tr><td style="text-align:left"><code>actions/setup-java</code></td><td style="text-align:left">安装 Java</td></tr><tr><td style="text-align:left"><code>actions/setup-python</code></td><td style="text-align:left">安装 Python</td></tr><tr><td style="text-align:left"><code>actions/setup-go</code></td><td style="text-align:left">安装 Go</td></tr><tr><td style="text-align:left"><code>actions/setup-dotnet</code></td><td style="text-align:left">安装 .NET</td></tr><tr><td style="text-align:left"><code>actions/cache</code></td><td style="text-align:left">缓存目录</td></tr><tr><td style="text-align:left"><code>actions/upload-artifact</code></td><td style="text-align:left">上传构建产物</td></tr><tr><td style="text-align:left"><code>actions/download-artifact</code></td><td style="text-align:left">下载构建产物</td></tr><tr><td style="text-align:left"><code>actions/github-script</code></td><td style="text-align:left">执行 JavaScript 操作 GitHub API</td></tr><tr><td style="text-align:left"><code>actions/labeler</code></td><td style="text-align:left">自动打 Label</td></tr><tr><td style="text-align:left"><code>actions/stale</code></td><td style="text-align:left">自动关闭长期未处理的 Issue</td></tr><tr><td style="text-align:left"><code>actions/upload-pages-artifact</code></td><td style="text-align:left">上传 GitHub Pages 构建产物</td></tr></tbody></table><p>每个 Action 仓库的 README 和 Release Notes 说明了输入参数、输出和版本变更，升级主版本前应逐一核对。</p><h4 id="GitHub-Marketplace">GitHub Marketplace</h4><p><a href="https://github.com/marketplace?type=actions">GitHub Marketplace</a> 收录了官方和第三方 Action，覆盖面最广。可按关键词搜索，例如：</p><ul class="lvl-0"><li class="lvl-2"><p><code>docker</code>、<code>aws</code>、<code>azure</code>、<code>kubernetes</code>：云与容器相关</p></li><li class="lvl-2"><p><code>slack</code>、<code>discord</code>：通知集成</p></li><li class="lvl-2"><p><code>npm</code>、<code>gradle</code>：包管理与构建工具</p></li></ul><p>Marketplace 中的第三方 Action 质量参差不齐，引入前应查看维护状态、Issue 数量和最近更新时间，优先选择活跃维护的项目；对安全敏感的场景，建议固定到完整 commit SHA（见 <a href="#%E4%BA%8C%E5%8D%81%E4%B8%80%E5%AE%89%E5%85%A8%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5">第二十一章</a>）。</p><h3 id="3-6-Runner">3.6 Runner</h3><p>Runner 是执行 Job 的机器。GitHub 托管 Runner 常用标签：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"><span class="attr">runs-on:</span> <span class="string">windows-latest</span></span><br><span class="line"><span class="attr">runs-on:</span> <span class="string">macos-latest</span></span><br></pre></td></tr></table></figure><p>也可以使用自托管 Runner：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">runs-on:</span> [<span class="string">self-hosted</span>, <span class="string">linux</span>, <span class="string">x64</span>]</span><br></pre></td></tr></table></figure><p>注意：</p><ul class="lvl-0"><li class="lvl-2"><p>GitHub 托管 Runner 通常是一次性的虚拟环境。</p></li><li class="lvl-2"><p>不要假设上一次 Workflow 创建的文件还存在。</p></li><li class="lvl-2"><p><code>ubuntu-latest</code> 会随 GitHub 更新，不代表永久固定的 Ubuntu 版本。</p></li><li class="lvl-2"><p>对环境高度敏感时可以使用明确的 Runner 标签或容器。</p></li></ul><hr><h2 id="四、Workflow-顶层语法">四、Workflow 顶层语法</h2><p>一个较完整的结构如下：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><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="attr">name:</span> <span class="string">CI</span></span><br><span class="line"><span class="attr">run-name:</span> <span class="string">CI</span> <span class="string">for</span> <span class="string">$&#123;&#123;</span> <span class="string">github.ref_name</span> <span class="string">&#125;&#125;</span> <span class="string">by</span> <span class="string">@$&#123;&#123;</span> <span class="string">github.actor</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>]</span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line"></span><br><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line"></span><br><span class="line"><span class="attr">env:</span></span><br><span class="line">  <span class="attr">NODE_VERSION:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">defaults:</span></span><br><span class="line">  <span class="attr">run:</span></span><br><span class="line">    <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line"></span><br><span class="line"><span class="attr">concurrency:</span></span><br><span class="line">  <span class="attr">group:</span> <span class="string">ci-$&#123;&#123;</span> <span class="string">github.workflow</span> <span class="string">&#125;&#125;-$&#123;&#123;</span> <span class="string">github.ref</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">cancel-in-progress:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">timeout-minutes:</span> <span class="number">15</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><h3 id="4-1-name">4.1 <code>name</code></h3><p>Actions 页面中的工作流名称：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Continuous</span> <span class="string">Integration</span></span><br></pre></td></tr></table></figure><p>省略时，GitHub 通常显示文件路径。</p><h3 id="4-2-run-name">4.2 <code>run-name</code></h3><p>单次运行记录的动态名称：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">run-name:</span> <span class="string">Deploy</span> <span class="string">$&#123;&#123;</span> <span class="string">inputs.environment</span> <span class="string">&#125;&#125;</span> <span class="string">by</span> <span class="string">@$&#123;&#123;</span> <span class="string">github.actor</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><h3 id="4-3-env">4.3 <code>env</code></h3><p>定义整个 Workflow 可用的环境变量：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">env:</span></span><br><span class="line">  <span class="attr">NODE_ENV:</span> <span class="string">test</span></span><br><span class="line">  <span class="attr">CI:</span> <span class="string">&quot;true&quot;</span></span><br></pre></td></tr></table></figure><h3 id="4-4-defaults">4.4 <code>defaults</code></h3><p>为所有 <code>run</code> Step 设置默认 Shell 或工作目录：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">defaults:</span></span><br><span class="line">  <span class="attr">run:</span></span><br><span class="line">    <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line">    <span class="attr">working-directory:</span> <span class="string">./backend</span></span><br></pre></td></tr></table></figure><p><code>defaults.run</code> 中不能使用 Context 或表达式。</p><hr><h2 id="五、触发事件-on">五、触发事件 <code>on</code></h2><h3 id="5-1-任意-push">5.1 任意 push</h3><figure class="highlight yaml"><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="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br></pre></td></tr></table></figure><p>简写：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span> <span class="string">push</span></span><br></pre></td></tr></table></figure><p>多个简单事件：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span> [<span class="string">push</span>, <span class="string">pull_request</span>]</span><br></pre></td></tr></table></figure><h3 id="5-2-限定分支">5.2 限定分支</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">main</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;release/**&quot;</span></span><br></pre></td></tr></table></figure><p>只忽略某些分支：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches-ignore:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;docs/**&quot;</span></span><br></pre></td></tr></table></figure><p>同一个事件中不能同时使用 <code>branches</code> 和 <code>branches-ignore</code>。需要混合规则时，在 <code>branches</code> 中使用 <code>!</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></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;**&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;!experimental/**&quot;</span></span><br></pre></td></tr></table></figure><p>负模式顺序有意义：后面的匹配可以改变前面的结果。</p><h3 id="5-3-标签触发">5.3 标签触发</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">tags:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;v*&quot;</span></span><br></pre></td></tr></table></figure><p>匹配示例：</p><figure class="highlight text"><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">v1.0.0       匹配</span><br><span class="line">v3.0.12      匹配</span><br><span class="line">release-1.0  不匹配</span><br></pre></td></tr></table></figure><p>更严格的模式：</p><figure class="highlight yaml"><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="attr">tags:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="string">&quot;v[0-9]+.[0-9]+.[0-9]+&quot;</span></span><br></pre></td></tr></table></figure><p>GitHub 的过滤模式是 glob，不是完整正则表达式；复杂版本校验仍应在 Step 中执行。</p><h3 id="5-4-路径过滤">5.4 路径过滤</h3><p>只在源码改变时运行：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;src/**&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;package.json&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;package-lock.json&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;.github/workflows/ci.yml&quot;</span></span><br></pre></td></tr></table></figure><p>忽略纯文档修改：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line">    <span class="attr">paths-ignore:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;**/*.md&quot;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;docs/**&quot;</span></span><br></pre></td></tr></table></figure><p>同时配置分支和路径时，两者必须都匹配：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>]</span><br><span class="line">    <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&quot;src/**&quot;</span></span><br></pre></td></tr></table></figure><p>注意：如果分支保护规则把某个 Workflow 设为必需检查，路径过滤导致它完全不运行时，该检查可能一直显示 Pending。此时可以让 Workflow 总是触发，再在 Job 中通过 <code>if</code> 决定是否执行。</p><h3 id="5-5-Pull-Request">5.5 Pull Request</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>]</span><br><span class="line">    <span class="attr">types:</span> [<span class="string">opened</span>, <span class="string">synchronize</span>, <span class="string">reopened</span>, <span class="string">ready_for_review</span>]</span><br></pre></td></tr></table></figure><p>常见 <code>types</code>：</p><ul class="lvl-0"><li class="lvl-2"><p><code>opened</code>：PR 新建。</p></li><li class="lvl-2"><p><code>synchronize</code>：PR 分支出现新提交。</p></li><li class="lvl-2"><p><code>reopened</code>：重新打开。</p></li><li class="lvl-2"><p><code>closed</code>：关闭或合并。</p></li><li class="lvl-2"><p><code>ready_for_review</code>：Draft 转为可评审。</p></li></ul><p>仅在 PR 被合并后执行：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line">    <span class="attr">types:</span> [<span class="string">closed</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">after-merge:</span></span><br><span class="line">    <span class="attr">if:</span> <span class="string">github.event.pull_request.merged</span> <span class="string">==</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;PR merged&quot;</span></span><br></pre></td></tr></table></figure><p><code>pull_request_target</code> 与 <code>pull_request</code> 不同。它在目标仓库默认分支的安全上下文中运行，可能拥有 Secret 和写权限。不要在 <code>pull_request_target</code> 中检出并执行不受信任的 PR 代码。</p><h3 id="5-6-Release">5.6 Release</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">release:</span></span><br><span class="line">    <span class="attr">types:</span> [<span class="string">published</span>]</span><br></pre></td></tr></table></figure><p>适合在 GitHub Release 发布后上传资源或同步到其他平台。</p><h3 id="5-7-多事件不同配置">5.7 多事件不同配置</h3><p>只要其中一个事件触发就会运行：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>]</span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>]</span><br><span class="line">  <span class="attr">workflow_dispatch:</span></span><br></pre></td></tr></table></figure><hr><h2 id="六、权限-permissions-与-GITHUB-TOKEN">六、权限 <code>permissions</code> 与 <code>GITHUB_TOKEN</code></h2><p>每次 Workflow 运行时，GitHub 自动创建短期 <code>GITHUB_TOKEN</code>。它只对当前仓库和本次运行有效，任务结束后失效。</p><h3 id="6-1-最小只读权限">6.1 最小只读权限</h3><figure class="highlight yaml"><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="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br></pre></td></tr></table></figure><p>检出代码通常只需要 <code>contents: read</code>。</p><h3 id="6-2-禁用全部默认权限">6.2 禁用全部默认权限</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">permissions:</span> &#123;&#125;</span><br></pre></td></tr></table></figure><p>随后在具体 Job 中按需授权：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">release:</span></span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">contents:</span> <span class="string">write</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">gh</span> <span class="string">release</span> <span class="string">create</span> <span class="string">v1.0.0</span> <span class="string">--generate-notes</span></span><br><span class="line">        <span class="attr">env:</span></span><br><span class="line">          <span class="attr">GH_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><h3 id="6-3-常用权限">6.3 常用权限</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">write</span>         <span class="comment"># 创建标签或 Release；write 已包含 read</span></span><br><span class="line">  <span class="attr">pull-requests:</span> <span class="string">write</span>    <span class="comment"># 评论、标记 PR</span></span><br><span class="line">  <span class="attr">issues:</span> <span class="string">write</span>           <span class="comment"># 创建或修改 Issue</span></span><br><span class="line">  <span class="attr">packages:</span> <span class="string">write</span>         <span class="comment"># 发布 GitHub Packages / GHCR</span></span><br><span class="line">  <span class="attr">actions:</span> <span class="string">read</span>           <span class="comment"># 读取 Actions 信息</span></span><br><span class="line">  <span class="attr">checks:</span> <span class="string">write</span>           <span class="comment"># 创建检查结果</span></span><br><span class="line">  <span class="attr">security-events:</span> <span class="string">write</span>  <span class="comment"># 上传代码扫描结果</span></span><br><span class="line">  <span class="attr">id-token:</span> <span class="string">write</span>         <span class="comment"># 请求 OIDC 身份令牌</span></span><br></pre></td></tr></table></figure><p>以上是权限名称展示，不代表一个普通 Job 应同时拥有全部权限。实际工作流应只声明当前 Job 必需的最少几项。</p><p>只要显式设置了某些权限，未列出的权限通常会变成 <code>none</code>。</p><h3 id="6-4-使用-Token">6.4 使用 Token</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Create</span> <span class="string">release</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">gh</span> <span class="string">release</span> <span class="string">create</span> <span class="string">&quot;$TAG&quot;</span> <span class="string">--generate-notes</span></span><br><span class="line">  <span class="attr">env:</span></span><br><span class="line">    <span class="attr">GH_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><p>即使没有显式把 Token 传给第三方 Action，Action 仍可能通过 <code>github.token</code> 获取它，因此最小权限非常重要。</p><hr><h2 id="七、并发控制-concurrency">七、并发控制 <code>concurrency</code></h2><p>并发组用于避免同一类任务同时运行。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">concurrency:</span></span><br><span class="line">  <span class="attr">group:</span> <span class="string">ci-$&#123;&#123;</span> <span class="string">github.workflow</span> <span class="string">&#125;&#125;-$&#123;&#123;</span> <span class="string">github.ref</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">cancel-in-progress:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>变量含义：</p><ul class="lvl-0"><li class="lvl-2"><p><code>github.workflow</code>：Workflow 的 <code>name</code>，例如 <code>CI</code>。</p></li><li class="lvl-2"><p><code>github.ref</code>：完整 Git 引用，例如 <code>refs/heads/main</code>、<code>refs/pull/123/merge</code> 或 <code>refs/tags/v3.0.12</code>。</p></li><li class="lvl-2"><p><code>github.ref_name</code>：简短名称，例如 <code>main</code> 或 <code>v3.0.12</code>。</p></li></ul><p>main 分支示例：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ci-CI-refs/heads/main</span><br></pre></td></tr></table></figure><p>同一个 PR 连续推送多次时：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">cancel-in-progress:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><p>旧 CI 会被取消，只保留最新提交的 CI，节约时间。</p><p>发布任务一般不应中途取消：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">concurrency:</span></span><br><span class="line">  <span class="attr">group:</span> <span class="string">publish-$&#123;&#123;</span> <span class="string">github.ref</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">cancel-in-progress:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>否则可能出现“npm 已发布，但 GitHub Release 尚未创建”这种不完整状态。</p><p>按部署环境分组：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">concurrency:</span></span><br><span class="line">  <span class="attr">group:</span> <span class="string">deploy-$&#123;&#123;</span> <span class="string">inputs.environment</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">cancel-in-progress:</span> <span class="literal">false</span></span><br></pre></td></tr></table></figure><p>注意：</p><ul class="lvl-0"><li class="lvl-2"><p>同一并发组最多有一个 Running 和一个 Pending 运行。</p></li><li class="lvl-2"><p>并发组名称不区分大小写。</p></li><li class="lvl-2"><p>GitHub 不保证同组 Pending 任务的执行顺序。</p></li></ul><hr><h2 id="八、Job-语法">八、Job 语法</h2><h3 id="8-1-Job-ID-与显示名称">8.1 Job ID 与显示名称</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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="attr">jobs:</span></span><br><span class="line">  <span class="attr">unit-test:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">Unit</span> <span class="string">tests</span> <span class="string">on</span> <span class="string">Node.js</span> <span class="number">24</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p><code>unit-test</code> 是内部 ID，用于 <code>needs.unit-test</code>。</p></li><li class="lvl-2"><p><code>name</code> 是 Actions 页面显示名称。</p></li></ul><p>Job ID 建议只使用字母、数字、<code>-</code> 和 <code>_</code>。</p><h3 id="8-2-选择-Runner">8.2 选择 Runner</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">linux:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">windows:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">windows-latest</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">mac:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">macos-latest</span></span><br></pre></td></tr></table></figure><h3 id="8-3-超时">8.3 超时</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">timeout-minutes:</span> <span class="number">20</span></span><br></pre></td></tr></table></figure><p>应为可能卡住的网络、测试和部署 Job 设置合理超时。</p><h3 id="8-4-Job-级环境变量和权限">8.4 Job 级环境变量和权限</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">    <span class="attr">env:</span></span><br><span class="line">      <span class="attr">NODE_ENV:</span> <span class="string">test</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><h3 id="8-5-Job-默认-Shell-和目录">8.5 Job 默认 Shell 和目录</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">backend:</span></span><br><span class="line">    <span class="attr">defaults:</span></span><br><span class="line">      <span class="attr">run:</span></span><br><span class="line">        <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line">        <span class="attr">working-directory:</span> <span class="string">backend</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><hr><h2 id="九、Step-语法">九、Step 语法</h2><h3 id="9-1-执行单行和多行命令">9.1 执行单行和多行命令</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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="bullet">-</span> <span class="attr">name:</span> <span class="string">Single</span> <span class="string">command</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Multiple</span> <span class="string">commands</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    npm ci</span></span><br><span class="line"><span class="string">    npm run typecheck</span></span><br><span class="line"><span class="string">    npm test</span></span><br></pre></td></tr></table></figure><h3 id="9-2-指定-Shell">9.2 指定 Shell</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;bash&quot;</span></span><br><span class="line">  <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">Write-Output</span> <span class="string">&quot;PowerShell&quot;</span></span><br><span class="line">  <span class="attr">shell:</span> <span class="string">pwsh</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    print(&quot;Python&quot;)</span></span><br><span class="line"><span class="string"></span>  <span class="attr">shell:</span> <span class="string">python</span></span><br></pre></td></tr></table></figure><p>不同 Runner 的默认 Shell 不完全相同。跨平台 Workflow 最好显式指定。</p><h3 id="9-3-指定工作目录">9.3 指定工作目录</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Test</span> <span class="string">frontend</span></span><br><span class="line">  <span class="attr">working-directory:</span> <span class="string">frontend</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    npm ci</span></span><br><span class="line"><span class="string">    npm test</span></span><br></pre></td></tr></table></figure><h3 id="9-4-调用-Action-并传参">9.4 调用 Action 并传参</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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="bullet">-</span> <span class="attr">name:</span> <span class="string">Setup</span> <span class="string">Node.js</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">node-version:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line">    <span class="attr">cache:</span> <span class="string">npm</span></span><br><span class="line">    <span class="attr">cache-dependency-path:</span> <span class="string">|</span></span><br><span class="line"><span class="string">      package-lock.json</span></span><br><span class="line"><span class="string">      frontend/package-lock.json</span></span><br></pre></td></tr></table></figure><p><code>with</code> 支持哪些参数由该 Action 的 <code>action.yml</code> 定义，应查阅对应仓库文档。</p><h3 id="9-5-Step-ID-和输出">9.5 Step ID 和输出</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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 class="attr">name:</span> <span class="string">Read</span> <span class="string">version</span></span><br><span class="line">  <span class="attr">id:</span> <span class="string">package</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;version=$(node -p &quot;</span><span class="string">require(&#x27;./package.json&#x27;).version&quot;)&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_OUTPUT&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Print</span> <span class="string">version</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;Version is $<span class="template-variable">&#123;&#123; steps.package.outputs.version &#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><p><code>id</code> 是表达式引用名，不是显示名称。</p><h3 id="9-6-超时和允许失败">9.6 超时和允许失败</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Optional</span> <span class="string">audit</span></span><br><span class="line">  <span class="attr">continue-on-error:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">timeout-minutes:</span> <span class="number">5</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">npm</span> <span class="string">audit</span></span><br></pre></td></tr></table></figure><p><code>continue-on-error</code> 应谨慎使用，否则真正的问题可能被隐藏。</p><hr><h2 id="十、变量、Context-和表达式">十、变量、Context 和表达式</h2><p>表达式写在：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">$&#123;&#123;</span> <span class="string">...</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><h3 id="10-1-常用-Context">10.1 常用 Context</h3><h4 id="github"><code>github</code></h4><p>当前仓库、事件和运行信息：</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></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    echo &quot;repository=$&#123;&#123; github.repository &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;actor=$&#123;&#123; github.actor &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;sha=$&#123;&#123; github.sha &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;ref=$&#123;&#123; github.ref &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;ref_name=$&#123;&#123; github.ref_name &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;event=$&#123;&#123; github.event_name &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;run_id=$&#123;&#123; github.run_id &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;attempt=$&#123;&#123; github.run_attempt &#125;&#125;&quot;</span></span><br></pre></td></tr></table></figure><p>常见值：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">github.repository = hanqunfeng/claude-trace</span><br><span class="line">github.actor      = 触发任务的 GitHub 用户</span><br><span class="line">github.sha        = 当前提交 SHA</span><br><span class="line">github.ref        = refs/heads/main 或 refs/tags/v3.0.12</span><br><span class="line">github.ref_name   = main 或 v3.0.12</span><br><span class="line">github.workflow   = Workflow 的 name</span><br><span class="line">github.job        = 当前 Job ID</span><br></pre></td></tr></table></figure><h4 id="env"><code>env</code></h4><p>读取 YAML 中定义的环境变量：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">env:</span></span><br><span class="line">  <span class="attr">APP_NAME:</span> <span class="string">demo</span></span><br><span class="line"></span><br><span class="line"><span class="attr">steps:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$<span class="template-variable">&#123;&#123; env.APP_NAME &#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><p>Shell 内也可使用：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$APP_NAME&quot;</span></span><br></pre></td></tr></table></figure><h4 id="vars"><code>vars</code></h4><p>读取仓库、组织或 Environment 配置变量：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;Region=$<span class="template-variable">&#123;&#123; vars.DEPLOY_REGION &#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><p><code>vars</code> 不是 Secret，会显示在日志和界面中。</p><h4 id="secrets"><code>secrets</code></h4><p>读取加密 Secret：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">./deploy.sh</span></span><br><span class="line">  <span class="attr">env:</span></span><br><span class="line">    <span class="attr">API_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.API_TOKEN</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><h4 id="runner"><code>runner</code></h4><p>Runner 信息：</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></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    echo &quot;OS=$&#123;&#123; runner.os &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;Arch=$&#123;&#123; runner.arch &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;Temp=$&#123;&#123; runner.temp &#125;&#125;&quot;</span></span><br></pre></td></tr></table></figure><h4 id="job"><code>job</code></h4><figure class="highlight yaml"><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="bullet">-</span> <span class="attr">if:</span> <span class="string">always()</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;Job status=$<span class="template-variable">&#123;&#123; job.status &#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><h4 id="steps"><code>steps</code></h4><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></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">id:</span> <span class="string">tests</span></span><br><span class="line">  <span class="attr">continue-on-error:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    echo &quot;outcome=$&#123;&#123; steps.tests.outcome &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;conclusion=$&#123;&#123; steps.tests.conclusion &#125;&#125;&quot;</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p><code>outcome</code>：应用 <code>continue-on-error</code> 之前的原始结果。</p></li><li class="lvl-2"><p><code>conclusion</code>：应用之后的最终结果。</p></li></ul><h4 id="needs"><code>needs</code></h4><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$<span class="template-variable">&#123;&#123; needs.build.outputs.artifact-name &#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><h4 id="matrix"><code>matrix</code></h4><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;Node.js $<span class="template-variable">&#123;&#123; matrix.node &#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><h4 id="inputs"><code>inputs</code></h4><p>手动触发或复用 Workflow 的输入：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;Environment=$<span class="template-variable">&#123;&#123; inputs.environment &#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><h3 id="10-2-运算符">10.2 运算符</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">if:</span> <span class="string">github.ref</span> <span class="string">==</span> <span class="string">&#x27;refs/heads/main&#x27;</span></span><br><span class="line"><span class="attr">if:</span> <span class="string">github.ref</span> <span class="type">!=</span> <span class="string">&#x27;refs/heads/main&#x27;</span></span><br><span class="line"><span class="attr">if:</span> <span class="string">success()</span> <span class="string">&amp;&amp;</span> <span class="string">github.actor</span> <span class="type">!=</span> <span class="string">&#x27;dependabot[bot]&#x27;</span></span><br><span class="line"><span class="attr">if:</span> <span class="string">failure()</span> <span class="string">||</span> <span class="string">cancelled()</span></span><br><span class="line"><span class="attr">if:</span> <span class="type">!cancelled()</span></span><br></pre></td></tr></table></figure><p>在 YAML 中以 <code>!</code> 开头容易被当成 YAML Tag，最好保留 <code>$&#123;&#123; &#125;&#125;</code>：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">if:</span> <span class="string">$&#123;&#123;</span> <span class="type">!cancelled()</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><h3 id="10-3-常用函数">10.3 常用函数</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">if:</span> <span class="string">startsWith(github.ref,</span> <span class="string">&#x27;refs/tags/v&#x27;</span><span class="string">)</span></span><br><span class="line"><span class="attr">if:</span> <span class="string">endsWith(github.event.head_commit.message,</span> <span class="string">&#x27;[deploy]&#x27;</span><span class="string">)</span></span><br><span class="line"><span class="attr">if:</span> <span class="string">contains(github.event.pull_request.labels.*.name,</span> <span class="string">&#x27;ready&#x27;</span><span class="string">)</span></span><br><span class="line"><span class="attr">if:</span> <span class="string">contains(fromJSON(&#x27;[&quot;push&quot;,&quot;workflow_dispatch&quot;]&#x27;),</span> <span class="string">github.event_name)</span></span><br></pre></td></tr></table></figure><p>JSON 转换：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">env:</span></span><br><span class="line">  <span class="attr">CONTINUE:</span> <span class="string">&quot;true&quot;</span></span><br><span class="line">  <span class="attr">TIMEOUT:</span> <span class="string">&quot;10&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">continue-on-error:</span> <span class="string">$&#123;&#123;</span> <span class="string">fromJSON(env.CONTINUE)</span> <span class="string">&#125;&#125;</span></span><br><span class="line"><span class="attr">timeout-minutes:</span> <span class="string">$&#123;&#123;</span> <span class="string">fromJSON(env.TIMEOUT)</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><p>调试 Context：</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></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Dump</span> <span class="string">GitHub</span> <span class="string">context</span></span><br><span class="line">  <span class="attr">env:</span></span><br><span class="line">    <span class="attr">GITHUB_CONTEXT:</span> <span class="string">$&#123;&#123;</span> <span class="string">toJSON(github)</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$GITHUB_CONTEXT&quot;</span></span><br></pre></td></tr></table></figure><p>不要随意输出完整 <code>secrets</code>、Token 或包含敏感请求信息的 Context。</p><hr><h2 id="十一、环境变量、配置变量和-Secret">十一、环境变量、配置变量和 Secret</h2><h3 id="11-1-作用域优先级">11.1 作用域优先级</h3><p>环境变量可定义在 Workflow、Job 或 Step：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">env:</span></span><br><span class="line">  <span class="attr">LEVEL:</span> <span class="string">workflow</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">example:</span></span><br><span class="line">    <span class="attr">env:</span></span><br><span class="line">      <span class="attr">LEVEL:</span> <span class="string">job</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$LEVEL&quot;</span> <span class="comment"># 输出 job</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">env:</span></span><br><span class="line">          <span class="attr">LEVEL:</span> <span class="string">step</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$LEVEL&quot;</span> <span class="comment"># 输出 step</span></span><br></pre></td></tr></table></figure><p>越具体的作用域优先级越高。</p><h3 id="11-2-跨-Step-设置环境变量">11.2 跨 Step 设置环境变量</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Set</span> <span class="string">variable</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;BUILD_VERSION=1.2.3&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_ENV&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Read</span> <span class="string">variable</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$BUILD_VERSION&quot;</span></span><br></pre></td></tr></table></figure><p>写入 <code>$GITHUB_ENV</code> 后只对后续 Step 生效，当前 Step 中不会自动出现新值。</p><h3 id="11-3-添加-PATH">11.3 添加 PATH</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$HOME/.local/bin&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_PATH&quot;</span></span><br></pre></td></tr></table></figure><h3 id="11-4-Secret">11.4 Secret</h3><p>在仓库的 <strong>Settings → Secrets and variables → Actions</strong> 中配置：</p><figure class="highlight yaml"><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="attr">env:</span></span><br><span class="line">  <span class="attr">DEPLOY_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.DEPLOY_TOKEN</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><p>注意：</p><ul class="lvl-0"><li class="lvl-2"><p>Secret 不应直接写入仓库。</p></li><li class="lvl-2"><p>GitHub 会尝试遮蔽日志中的 Secret 原值，但不要依赖遮蔽来弥补主动输出。</p></li><li class="lvl-2"><p>来自 Fork 的 <code>pull_request</code> 默认无法读取普通 Secret。</p></li><li class="lvl-2"><p>Dependabot 触发的工作流也受到 Secret 和 Token 权限限制。</p></li><li class="lvl-2"><p>Secret 不能可靠地直接用于 <code>if</code>，可以先映射到 Job 环境变量。</p></li></ul><p>示例：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">deploy:</span></span><br><span class="line">    <span class="attr">env:</span></span><br><span class="line">      <span class="attr">HAS_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.DEPLOY_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="comment"># env Context 可以用于 Step 的 if，但不能用于 jobs.&lt;job_id&gt;.if。</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">env.HAS_TOKEN</span> <span class="type">!=</span> <span class="string">&#x27;&#x27;</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">./deploy.sh</span></span><br><span class="line">        <span class="attr">env:</span></span><br><span class="line">          <span class="attr">DEPLOY_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.DEPLOY_TOKEN</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><hr><h2 id="十二、条件执行-if">十二、条件执行 <code>if</code></h2><h3 id="12-1-分支和标签条件">12.1 分支和标签条件</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">if:</span> <span class="string">github.ref</span> <span class="string">==</span> <span class="string">&#x27;refs/heads/main&#x27;</span></span><br></pre></td></tr></table></figure><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">if:</span> <span class="string">startsWith(github.ref,</span> <span class="string">&#x27;refs/tags/v&#x27;</span><span class="string">)</span></span><br></pre></td></tr></table></figure><h3 id="12-2-事件条件">12.2 事件条件</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">if:</span> <span class="string">github.event_name</span> <span class="string">==</span> <span class="string">&#x27;workflow_dispatch&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="12-3-Job-成功、失败或取消">12.3 Job 成功、失败或取消</h3><p>默认相当于 <code>success()</code>：</p><figure class="highlight yaml"><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="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;Only on success&quot;</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">success()</span></span><br></pre></td></tr></table></figure><p>失败时上传日志：</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></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">failure</span> <span class="string">logs</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">failure()</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">actions/upload-artifact@v4</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">failure-logs</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">logs/</span></span><br></pre></td></tr></table></figure><p>无论前面结果如何都执行清理：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Cleanup</span></span><br><span class="line">  <span class="attr">if:</span> <span class="string">always()</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">./cleanup.sh</span></span><br></pre></td></tr></table></figure><p>更适合多数清理和汇总任务的写法：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">if:</span> <span class="string">$&#123;&#123;</span> <span class="type">!cancelled()</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><p><code>always()</code> 在任务被取消时仍可能执行，某些网络操作会拖延取消过程。</p><h3 id="12-4-Draft-PR-跳过测试">12.4 Draft PR 跳过测试</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">if:</span> <span class="string">github.event_name</span> <span class="type">!=</span> <span class="string">&#x27;pull_request&#x27;</span> <span class="string">||</span> <span class="string">github.event.pull_request.draft</span> <span class="string">==</span> <span class="literal">false</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><hr><h2 id="十三、Job-依赖和输出">十三、Job 依赖和输出</h2><h3 id="13-1-needs">13.1 <code>needs</code></h3><p>Job 默认并行。使用 <code>needs</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></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">run</span> <span class="string">build</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">deploy:</span></span><br><span class="line">    <span class="attr">needs:</span> <span class="string">build</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">./deploy.sh</span></span><br></pre></td></tr></table></figure><p>依赖多个 Job：</p><figure class="highlight yaml"><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="attr">deploy:</span></span><br><span class="line">  <span class="attr">needs:</span> [<span class="string">lint</span>, <span class="string">test</span>, <span class="string">build</span>]</span><br></pre></td></tr></table></figure><h3 id="13-2-Step-输出传给-Job">13.2 Step 输出传给 Job</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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">jobs:</span></span><br><span class="line">  <span class="attr">prepare:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">outputs:</span></span><br><span class="line">      <span class="attr">version:</span> <span class="string">$&#123;&#123;</span> <span class="string">steps.package.outputs.version</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">package</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;version=$(node -p &quot;</span><span class="string">require(&#x27;./package.json&#x27;).version&quot;)&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_OUTPUT&quot;</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">publish:</span></span><br><span class="line">    <span class="attr">needs:</span> <span class="string">prepare</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;Publishing $<span class="template-variable">&#123;&#123; needs.prepare.outputs.version &#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><p>输出适合少量文本。文件应通过 Artifact 传递。</p><h3 id="13-3-多行输出">13.3 多行输出</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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="bullet">-</span> <span class="attr">id:</span> <span class="string">notes</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    &#123;</span></span><br><span class="line"><span class="string">      echo &quot;body&lt;&lt;EOF&quot;</span></span><br><span class="line"><span class="string">      echo &quot;Line 1&quot;</span></span><br><span class="line"><span class="string">      echo &quot;Line 2&quot;</span></span><br><span class="line"><span class="string">      echo &quot;EOF&quot;</span></span><br><span class="line"><span class="string">    &#125; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;</span></span><br></pre></td></tr></table></figure><p>如果内容可能包含 <code>EOF</code>，应使用随机且不会出现在内容中的分隔符。</p><hr><h2 id="十四、Matrix-测试矩阵">十四、Matrix 测试矩阵</h2><h3 id="14-1-Node-js-多版本">14.1 Node.js 多版本</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">strategy:</span></span><br><span class="line">      <span class="attr">matrix:</span></span><br><span class="line">        <span class="attr">node:</span> [<span class="string">&quot;20&quot;</span>, <span class="string">&quot;22&quot;</span>, <span class="string">&quot;24&quot;</span>]</span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">node-version:</span> <span class="string">$&#123;&#123;</span> <span class="string">matrix.node</span> <span class="string">&#125;&#125;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><p>该配置创建 3 个并行 Job。</p><h3 id="14-2-多操作系统和多版本">14.2 多操作系统和多版本</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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="attr">strategy:</span></span><br><span class="line">  <span class="attr">matrix:</span></span><br><span class="line">    <span class="attr">os:</span> [<span class="string">ubuntu-latest</span>, <span class="string">windows-latest</span>, <span class="string">macos-latest</span>]</span><br><span class="line">    <span class="attr">node:</span> [<span class="string">&quot;20&quot;</span>, <span class="string">&quot;24&quot;</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">runs-on:</span> <span class="string">$&#123;&#123;</span> <span class="string">matrix.os</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><p>一共创建 <code>3 × 2 = 6</code> 个 Job。</p><h3 id="14-3-排除组合">14.3 排除组合</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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="attr">strategy:</span></span><br><span class="line">  <span class="attr">matrix:</span></span><br><span class="line">    <span class="attr">os:</span> [<span class="string">ubuntu-latest</span>, <span class="string">windows-latest</span>, <span class="string">macos-latest</span>]</span><br><span class="line">    <span class="attr">node:</span> [<span class="string">&quot;20&quot;</span>, <span class="string">&quot;24&quot;</span>]</span><br><span class="line">    <span class="attr">exclude:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">os:</span> <span class="string">macos-latest</span></span><br><span class="line">        <span class="attr">node:</span> <span class="string">&quot;20&quot;</span></span><br></pre></td></tr></table></figure><h3 id="14-4-添加特殊组合">14.4 添加特殊组合</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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="attr">strategy:</span></span><br><span class="line">  <span class="attr">matrix:</span></span><br><span class="line">    <span class="attr">node:</span> [<span class="string">&quot;20&quot;</span>, <span class="string">&quot;24&quot;</span>]</span><br><span class="line">    <span class="attr">include:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">node:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line">        <span class="attr">experimental:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><h3 id="14-5-失败策略">14.5 失败策略</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">strategy:</span></span><br><span class="line">  <span class="attr">fail-fast:</span> <span class="literal">false</span></span><br><span class="line">  <span class="attr">max-parallel:</span> <span class="number">3</span></span><br><span class="line">  <span class="attr">matrix:</span></span><br><span class="line">    <span class="attr">node:</span> [<span class="string">&quot;18&quot;</span>, <span class="string">&quot;20&quot;</span>, <span class="string">&quot;22&quot;</span>, <span class="string">&quot;24&quot;</span>]</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p><code>fail-fast: true</code>：一个普通矩阵 Job 失败后，取消其他进行中的矩阵 Job。</p></li><li class="lvl-2"><p><code>fail-fast: false</code>：让所有组合都运行完，便于看到完整兼容性结果。</p></li><li class="lvl-2"><p><code>max-parallel</code>：限制同时运行数量。</p></li></ul><p>实验版本允许失败：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">continue-on-error:</span> <span class="string">$&#123;&#123;</span> <span class="string">matrix.experimental</span> <span class="string">&#125;&#125;</span></span><br><span class="line"><span class="attr">strategy:</span></span><br><span class="line">  <span class="attr">matrix:</span></span><br><span class="line">    <span class="attr">node:</span> [<span class="string">&quot;20&quot;</span>, <span class="string">&quot;24&quot;</span>]</span><br><span class="line">    <span class="attr">experimental:</span> [<span class="literal">false</span>]</span><br><span class="line">    <span class="attr">include:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">node:</span> <span class="string">&quot;25&quot;</span></span><br><span class="line">        <span class="attr">experimental:</span> <span class="literal">true</span></span><br></pre></td></tr></table></figure><hr><h2 id="十五、缓存-Cache">十五、缓存 Cache</h2><p>缓存用于复用依赖下载数据，提高后续 Workflow 速度。缓存不是构建产物，也不能保证永久存在。</p><h3 id="15-1-setup-node-自动缓存-npm">15.1 setup-node 自动缓存 npm</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">node-version:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line">    <span class="attr">cache:</span> <span class="string">npm</span></span><br><span class="line">    <span class="attr">cache-dependency-path:</span> <span class="string">package-lock.json</span></span><br><span class="line"></span><br><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br></pre></td></tr></table></figure><p><code>setup-node</code> 缓存 npm 的全局下载缓存，不直接缓存 <code>node_modules</code>。<code>npm ci</code> 仍会根据 lockfile 创建干净依赖树。</p><p>Monorepo 多个 lockfile：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">cache-dependency-path:</span> <span class="string">|</span></span><br><span class="line"><span class="string">  package-lock.json</span></span><br><span class="line"><span class="string">  frontend/package-lock.json</span></span><br></pre></td></tr></table></figure><h3 id="15-2-actions-cache">15.2 actions/cache</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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="bullet">-</span> <span class="attr">name:</span> <span class="string">Cache</span> <span class="string">build</span> <span class="string">data</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">actions/cache@v4</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">.cache</span></span><br><span class="line">    <span class="attr">key:</span> <span class="string">$&#123;&#123;</span> <span class="string">runner.os</span> <span class="string">&#125;&#125;-build-$&#123;&#123;</span> <span class="string">hashFiles(&#x27;package-lock.json&#x27;)</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">restore-keys:</span> <span class="string">|</span></span><br><span class="line">      <span class="string">$&#123;&#123;</span> <span class="string">runner.os</span> <span class="string">&#125;&#125;-build-</span></span><br></pre></td></tr></table></figure><h3 id="15-3-缓存键">15.3 缓存键</h3><p>好的 Key 通常包含：</p><ul class="lvl-0"><li class="lvl-2"><p>操作系统。</p></li><li class="lvl-2"><p>运行时版本。</p></li><li class="lvl-2"><p>lockfile 哈希。</p></li><li class="lvl-2"><p>必要时加入配置版本。</p></li></ul><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">key:</span> <span class="string">npm-$&#123;&#123;</span> <span class="string">runner.os</span> <span class="string">&#125;&#125;-node-$&#123;&#123;</span> <span class="string">matrix.node</span> <span class="string">&#125;&#125;-$&#123;&#123;</span> <span class="string">hashFiles(&#x27;**/package-lock.json&#x27;)</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><h3 id="15-4-缓存安全">15.4 缓存安全</h3><ul class="lvl-0"><li class="lvl-2"><p>不要缓存 Secret、Token、<code>.npmrc</code> 认证文件。</p></li><li class="lvl-2"><p>Fork PR 可能读取基础分支创建的缓存。</p></li><li class="lvl-2"><p>不可信代码可能尝试缓存投毒。</p></li><li class="lvl-2"><p>发布任务优先考虑禁用缓存，以降低供应链风险。</p></li><li class="lvl-2"><p>Cache 命中不代表可以跳过 <code>npm ci</code> 或完整性检查。</p></li></ul><hr><h2 id="十六、构建产物-Artifact">十六、构建产物 Artifact</h2><p>Artifact 用于保存或跨 Job 传递文件，例如：</p><ul class="lvl-0"><li class="lvl-2"><p>编译结果。</p></li><li class="lvl-2"><p>测试报告。</p></li><li class="lvl-2"><p>覆盖率报告。</p></li><li class="lvl-2"><p>安装包。</p></li><li class="lvl-2"><p>失败日志。</p></li></ul><h3 id="16-1-上传">16.1 上传</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">dist</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">actions/upload-artifact@v4</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">dist</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">dist/</span></span><br><span class="line">    <span class="attr">if-no-files-found:</span> <span class="string">error</span></span><br><span class="line">    <span class="attr">retention-days:</span> <span class="number">7</span></span><br></pre></td></tr></table></figure><h3 id="16-2-下载">16.2 下载</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">jobs:</span></span><br><span class="line">  <span class="attr">build:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">run</span> <span class="string">build</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/upload-artifact@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">name:</span> <span class="string">dist</span></span><br><span class="line">          <span class="attr">path:</span> <span class="string">dist/</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">deploy:</span></span><br><span class="line">    <span class="attr">needs:</span> <span class="string">build</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/download-artifact@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">name:</span> <span class="string">dist</span></span><br><span class="line">          <span class="attr">path:</span> <span class="string">dist/</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">ls</span> <span class="string">-R</span> <span class="string">dist</span></span><br></pre></td></tr></table></figure><h3 id="16-3-Cache-与-Artifact-的区别">16.3 Cache 与 Artifact 的区别</h3><ul class="lvl-0"><li class="lvl-2"><p>Cache：优化速度，可被后续运行复用，可能被清理，不适合发布交付。</p></li><li class="lvl-2"><p>Artifact：保存本次运行结果，可下载或供后续 Job 使用，有明确保留期限。</p></li></ul><hr><h2 id="十七、容器和服务数据库">十七、容器和服务数据库</h2><h3 id="17-1-Job-运行在容器中">17.1 Job 运行在容器中</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">container:</span></span><br><span class="line">      <span class="attr">image:</span> <span class="string">node:24-bookworm</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><h3 id="17-2-PostgreSQL-服务">17.2 PostgreSQL 服务</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">jobs:</span></span><br><span class="line">  <span class="attr">integration-test:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">services:</span></span><br><span class="line">      <span class="attr">postgres:</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">postgres:17</span></span><br><span class="line">        <span class="attr">env:</span></span><br><span class="line">          <span class="attr">POSTGRES_USER:</span> <span class="string">test</span></span><br><span class="line">          <span class="attr">POSTGRES_PASSWORD:</span> <span class="string">test</span></span><br><span class="line">          <span class="attr">POSTGRES_DB:</span> <span class="string">app_test</span></span><br><span class="line">        <span class="attr">ports:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="number">5432</span><span class="string">:5432</span></span><br><span class="line">        <span class="attr">options:</span> <span class="string">&gt;-</span></span><br><span class="line"><span class="string">          --health-cmd &quot;pg_isready&quot;</span></span><br><span class="line"><span class="string">          --health-interval 10s</span></span><br><span class="line"><span class="string">          --health-timeout 5s</span></span><br><span class="line"><span class="string">          --health-retries 5</span></span><br><span class="line"><span class="string"></span>    <span class="attr">env:</span></span><br><span class="line">      <span class="attr">DATABASE_URL:</span> <span class="string">postgresql://test:test@localhost:5432/app_test</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">run</span> <span class="string">test:integration</span></span><br></pre></td></tr></table></figure><h3 id="17-3-Redis-服务">17.3 Redis 服务</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">redis:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">redis:7</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="number">6379</span><span class="string">:6379</span></span><br><span class="line">    <span class="attr">options:</span> <span class="string">&gt;-</span></span><br><span class="line"><span class="string">      --health-cmd &quot;redis-cli ping&quot;</span></span><br><span class="line"><span class="string">      --health-interval 10s</span></span><br><span class="line"><span class="string">      --health-timeout 5s</span></span><br><span class="line"><span class="string">      --health-retries 5</span></span><br></pre></td></tr></table></figure><p>服务容器主要用于 Linux Runner。真实项目应固定合适的镜像版本，避免 <code>latest</code> 引入意外变化。</p><hr><h2 id="十八、手动、定时和外部触发">十八、手动、定时和外部触发</h2><h3 id="18-1-手动触发-workflow-dispatch">18.1 手动触发 <code>workflow_dispatch</code></h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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">on:</span></span><br><span class="line">  <span class="attr">workflow_dispatch:</span></span><br><span class="line">    <span class="attr">inputs:</span></span><br><span class="line">      <span class="attr">environment:</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">Deployment</span> <span class="string">environment</span></span><br><span class="line">        <span class="attr">required:</span> <span class="literal">true</span></span><br><span class="line">        <span class="attr">type:</span> <span class="string">choice</span></span><br><span class="line">        <span class="attr">options:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">staging</span></span><br><span class="line">          <span class="bullet">-</span> <span class="string">production</span></span><br><span class="line">      <span class="attr">dry_run:</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">Only</span> <span class="string">show</span> <span class="string">planned</span> <span class="string">changes</span></span><br><span class="line">        <span class="attr">required:</span> <span class="literal">true</span></span><br><span class="line">        <span class="attr">default:</span> <span class="literal">true</span></span><br><span class="line">        <span class="attr">type:</span> <span class="string">boolean</span></span><br></pre></td></tr></table></figure><p>使用输入：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    echo &quot;environment=$&#123;&#123; inputs.environment &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;dry_run=$&#123;&#123; inputs.dry_run &#125;&#125;&quot;</span></span><br></pre></td></tr></table></figure><p>常用输入类型：</p><ul class="lvl-0"><li class="lvl-2"><p><code>string</code></p></li><li class="lvl-2"><p><code>boolean</code></p></li><li class="lvl-2"><p><code>choice</code></p></li><li class="lvl-2"><p><code>number</code></p></li><li class="lvl-2"><p><code>environment</code></p></li></ul><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></pre></td><td class="code"><pre><span class="line">gh workflow run deploy.yml \</span><br><span class="line">  -f environment=staging \</span><br><span class="line">  -f dry_run=<span class="literal">true</span></span><br></pre></td></tr></table></figure><h3 id="18-2-定时任务">18.2 定时任务</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">schedule:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">cron:</span> <span class="string">&quot;0 2 * * 1&quot;</span></span><br></pre></td></tr></table></figure><p>表示每周一 UTC 02:00。Cron 字段：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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">┌──────── 分钟 0-59</span><br><span class="line">│ ┌────── 小时 0-23</span><br><span class="line">│ │ ┌──── 日期 1-31</span><br><span class="line">│ │ │ ┌── 月份 1-12</span><br><span class="line">│ │ │ │ ┌ 星期 0-6（0 是周日）</span><br><span class="line">│ │ │ │ │</span><br><span class="line">0 2 * * 1</span><br></pre></td></tr></table></figure><p>注意：</p><ul class="lvl-0"><li class="lvl-2"><p>GitHub Actions Cron 使用 UTC。</p></li><li class="lvl-2"><p>最短间隔通常为 5 分钟。</p></li><li class="lvl-2"><p>高峰期可能延迟，不适合秒级准时任务。</p></li><li class="lvl-2"><p>Scheduled Workflow 使用默认分支上的 Workflow 文件。</p></li></ul><p>多个时间表可以读取触发它的表达式：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">schedule:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">cron:</span> <span class="string">&quot;0 2 * * 1&quot;</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">cron:</span> <span class="string">&quot;0 2 * * 5&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">steps:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">if:</span> <span class="string">github.event.schedule</span> <span class="string">==</span> <span class="string">&#x27;0 2 * * 5&#x27;</span></span><br><span class="line">    <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;Friday task&quot;</span></span><br></pre></td></tr></table></figure><h3 id="18-3-外部触发-repository-dispatch">18.3 外部触发 <code>repository_dispatch</code></h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">repository_dispatch:</span></span><br><span class="line">    <span class="attr">types:</span> [<span class="string">rebuild</span>]</span><br></pre></td></tr></table></figure><p>调用 API：</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">gh api repos/OWNER/REPO/dispatches \</span><br><span class="line">  -f event_type=rebuild \</span><br><span class="line">  -F <span class="string">&#x27;client_payload[version]=1.2.3&#x27;</span></span><br></pre></td></tr></table></figure><p>读取数据：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$<span class="template-variable">&#123;&#123; github.event.client_payload.version &#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><h3 id="18-4-一个-Workflow-完成后触发">18.4 一个 Workflow 完成后触发</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">on:</span></span><br><span class="line">  <span class="attr">workflow_run:</span></span><br><span class="line">    <span class="attr">workflows:</span> [<span class="string">&quot;CI&quot;</span>]</span><br><span class="line">    <span class="attr">types:</span> [<span class="string">completed</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">report:</span></span><br><span class="line">    <span class="attr">if:</span> <span class="string">github.event.workflow_run.conclusion</span> <span class="string">==</span> <span class="string">&#x27;success&#x27;</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;CI succeeded&quot;</span></span><br></pre></td></tr></table></figure><p><code>workflow_run</code> 可能拥有比原 Workflow 更高的权限。不要让低权限、不可信 Workflow 通过 Artifact、Cache 或输出向高权限 Workflow 注入可执行内容。</p><hr><h2 id="十九、复用-Workflow-和-Composite-Action">十九、复用 Workflow 和 Composite Action</h2><h3 id="19-1-可复用-Workflow">19.1 可复用 Workflow</h3><p>被调用文件 <code>.github/workflows/reusable-test.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><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="attr">name:</span> <span class="string">Reusable</span> <span class="string">test</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">workflow_call:</span></span><br><span class="line">    <span class="attr">inputs:</span></span><br><span class="line">      <span class="attr">node-version:</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">Node.js</span> <span class="string">version</span></span><br><span class="line">        <span class="attr">required:</span> <span class="literal">false</span></span><br><span class="line">        <span class="attr">default:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line">        <span class="attr">type:</span> <span class="string">string</span></span><br><span class="line">    <span class="attr">secrets:</span></span><br><span class="line">      <span class="attr">npm-token:</span></span><br><span class="line">        <span class="attr">required:</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">node-version:</span> <span class="string">$&#123;&#123;</span> <span class="string">inputs.node-version</span> <span class="string">&#125;&#125;</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><p>调用：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">uses:</span> <span class="string">./.github/workflows/reusable-test.yml</span></span><br><span class="line">    <span class="attr">with:</span></span><br><span class="line">      <span class="attr">node-version:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line">    <span class="attr">secrets:</span> <span class="string">inherit</span></span><br></pre></td></tr></table></figure><p>跨仓库调用：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">uses:</span> <span class="string">owner/automation/.github/workflows/node-test.yml@v1</span></span><br><span class="line">    <span class="attr">with:</span></span><br><span class="line">      <span class="attr">node-version:</span> <span class="string">&quot;24&quot;</span></span><br></pre></td></tr></table></figure><p>调用可复用 Workflow 的 <code>uses</code> 位于 Job 级别，不放在 <code>steps</code> 中。</p><h3 id="19-2-可复用-Workflow-输出">19.2 可复用 Workflow 输出</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">on:</span></span><br><span class="line">  <span class="attr">workflow_call:</span></span><br><span class="line">    <span class="attr">outputs:</span></span><br><span class="line">      <span class="attr">version:</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">Package</span> <span class="string">version</span></span><br><span class="line">        <span class="attr">value:</span> <span class="string">$&#123;&#123;</span> <span class="string">jobs.prepare.outputs.version</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">prepare:</span></span><br><span class="line">    <span class="attr">outputs:</span></span><br><span class="line">      <span class="attr">version:</span> <span class="string">$&#123;&#123;</span> <span class="string">steps.package.outputs.version</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">id:</span> <span class="string">package</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;version=$(node -p &quot;</span><span class="string">require(&#x27;./package.json&#x27;).version&quot;)&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_OUTPUT&quot;</span></span><br></pre></td></tr></table></figure><p>调用方：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">prepare:</span></span><br><span class="line">    <span class="attr">uses:</span> <span class="string">./.github/workflows/version.yml</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">print:</span></span><br><span class="line">    <span class="attr">needs:</span> <span class="string">prepare</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$<span class="template-variable">&#123;&#123; needs.prepare.outputs.version &#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><h3 id="19-3-Composite-Action">19.3 Composite Action</h3><p>适合复用一组 Step。创建 <code>.github/actions/setup-project/action.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><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="attr">name:</span> <span class="string">Setup</span> <span class="string">project</span></span><br><span class="line"><span class="attr">description:</span> <span class="string">Install</span> <span class="string">Node.js</span> <span class="string">and</span> <span class="string">project</span> <span class="string">dependencies</span></span><br><span class="line"></span><br><span class="line"><span class="attr">inputs:</span></span><br><span class="line">  <span class="attr">node-version:</span></span><br><span class="line">    <span class="attr">description:</span> <span class="string">Node.js</span> <span class="string">version</span></span><br><span class="line">    <span class="attr">required:</span> <span class="literal">false</span></span><br><span class="line">    <span class="attr">default:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="attr">runs:</span></span><br><span class="line">  <span class="attr">using:</span> <span class="string">composite</span></span><br><span class="line">  <span class="attr">steps:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br><span class="line">      <span class="attr">with:</span></span><br><span class="line">        <span class="attr">node-version:</span> <span class="string">$&#123;&#123;</span> <span class="string">inputs.node-version</span> <span class="string">&#125;&#125;</span></span><br><span class="line">        <span class="attr">cache:</span> <span class="string">npm</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="attr">shell:</span> <span class="string">bash</span></span><br></pre></td></tr></table></figure><p>调用本地 Composite Action 前必须先检出仓库：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">steps:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">./.github/actions/setup-project</span></span><br><span class="line">    <span class="attr">with:</span></span><br><span class="line">      <span class="attr">node-version:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><p>选择建议：</p><ul class="lvl-0"><li class="lvl-2"><p>复用整个 Job 或标准发布流程：Reusable Workflow。</p></li><li class="lvl-2"><p>在多个 Job 中复用若干 Step：Composite Action。</p></li><li class="lvl-2"><p>只复用一行命令：Shell/npm script 通常更简单。</p></li></ul><hr><h2 id="二十、Environment、部署审批和-OIDC">二十、Environment、部署审批和 OIDC</h2><h3 id="20-1-Environment">20.1 Environment</h3><p>仓库可配置 <code>staging</code>、<code>production</code> 等 Environment：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">deploy:</span></span><br><span class="line">    <span class="attr">environment:</span></span><br><span class="line">      <span class="attr">name:</span> <span class="string">production</span></span><br><span class="line">      <span class="attr">url:</span> <span class="string">https://example.com</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">./deploy.sh</span></span><br></pre></td></tr></table></figure><p>Environment 可配置：</p><ul class="lvl-0"><li class="lvl-2"><p>审批人。</p></li><li class="lvl-2"><p>等待时间。</p></li><li class="lvl-2"><p>允许部署的分支或标签。</p></li><li class="lvl-2"><p>Environment 专属 Secret 和变量。</p></li></ul><p>生产部署示例：</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></pre></td><td class="code"><pre><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">workflow_dispatch:</span></span><br><span class="line">    <span class="attr">inputs:</span></span><br><span class="line">      <span class="attr">environment:</span></span><br><span class="line">        <span class="attr">type:</span> <span class="string">environment</span></span><br><span class="line">        <span class="attr">required:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">deploy:</span></span><br><span class="line">    <span class="attr">environment:</span> <span class="string">$&#123;&#123;</span> <span class="string">inputs.environment</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">./deploy.sh</span></span><br></pre></td></tr></table></figure><h3 id="20-2-OIDC-是什么">20.2 OIDC 是什么</h3><p>传统发布需要把长期 Token 保存为 Secret。OIDC 的流程是：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><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">Workflow</span><br><span class="line">  → GitHub 签发短期身份令牌</span><br><span class="line">  → npm / 云平台验证仓库、工作流、分支或 Environment</span><br><span class="line">  → 平台签发短期权限或直接允许发布</span><br></pre></td></tr></table></figure><p>优势：</p><ul class="lvl-0"><li class="lvl-2"><p>不保存长期发布凭证。</p></li><li class="lvl-2"><p>凭证只在本次 Job 中短期有效。</p></li><li class="lvl-2"><p>平台可以限制仓库、Workflow、分支和 Environment。</p></li><li class="lvl-2"><p>降低 Token 泄漏及轮换成本。</p></li></ul><h3 id="20-3-OIDC-基础权限">20.3 OIDC 基础权限</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">  <span class="attr">id-token:</span> <span class="string">write</span></span><br></pre></td></tr></table></figure><p><code>id-token: write</code> 只允许请求 OIDC Token，并不会自动赋予修改仓库或云资源的权限。</p><h3 id="20-4-npm-Trusted-Publishing">20.4 npm Trusted Publishing</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">name:</span> <span class="string">Publish</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">tags:</span> [<span class="string">&quot;v*&quot;</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">  <span class="attr">id-token:</span> <span class="string">write</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">publish:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">node-version:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line">          <span class="attr">registry-url:</span> <span class="string">https://registry.npmjs.org</span></span><br><span class="line">          <span class="attr">package-manager-cache:</span> <span class="literal">false</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">publish</span> <span class="string">--access</span> <span class="string">public</span></span><br></pre></td></tr></table></figure><p>还必须在 <a href="http://npmjs.com">npmjs.com</a> 包设置中配置 Trusted Publisher，确保以下信息一致：</p><ul class="lvl-0"><li class="lvl-2"><p>GitHub owner / organization。</p></li><li class="lvl-2"><p>Repository。</p></li><li class="lvl-2"><p>Workflow 文件名。</p></li><li class="lvl-2"><p>可选的 Environment。</p></li><li class="lvl-2"><p>Allowed action 是 <code>npm publish</code>。</p></li></ul><p>npm Trusted Publishing 当前要求 Node.js <code>&gt;= 22.14.0</code>、npm <code>&gt;= 11.5.1</code>，并使用受支持的云托管 Runner。本项目的 Node.js 24 满足要求，工作流还会显式检查 npm 最低版本。OIDC 发布会自动生成 provenance。</p><h3 id="20-5-GitHub-Release-权限">20.5 GitHub Release 权限</h3><p>如果发布后还要创建 GitHub Release：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">write</span></span><br><span class="line">  <span class="attr">id-token:</span> <span class="string">write</span></span><br></pre></td></tr></table></figure><p>可以拆成两个 Job，让 npm 发布 Job 不拥有仓库写权限：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">publish:</span></span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">      <span class="attr">id-token:</span> <span class="string">write</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">publish</span></span><br><span class="line"></span><br><span class="line">  <span class="attr">release:</span></span><br><span class="line">    <span class="attr">needs:</span> <span class="string">publish</span></span><br><span class="line">    <span class="attr">permissions:</span></span><br><span class="line">      <span class="attr">contents:</span> <span class="string">write</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">gh</span> <span class="string">release</span> <span class="string">create</span> <span class="string">&quot;$<span class="template-variable">&#123;&#123; github.ref_name &#125;&#125;</span>&quot;</span> <span class="string">--generate-notes</span></span><br><span class="line">        <span class="attr">env:</span></span><br><span class="line">          <span class="attr">GH_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><hr><h2 id="二十一、安全最佳实践">二十一、安全最佳实践</h2><h3 id="21-1-最小权限">21.1 最小权限</h3><figure class="highlight yaml"><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="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br></pre></td></tr></table></figure><p>只在需要的 Job 中增加 <code>write</code>。</p><h3 id="21-2-固定第三方-Action">21.2 固定第三方 Action</h3><p>最严格方式：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">uses:</span> <span class="string">third-party/action@完整40位commitSHA</span> <span class="comment"># v2.3.1</span></span><br></pre></td></tr></table></figure><p>GitHub 官方也建议固定 Action 到完整 SHA。Dependabot 可帮助更新 Action 版本。</p><p><code>.github/dependabot.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></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="number">2</span></span><br><span class="line"><span class="attr">updates:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">package-ecosystem:</span> <span class="string">github-actions</span></span><br><span class="line">    <span class="attr">directory:</span> <span class="string">/</span></span><br><span class="line">    <span class="attr">schedule:</span></span><br><span class="line">      <span class="attr">interval:</span> <span class="string">weekly</span></span><br></pre></td></tr></table></figure><h3 id="21-3-不要把不可信输入直接拼入-Shell">21.3 不要把不可信输入直接拼入 Shell</h3><p>危险：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$<span class="template-variable">&#123;&#123; github.event.pull_request.title &#125;&#125;</span>&quot;</span></span><br></pre></td></tr></table></figure><p>攻击者可能构造包含 Shell 语法的 PR 标题。</p><p>更安全：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">env:</span></span><br><span class="line">    <span class="attr">PR_TITLE:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.event.pull_request.title</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">printf</span> <span class="string">&#x27;%s\n&#x27;</span> <span class="string">&quot;$PR_TITLE&quot;</span></span><br></pre></td></tr></table></figure><p>表达式由 GitHub 在脚本执行前替换；通过环境变量传递可以避免内容被解释成脚本源码。</p><h3 id="21-4-谨慎使用-pull-request-target">21.4 谨慎使用 <code>pull_request_target</code></h3><p>可以安全用于：</p><ul class="lvl-0"><li class="lvl-2"><p>添加标签。</p></li><li class="lvl-2"><p>评论 PR。</p></li><li class="lvl-2"><p>检查 PR 元数据。</p></li></ul><p>不要：</p><ul class="lvl-0"><li class="lvl-2"><p>检出 Fork PR 的 HEAD 后运行其脚本。</p></li><li class="lvl-2"><p>安装 Fork 修改过的依赖。</p></li><li class="lvl-2"><p>把高权限 Token 交给不可信代码。</p></li></ul><h3 id="21-5-不记录-Secret">21.5 不记录 Secret</h3><p>避免：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">env</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$<span class="template-variable">&#123;&#123; secrets.DEPLOY_TOKEN &#125;&#125;</span>&quot;</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">curl</span> <span class="string">-v</span> <span class="string">-H</span> <span class="string">&quot;Authorization: Bearer $TOKEN&quot;</span> <span class="string">...</span></span><br></pre></td></tr></table></figure><h3 id="21-6-审查依赖安装脚本">21.6 审查依赖安装脚本</h3><p><code>npm ci</code> 可能执行依赖的生命周期脚本。发布任务应：</p><ul class="lvl-0"><li class="lvl-2"><p>使用提交到仓库的 lockfile。</p></li><li class="lvl-2"><p>审查 lockfile 变化。</p></li><li class="lvl-2"><p>使用 Dependabot/Renovate。</p></li><li class="lvl-2"><p>根据 npm 版本采用脚本 allowlist 等安全机制。</p></li><li class="lvl-2"><p>避免发布阶段动态安装未固定的 <code>latest</code> 工具。</p></li></ul><h3 id="21-7-限制超时和并发">21.7 限制超时和并发</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">timeout-minutes:</span> <span class="number">20</span></span><br></pre></td></tr></table></figure><p>既避免无限等待，也减少异常任务消耗。</p><h3 id="21-8-Environment-审批">21.8 Environment 审批</h3><p>生产部署使用受保护 Environment，并要求人工审批。即使 Workflow 被错误触发，也不会立即部署。</p><h3 id="21-9-Fork、Artifact-和-Cache">21.9 Fork、Artifact 和 Cache</h3><p>来自不可信代码的 Artifact 或 Cache 也可能包含恶意内容。高权限 Workflow 不应下载并执行低权限 Workflow 产生的任意二进制或脚本。</p><hr><h2 id="二十二、调试与排错">二十二、调试与排错</h2><h3 id="22-1-查看日志">22.1 查看日志</h3><p>在 GitHub 仓库 Actions 页面中：</p><ol><li class="lvl-3"><p>选择 Workflow。</p></li><li class="lvl-3"><p>选择某次运行。</p></li><li class="lvl-3"><p>选择 Job。</p></li><li class="lvl-3"><p>展开失败 Step。</p></li></ol><p>使用 GitHub CLI：</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">gh run list</span><br><span class="line">gh run view RUN_ID</span><br><span class="line">gh run view RUN_ID --<span class="built_in">log</span></span><br><span class="line">gh run view RUN_ID --log-failed</span><br><span class="line">gh run watch RUN_ID</span><br></pre></td></tr></table></figure><h3 id="22-2-重新运行">22.2 重新运行</h3><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">gh run rerun RUN_ID</span><br><span class="line">gh run rerun RUN_ID --failed</span><br></pre></td></tr></table></figure><p>重新运行时：</p><ul class="lvl-0"><li class="lvl-2"><p><code>github.run_id</code> 不变。</p></li><li class="lvl-2"><p><code>github.run_attempt</code> 增加。</p></li><li class="lvl-2"><p>使用原运行对应的 commit。</p></li></ul><h3 id="22-3-输出调试信息">22.3 输出调试信息</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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 class="attr">name:</span> <span class="string">Debug</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    echo &quot;event=$&#123;&#123; github.event_name &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;ref=$&#123;&#123; github.ref &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;sha=$&#123;&#123; github.sha &#125;&#125;&quot;</span></span><br><span class="line"><span class="string">    echo &quot;runner=$&#123;&#123; runner.os &#125;&#125;/$&#123;&#123; runner.arch &#125;&#125;&quot;</span></span><br></pre></td></tr></table></figure><p>创建 Repository Secret：</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ACTIONS_STEP_DEBUG=true</span><br></pre></td></tr></table></figure><p>可启用更详细的 Step 调试日志。需要时临时开启，排查后删除。</p><h3 id="22-4-Job-Summary">22.4 Job Summary</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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="bullet">-</span> <span class="attr">name:</span> <span class="string">Write</span> <span class="string">summary</span></span><br><span class="line">  <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string">    &#123;</span></span><br><span class="line"><span class="string">      echo &quot;## Test result&quot;</span></span><br><span class="line"><span class="string">      echo &quot;&quot;</span></span><br><span class="line"><span class="string">      echo &quot;- Node.js: $(node --version)&quot;</span></span><br><span class="line"><span class="string">      echo &quot;- Status: passed&quot;</span></span><br><span class="line"><span class="string">    &#125; &gt;&gt; &quot;$GITHUB_STEP_SUMMARY&quot;</span></span><br></pre></td></tr></table></figure><p>Summary 会显示在运行概览页面，比在长日志中查找结果更方便。</p><h3 id="22-5-本地检查-YAML">22.5 本地检查 YAML</h3><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">ruby -e <span class="string">&#x27;require &quot;yaml&quot;; YAML.load_file(&quot;.github/workflows/ci.yml&quot;, aliases: true)&#x27;</span></span><br></pre></td></tr></table></figure><p>更推荐安装 <code>actionlint</code>：</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">brew install actionlint</span><br><span class="line">actionlint</span><br></pre></td></tr></table></figure><p>普通 YAML 解析器只能检查 YAML 是否有效，无法完整理解 GitHub Actions 的 Context、Action 参数和事件语义。</p><h3 id="22-6-为什么-Workflow-没有触发">22.6 为什么 Workflow 没有触发</h3><p>检查：</p><ul class="lvl-0"><li class="lvl-2"><p>文件是否位于 <code>.github/workflows/</code>。</p></li><li class="lvl-2"><p>Workflow 是否已存在于默认分支。</p></li><li class="lvl-2"><p>分支、标签和路径过滤是否匹配。</p></li><li class="lvl-2"><p>YAML 是否有效。</p></li><li class="lvl-2"><p>仓库是否禁用了 Actions。</p></li><li class="lvl-2"><p>Fork Workflow 是否等待维护者批准。</p></li><li class="lvl-2"><p>commit message 是否使用了跳过 CI 的标记。</p></li><li class="lvl-2"><p>定时 Workflow 是否因仓库长期无活动而被禁用。</p></li></ul><h3 id="22-7-为什么-Secret-是空的">22.7 为什么 Secret 是空的</h3><p>常见原因：</p><ul class="lvl-0"><li class="lvl-2"><p>Secret 名称拼写错误。</p></li><li class="lvl-2"><p>Fork PR 不提供 Secret。</p></li><li class="lvl-2"><p>Dependabot 运行受到限制。</p></li><li class="lvl-2"><p>使用的是 Environment Secret，但 Job 没有声明对应 <code>environment</code>。</p></li><li class="lvl-2"><p>Secret 只配置在组织中，但仓库未被授权使用。</p></li></ul><h3 id="22-8-为什么后续-Job-找不到文件">22.8 为什么后续 Job 找不到文件</h3><p>每个 Job 通常使用不同 Runner。使用 Artifact：</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># Job A</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/upload-artifact@v4</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">build</span></span><br><span class="line">    <span class="attr">path:</span> <span class="string">dist/</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Job B</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/download-artifact@v4</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">build</span></span><br></pre></td></tr></table></figure><h3 id="22-9-为什么环境变量在下一步不存在">22.9 为什么环境变量在下一步不存在</h3><p>普通 Shell <code>export</code> 只影响当前 Step：</p><figure class="highlight yaml"><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="bullet">-</span> <span class="attr">run:</span> <span class="string">export</span> <span class="string">VERSION=1.0.0</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;$VERSION&quot;</span> <span class="comment"># 空</span></span><br></pre></td></tr></table></figure><p>应写入：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">run:</span> <span class="string">echo</span> <span class="string">&quot;VERSION=1.0.0&quot;</span> <span class="string">&gt;&gt;</span> <span class="string">&quot;$GITHUB_ENV&quot;</span></span><br></pre></td></tr></table></figure><hr><h2 id="二十三、常用完整模板">二十三、常用完整模板</h2><h3 id="23-1-Node-js-CI">23.1 Node.js CI</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">name:</span> <span class="string">CI</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>]</span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line"></span><br><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line"></span><br><span class="line"><span class="attr">concurrency:</span></span><br><span class="line">  <span class="attr">group:</span> <span class="string">ci-$&#123;&#123;</span> <span class="string">github.workflow</span> <span class="string">&#125;&#125;-$&#123;&#123;</span> <span class="string">github.ref</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">cancel-in-progress:</span> <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">timeout-minutes:</span> <span class="number">15</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">node-version:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line">          <span class="attr">cache:</span> <span class="string">npm</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">run</span> <span class="string">typecheck</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">run</span> <span class="string">build</span></span><br></pre></td></tr></table></figure><h3 id="23-2-多平台测试">23.2 多平台测试</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">name:</span> <span class="string">Cross-platform</span> <span class="string">CI</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span> [<span class="string">push</span>, <span class="string">pull_request</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">$&#123;&#123;</span> <span class="string">matrix.os</span> <span class="string">&#125;&#125;</span> <span class="string">/</span> <span class="string">Node.js</span> <span class="string">$&#123;&#123;</span> <span class="string">matrix.node</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">strategy:</span></span><br><span class="line">      <span class="attr">fail-fast:</span> <span class="literal">false</span></span><br><span class="line">      <span class="attr">matrix:</span></span><br><span class="line">        <span class="attr">os:</span> [<span class="string">ubuntu-latest</span>, <span class="string">windows-latest</span>, <span class="string">macos-latest</span>]</span><br><span class="line">        <span class="attr">node:</span> [<span class="string">&quot;20&quot;</span>, <span class="string">&quot;24&quot;</span>]</span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">$&#123;&#123;</span> <span class="string">matrix.os</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">node-version:</span> <span class="string">$&#123;&#123;</span> <span class="string">matrix.node</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">cache:</span> <span class="string">npm</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><h3 id="23-3-构建并保存-Artifact">23.3 构建并保存 Artifact</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">name:</span> <span class="string">Build</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">branches:</span> [<span class="string">main</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">build:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/setup-node@v6</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">node-version:</span> <span class="string">&quot;24&quot;</span></span><br><span class="line">          <span class="attr">cache:</span> <span class="string">npm</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">run</span> <span class="string">build</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/upload-artifact@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">name:</span> <span class="string">application-$&#123;&#123;</span> <span class="string">github.sha</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">path:</span> <span class="string">dist/</span></span><br><span class="line">          <span class="attr">if-no-files-found:</span> <span class="string">error</span></span><br><span class="line">          <span class="attr">retention-days:</span> <span class="number">14</span></span><br></pre></td></tr></table></figure><h3 id="23-4-手动部署">23.4 手动部署</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">name:</span> <span class="string">Deploy</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">workflow_dispatch:</span></span><br><span class="line">    <span class="attr">inputs:</span></span><br><span class="line">      <span class="attr">environment:</span></span><br><span class="line">        <span class="attr">description:</span> <span class="string">Target</span> <span class="string">environment</span></span><br><span class="line">        <span class="attr">required:</span> <span class="literal">true</span></span><br><span class="line">        <span class="attr">type:</span> <span class="string">environment</span></span><br><span class="line"></span><br><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line"></span><br><span class="line"><span class="attr">concurrency:</span></span><br><span class="line">  <span class="attr">group:</span> <span class="string">deploy-$&#123;&#123;</span> <span class="string">inputs.environment</span> <span class="string">&#125;&#125;</span></span><br><span class="line">  <span class="attr">cancel-in-progress:</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">deploy:</span></span><br><span class="line">    <span class="attr">environment:</span> <span class="string">$&#123;&#123;</span> <span class="string">inputs.environment</span> <span class="string">&#125;&#125;</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">timeout-minutes:</span> <span class="number">30</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">./scripts/deploy.sh</span></span><br><span class="line">        <span class="attr">env:</span></span><br><span class="line">          <span class="attr">DEPLOY_TOKEN:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.DEPLOY_TOKEN</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><h3 id="23-5-Docker-发布到-GHCR">23.5 Docker 发布到 GHCR</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">name:</span> <span class="string">Publish</span> <span class="string">image</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">push:</span></span><br><span class="line">    <span class="attr">tags:</span> [<span class="string">&quot;v*&quot;</span>]</span><br><span class="line"></span><br><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line">  <span class="attr">packages:</span> <span class="string">write</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">publish:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Login</span> <span class="string">to</span> <span class="string">GHCR</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">docker/login-action@固定的完整commitSHA</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">registry:</span> <span class="string">ghcr.io</span></span><br><span class="line">          <span class="attr">username:</span> <span class="string">$&#123;&#123;</span> <span class="string">github.actor</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">password:</span> <span class="string">$&#123;&#123;</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">&#125;&#125;</span></span><br><span class="line"></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">and</span> <span class="string">push</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">docker/build-push-action@固定的完整commitSHA</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">context:</span> <span class="string">.</span></span><br><span class="line">          <span class="attr">push:</span> <span class="literal">true</span></span><br><span class="line">          <span class="attr">tags:</span> <span class="string">ghcr.io/$&#123;&#123;</span> <span class="string">github.repository</span> <span class="string">&#125;&#125;:$&#123;&#123;</span> <span class="string">github.ref_name</span> <span class="string">&#125;&#125;</span></span><br></pre></td></tr></table></figure><p>实际使用时把占位符替换为官方发布页对应版本的完整 SHA。</p><h3 id="23-6-PR-失败时上传日志">23.6 PR 失败时上传日志</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><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="attr">name:</span> <span class="string">Test</span> <span class="string">with</span> <span class="string">diagnostics</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line">  <span class="attr">pull_request:</span></span><br><span class="line"></span><br><span class="line"><span class="attr">permissions:</span></span><br><span class="line">  <span class="attr">contents:</span> <span class="string">read</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span></span><br><span class="line">    <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v6</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">ci</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">logs</span> <span class="string">after</span> <span class="string">failure</span></span><br><span class="line">        <span class="attr">if:</span> <span class="string">failure()</span></span><br><span class="line">        <span class="attr">uses:</span> <span class="string">actions/upload-artifact@v4</span></span><br><span class="line">        <span class="attr">with:</span></span><br><span class="line">          <span class="attr">name:</span> <span class="string">test-logs-$&#123;&#123;</span> <span class="string">github.run_id</span> <span class="string">&#125;&#125;</span></span><br><span class="line">          <span class="attr">path:</span> <span class="string">logs/</span></span><br><span class="line">          <span class="attr">if-no-files-found:</span> <span class="string">ignore</span></span><br></pre></td></tr></table></figure><hr><h2 id="二十四、常见问题">二十四、常见问题</h2><h3 id="Q1：name、Job-ID、Step-name-有什么区别？">Q1：<code>name</code>、Job ID、Step name 有什么区别？</h3><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><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="attr">name:</span> <span class="string">CI</span>             <span class="comment"># Workflow 名称</span></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line">  <span class="attr">test:</span>              <span class="comment"># Job ID</span></span><br><span class="line">    <span class="attr">name:</span> <span class="string">Unit</span> <span class="string">Test</span>  <span class="comment"># Job 显示名称</span></span><br><span class="line">    <span class="attr">steps:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Run</span>    <span class="comment"># Step 显示名称</span></span><br><span class="line">        <span class="attr">run:</span> <span class="string">npm</span> <span class="string">test</span></span><br></pre></td></tr></table></figure><h3 id="Q2：为什么-run-之前通常要-checkout？">Q2：为什么 <code>run</code> 之前通常要 checkout？</h3><p>Runner 默认没有仓库文件。<code>actions/checkout</code> 把对应 commit 检出到 <code>$GITHUB_WORKSPACE</code>。</p><h3 id="Q3：不同-Step-是否共享文件？">Q3：不同 Step 是否共享文件？</h3><p>同一 Job 中共享。不同 Job 通常不共享，需要 Artifact。</p><h3 id="Q4：不同-Step-是否共享-export-的环境变量？">Q4：不同 Step 是否共享 <code>export</code> 的环境变量？</h3><p>不共享。写入 <code>$GITHUB_ENV</code> 才能传给后续 Step。</p><h3 id="Q5：-swig￼303-和-VAR-有什么区别？">Q5：<code>$&#123;&#123; &#125;&#125;</code> 和 <code>$VAR</code> 有什么区别？</h3><ul class="lvl-0"><li class="lvl-2"><p><code>$&#123;&#123; &#125;&#125;</code>：由 GitHub Actions 在 Step 运行前求值。</p></li><li class="lvl-2"><p><code>$VAR</code>：由 Linux/macOS Shell 在运行时读取。</p></li><li class="lvl-2"><p>PowerShell 通常使用 <code>$env:VAR</code>。</p></li></ul><h3 id="Q6：为什么连续-push-后旧-CI-被取消？">Q6：为什么连续 push 后旧 CI 被取消？</h3><p>Workflow 设置了相同 <code>concurrency.group</code> 和 <code>cancel-in-progress: true</code>。</p><h3 id="Q7：为什么发布-Workflow-不取消旧任务？">Q7：为什么发布 Workflow 不取消旧任务？</h3><p>发布包含不可逆的外部操作。中途取消可能导致 npm、GitHub Release 状态不一致。</p><h3 id="Q8：为什么-Job-默认并行？">Q8：为什么 Job 默认并行？</h3><p>没有 <code>needs</code> 依赖的 Job 彼此独立。使用：</p><figure class="highlight yaml"><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="attr">job-b:</span></span><br><span class="line">  <span class="attr">needs:</span> <span class="string">job-a</span></span><br></pre></td></tr></table></figure><p>即可改成串行依赖。</p><h3 id="Q9：Action-的-v6-是-npm-版本吗？">Q9：Action 的 <code>@v6</code> 是 npm 版本吗？</h3><p>不是。它是 Action 仓库的 Git 标签。<code>actions/setup-node@v6</code> 表示 setup-node Action v6，<code>node-version: &quot;24&quot;</code> 才是 Node.js 版本。</p><h3 id="Q10：Workflow-可以修改仓库吗？">Q10：Workflow 可以修改仓库吗？</h3><p>可以，但必须给 <code>GITHUB_TOKEN</code> 对应写权限，例如 <code>contents: write</code>。分支保护规则仍可能限制直接 push。</p><h3 id="Q11：为什么-Fork-PR-读取不到-Secret？">Q11：为什么 Fork PR 读取不到 Secret？</h3><p>这是安全设计，防止外部贡献者修改 Workflow 后输出或上传 Secret。</p><h3 id="Q12：npm-ci-和-npm-install-在-CI-中如何选择？">Q12：<code>npm ci</code> 和 <code>npm install</code> 在 CI 中如何选择？</h3><p>有 lockfile 时优先 <code>npm ci</code>：</p><ul class="lvl-0"><li class="lvl-2"><p>按 lockfile 严格安装。</p></li><li class="lvl-2"><p>不修改 lockfile。</p></li><li class="lvl-2"><p>依赖不一致时直接失败。</p></li><li class="lvl-2"><p>更适合可重复构建。</p></li></ul><h3 id="Q13：可以在本地完整模拟-GitHub-Actions-吗？">Q13：可以在本地完整模拟 GitHub Actions 吗？</h3><p>可以使用第三方工具做部分模拟，但 Runner 镜像、权限、OIDC、Secret、GitHub 事件和托管服务无法保证完全一致。最终仍应以 GitHub Actions 实际运行结果为准。</p><h3 id="Q14：修改-Workflow-后如何安全验证？">Q14：修改 Workflow 后如何安全验证？</h3><ol><li class="lvl-3"><p>运行 YAML/actionlint 检查。</p></li><li class="lvl-3"><p>先在功能分支通过 <code>pull_request</code> 验证低权限 CI。</p></li><li class="lvl-3"><p>为部署流程增加 <code>workflow_dispatch</code> 的 dry-run 输入。</p></li><li class="lvl-3"><p>使用 staging Environment。</p></li><li class="lvl-3"><p>对发布流程严格校验标签和版本。</p></li><li class="lvl-3"><p>不要用真实版本反复测试不可撤销的 npm publish。</p></li></ol><hr><h2 id="二十五、官方参考资料">二十五、官方参考资料</h2><ul class="lvl-0"><li class="lvl-2"><p><a href="https://docs.github.com/zh/actions">GitHub Actions 文档</a></p></li><li class="lvl-2"><p><a href="https://github.com/actions">官方 Action 仓库</a></p></li><li class="lvl-2"><p><a href="https://github.com/marketplace?type=actions">GitHub Marketplace（Actions）</a></p></li><li class="lvl-2"><p><a href="https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions">Workflow 语法</a></p></li><li class="lvl-2"><p><a href="https://docs.github.com/actions/using-workflows/events-that-trigger-workflows">触发 Workflow 的事件</a></p></li><li class="lvl-2"><p><a href="https://docs.github.com/actions/learn-github-actions/expressions">表达式</a></p></li><li class="lvl-2"><p><a href="https://docs.github.com/actions/learn-github-actions/contexts">Context 参考</a></p></li><li class="lvl-2"><p><a href="https://docs.github.com/actions/using-workflows/caching-dependencies-to-speed-up-workflows">依赖缓存</a></p></li><li class="lvl-2"><p><a href="https://docs.github.com/actions/using-workflows/storing-workflow-data-as-artifacts">Artifact</a></p></li><li class="lvl-2"><p><a href="https://docs.github.com/actions/using-workflows/reusing-workflows">复用 Workflow</a></p></li><li class="lvl-2"><p><a href="https://docs.github.com/actions/reference/openid-connect-reference">OIDC 参考</a></p></li><li class="lvl-2"><p><a href="https://docs.github.com/actions/security-guides/automatic-token-authentication">使用 GITHUB_TOKEN</a></p></li><li class="lvl-2"><p><a href="https://docs.npmjs.com/trusted-publishers/">npm Trusted Publishing</a></p></li></ul><p>GitHub Actions 和各个 Action 会持续更新。编写新 Workflow 或升级主要版本时，应重新查看官方文档及对应 Action 的 Release Notes。</p>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/07/11/github-action/</id>
    <link href="https://blog.hanqunfeng.com/2026/07/11/github-action/"/>
    <published>2026-07-11T13:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<p>本文面向第一次接触 GitHub Actions 的开发者，从基本概念、YAML 语法和变量表达式开始，逐步介绍 CI、测试矩阵、缓存、构建产物、发布、OIDC、安全和排错。</p>
<blockquote>
<p>文档中的示例需要放在仓库的 <code>.github/workflows/</code> 目录中，例如 <code>.github/workflows/ci.yml</code>。<br>
GitHub Actions 会执行代码和第三方 Action，复制示例前应根据项目实际情况调整权限、Node.js 版本和命令。</p>
</blockquote>]]>
    </summary>
    <title>GitHub Actions Workflow 详细指南</title>
    <updated>2026-07-13T04:25:15.795Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="git" scheme="https://blog.hanqunfeng.com/tags/git/"/>
    <category term="gitlab" scheme="https://blog.hanqunfeng.com/tags/gitlab/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">本文档介绍 GitLab REST API 的 <strong>v3</strong> 与 <strong>v4</strong> 差异，并结合本仓库迁移工具的实际调用场景说明：如何判断版本、常见接口对比、v4 独有能力，以及排障要点。</li><li class="lvl-2">官方背景：GitLab 自 <strong>9.0</strong> 起引入并推荐 <strong>API v4</strong>；<strong>9.5</strong> 起 v3 停止支持；<strong>11.0</strong> 起 v3 被完全移除。现代实例（11.0+）只能使用 v4。</li></ul><span id="more"></span><h2 id="版本生命周期">版本生命周期</h2><table><thead><tr><th style="text-align:left">GitLab 版本</th><th style="text-align:left">API v3</th><th style="text-align:left">API v4</th></tr></thead><tbody><tr><td style="text-align:left">8.x</td><td style="text-align:left">默认 API</td><td style="text-align:left">不可用</td></tr><tr><td style="text-align:left">9.0</td><td style="text-align:left">仍可用，但不推荐</td><td style="text-align:left">引入并推荐使用</td></tr><tr><td style="text-align:left">9.5（2017-08-22）</td><td style="text-align:left"><strong>停止支持（Unsupported）</strong></td><td style="text-align:left">推荐使用</td></tr><tr><td style="text-align:left">11.0+</td><td style="text-align:left"><strong>已删除（Removed）</strong></td><td style="text-align:left">唯一可用 REST API</td></tr></tbody></table><h2 id="如何判断实例使用-v3-还是-v4">如何判断实例使用 v3 还是 v4</h2><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 探测 v4</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/version&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># 探测 v3（仅旧实例可能成功，gitlab 8.13 以后才加入 /version 这个接口）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/version&quot;</span> | jq .</span><br></pre></td></tr></table></figure><p>能返回 JSON（含 <code>version</code> 字段）的路径，即为该实例应使用的 API 版本。</p><h2 id="共同基础">共同基础</h2><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><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">GITLAB=<span class="string">&quot;https://gitlab.example.com&quot;</span>   <span class="comment"># GitLab 地址，不要带末尾斜杠</span></span><br><span class="line">TOKEN=<span class="string">&quot;your_personal_access_token&quot;</span>    <span class="comment"># PAT 或旧版 Private Token</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 示例路径</span></span><br><span class="line">GROUP=<span class="string">&quot;android&quot;</span></span><br><span class="line">PROJECT=<span class="string">&quot;tool&quot;</span></span><br><span class="line">PROJECT_PATH=<span class="string">&quot;android/tool&quot;</span>              <span class="comment"># group 项目</span></span><br><span class="line">USER_PROJECT_PATH=<span class="string">&quot;hanqunfeng/wifitest&quot;</span>  <span class="comment"># 个人项目</span></span><br><span class="line">USERNAME=<span class="string">&quot;hanqunfeng&quot;</span></span><br><span class="line">USER_ID=<span class="string">&quot;42&quot;</span></span><br><span class="line">NAMESPACE_ID=<span class="string">&quot;123&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># URL 编码（含斜杠的路径必须编码）</span></span><br><span class="line">ENCODED_PATH=$(jq -nr --arg v <span class="string">&quot;<span class="variable">$PROJECT_PATH</span>&quot;</span> <span class="string">&#x27;$v|@uri&#x27;</span>)</span><br><span class="line">ENCODED_USER_PATH=$(jq -nr --arg v <span class="string">&quot;<span class="variable">$USER_PROJECT_PATH</span>&quot;</span> <span class="string">&#x27;$v|@uri&#x27;</span>)</span><br></pre></td></tr></table></figure><h3 id="基础-URL-格式">基础 URL 格式</h3><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">https://&lt;gitlab-host&gt;/api/v3/&lt;endpoint&gt;   # 旧版</span><br><span class="line">https://&lt;gitlab-host&gt;/api/v4/&lt;endpoint&gt;   # 现代</span><br></pre></td></tr></table></figure><h3 id="认证方式（v3-v4-相同）">认证方式（v3 / v4 相同）</h3><p>新版本使用 <code>Personal Access Token（PAT）</code>，老版本使用 <code>Private Token</code>，通过请求头传递：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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"># v4 认证示例</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects?per_page=10&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3 认证示例（仅旧实例）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/projects?per_page=10&quot;</span> | jq .</span><br></pre></td></tr></table></figure><p>创建<code>Personal Access Token（PAT）</code>时分配的常见权限（迁移相关）, <code>Private Token</code> 拥有全部权限：</p><table><thead><tr><th style="text-align:left">操作</th><th style="text-align:left">建议 scope</th></tr></thead><tbody><tr><td style="text-align:left">读取项目/用户/成员</td><td style="text-align:left"><code>api</code> 或 <code>read_api</code></td></tr><tr><td style="text-align:left">创建 Group/Project/用户</td><td style="text-align:left"><code>api</code></td></tr><tr><td style="text-align:left">git clone</td><td style="text-align:left"><code>read_repository</code></td></tr><tr><td style="text-align:left">git push</td><td style="text-align:left"><code>write_repository</code></td></tr></tbody></table><h3 id="路径编码（v3-v4-均需注意）">路径编码（v3 / v4 均需注意）</h3><p>项目路径 <code>group/subgroup/project</code> 中的 <code>/</code> 必须 URL 编码为 <code>%2F</code>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 错误：未编码，可能返回 HTML 重定向</span></span><br><span class="line">/api/v4/projects/group/subgroup/project</span><br><span class="line"></span><br><span class="line"><span class="comment"># 正确</span></span><br><span class="line">/api/v4/projects/group%2Fsubgroup%2Fproject</span><br></pre></td></tr></table></figure><p>bash 中可用 <code>jq</code> 编码：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">ENCODED_PATH=$(jq -nr --arg v <span class="string">&quot;<span class="variable">$PROJECT_PATH</span>&quot;</span> <span class="string">&#x27;$v|@uri&#x27;</span>)</span><br><span class="line"></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>&quot;</span> | jq .</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"># v4 示例：第 1 页，每页 100 条</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects?per_page=100&amp;page=1&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># v3 示例（仅旧实例）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/projects?per_page=100&amp;page=1&quot;</span></span><br></pre></td></tr></table></figure><blockquote><p>v3 部分旧端点可能忽略 <code>page</code> 参数或分页行为不一致；本仓库在拉取 members 时会检测“不足一页即停止”，避免死循环。</p></blockquote><hr><h2 id="迁移相关接口对比">迁移相关接口对比</h2><p>以下按本仓库脚本实际用到的 API 进行对比。</p><h3 id="1）获取版本信息">1）获取版本信息</h3><table><thead><tr><th>项目</th><th style="text-align:left">v3</th><th style="text-align:left">v4</th></tr></thead><tbody><tr><td>端点</td><td style="text-align:left"><code>GET /api/v3/version</code></td><td style="text-align:left"><code>GET /api/v4/version</code></td></tr><tr><td>用途</td><td style="text-align:left">探测 API 是否可用</td><td style="text-align:left">探测 API 是否可用</td></tr></tbody></table><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"># v4</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/version&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3（仅旧实例）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/version&quot;</span> | jq .</span><br></pre></td></tr></table></figure><h3 id="2）项目列表">2）项目列表</h3><table><thead><tr><th>项目</th><th style="text-align:left">v3</th><th style="text-align:left">v4</th></tr></thead><tbody><tr><td>端点</td><td style="text-align:left"><code>GET /api/v3/projects</code></td><td style="text-align:left"><code>GET /api/v4/projects</code></td></tr><tr><td>分页</td><td style="text-align:left"><code>per_page</code> + <code>page</code></td><td style="text-align:left"><code>per_page</code> + <code>page</code></td></tr><tr><td>简化字段</td><td style="text-align:left"><code>simple=true</code></td><td style="text-align:left"><code>simple=true</code></td></tr></tbody></table><blockquote><p><strong>v3 兼容提示（迁移场景常见）</strong>：部分旧版 GitLab 在 <code>GET /api/v3/projects</code> 下可能只返回“对当前 token 用户可见/有关联”的项目集合，即便该用户在 Web UI 中是管理员，也可能出现“拿不到未授权项目”的情况。<br>这时可改用 <code>GET /api/v3/projects/all</code> 拉取更完整的实例项目列表（脚本迁移步骤 1 即属于此类场景）。</p><ul class="lvl-1"><li class="lvl-2"><strong>不带 <code>all</code></strong>：更偏向“当前用户可见/相关”的项目集合（不同旧版本行为可能不完全一致）。</li><li class="lvl-2"><strong>带 <code>all</code>（<code>/projects/all</code>）</strong>：更偏向“实例范围的项目集合”（更适合迁移时生成全量 <code>repos.txt</code>）。</li></ul></blockquote><p><strong>namespace 类型识别差异（重要）</strong>：</p><table><thead><tr><th style="text-align:left">字段</th><th style="text-align:left">v3</th><th style="text-align:left">v4</th></tr></thead><tbody><tr><td style="text-align:left"><code>namespace.kind</code></td><td style="text-align:left">通常<strong>不存在</strong></td><td style="text-align:left">存在：<code>group</code> / <code>user</code></td></tr><tr><td style="text-align:left">推断个人项目</td><td style="text-align:left">看 <code>namespace.owner_id</code> 是否有值</td><td style="text-align:left">优先用 <code>namespace.kind == &quot;user&quot;</code></td></tr></tbody></table><p>本仓库步骤 1 的逻辑：</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 伪代码</span></span><br><span class="line"><span class="keyword">if</span> namespace.kind</span><br><span class="line">  kind = namespace.kind</span><br><span class="line"><span class="keyword">elsif</span> namespace.owner_id == null</span><br><span class="line">  kind = <span class="string">&quot;group&quot;</span></span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">  kind = <span class="string">&quot;user&quot;</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><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"># v4：拉取项目列表（simple 减少字段体积）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects?per_page=100&amp;page=1&amp;simple=true&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3：拉取项目列表（部分旧版本可能会缺少未授权项目）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/projects?per_page=100&amp;page=1&amp;simple=true&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3：拉取“全量”项目列表（迁移场景更推荐，用于避免遗漏项目）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/projects/all?per_page=100&amp;page=1&amp;simple=true&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：只看 namespace.kind / owner_id（便于区分 group/user 项目）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects?per_page=100&amp;page=1&amp;simple=true&quot;</span> \</span><br><span class="line">  | jq <span class="string">&#x27;.[] | &#123;path, namespace: &#123;path: .namespace.path, kind: .namespace.kind, owner_id: .namespace.owner_id&#125;&#125;&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="3）获取单个项目">3）获取单个项目</h3><table><thead><tr><th>项目</th><th style="text-align:left">v3</th><th style="text-align:left">v4</th></tr></thead><tbody><tr><td>端点</td><td style="text-align:left"><code>GET /api/v3/projects/:id_or_encoded_path</code></td><td style="text-align:left"><code>GET /api/v4/projects/:id_or_encoded_path</code></td></tr><tr><td>路径编码</td><td style="text-align:left">需要</td><td style="text-align:left">需要</td></tr></tbody></table><p>v4 返回的 <code>namespace</code> 信息更完整（含 <code>kind</code>），便于区分 Group 项目与个人项目。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><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="comment"># v4：按编码路径获取 group 项目</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：按编码路径获取个人项目</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_USER_PATH&#125;</span>&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：按 numeric id 获取（id 从列表接口中获得）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/123&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3：按编码路径获取项目</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>&quot;</span> | jq .</span><br></pre></td></tr></table></figure><h3 id="4）Group-管理">4）Group 管理</h3><table><thead><tr><th>操作</th><th style="text-align:left">v3</th><th style="text-align:left">v4</th></tr></thead><tbody><tr><td>搜索 Group</td><td style="text-align:left"><code>GET /api/v3/groups?search=&lt;name&gt;</code></td><td style="text-align:left"><code>GET /api/v4/groups?search=&lt;name&gt;</code></td></tr><tr><td>获取 Group</td><td style="text-align:left"><code>GET /api/v3/groups/:id_or_path</code></td><td style="text-align:left"><code>GET /api/v4/groups/:id_or_path</code></td></tr><tr><td>创建 Group</td><td style="text-align:left"><code>POST /api/v3/groups</code></td><td style="text-align:left"><code>POST /api/v4/groups</code></td></tr></tbody></table><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">name=&lt;name&gt;&amp;path=&lt;path&gt;</span><br></pre></td></tr></table></figure><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><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"># v4：搜索 Group</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/groups?search=<span class="variable">$GROUP</span>&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：按 path 获取 Group（并取出 namespace_id）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/groups/<span class="variable">$GROUP</span>&quot;</span> | jq <span class="string">&#x27;&#123;id, name, path, full_path&#125;&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：创建 Group</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;name=<span class="variable">$GROUP</span>&amp;path=<span class="variable">$GROUP</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/groups&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3：搜索 Group</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/groups?search=<span class="variable">$GROUP</span>&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3：创建 Group</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;name=<span class="variable">$GROUP</span>&amp;path=<span class="variable">$GROUP</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/groups&quot;</span> | jq .</span><br></pre></td></tr></table></figure><h3 id="5）Project-管理">5）Project 管理</h3><table><thead><tr><th>操作</th><th style="text-align:left">v3</th><th style="text-align:left">v4</th></tr></thead><tbody><tr><td>检查是否存在</td><td style="text-align:left"><code>GET /api/v3/projects/:encoded_path</code></td><td style="text-align:left"><code>GET /api/v4/projects/:encoded_path</code></td></tr><tr><td>创建 Project</td><td style="text-align:left"><code>POST /api/v3/projects</code></td><td style="text-align:left"><code>POST /api/v4/projects</code></td></tr></tbody></table><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">name=&lt;project&gt;&amp;namespace_id=&lt;namespace_id&gt;</span><br></pre></td></tr></table></figure><blockquote><p>v4 通过 <code>namespace_id</code> 明确指定项目归属（Group 或用户个人命名空间），是本仓库创建个人项目的关键。</p></blockquote><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><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="comment"># v4：检查项目是否存在（200=存在，404=不存在）</span></span><br><span class="line">curl -s -o /dev/null -w <span class="string">&quot;%&#123;http_code&#125;\n&quot;</span> \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：在 Group 下创建 Project</span></span><br><span class="line"><span class="comment"># 1) 获取 Group 的 namespace_id</span></span><br><span class="line">NAMESPACE_ID=$(curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/groups/<span class="variable">$GROUP</span>&quot;</span> | jq -r <span class="string">&#x27;.id&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 2) 创建项目</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;name=<span class="variable">$PROJECT</span>&amp;namespace_id=<span class="variable">$NAMESPACE_ID</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：在用户个人命名空间下创建 Project</span></span><br><span class="line">USER_NAMESPACE_ID=$(curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/namespaces?search=<span class="variable">$USERNAME</span>&quot;</span> \</span><br><span class="line">  | jq -r --arg u <span class="string">&quot;<span class="variable">$USERNAME</span>&quot;</span> <span class="string">&#x27;[.[] | select(.path == $u and .kind == &quot;user&quot;) | .id] | first&#x27;</span>)</span><br><span class="line"></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;name=wifitest&amp;namespace_id=<span class="variable">$USER_NAMESPACE_ID</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3：检查项目是否存在</span></span><br><span class="line">curl -s -o /dev/null -w <span class="string">&quot;%&#123;http_code&#125;\n&quot;</span> \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># v3：创建 Project</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;name=<span class="variable">$PROJECT</span>&amp;namespace_id=<span class="variable">$NAMESPACE_ID</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/projects&quot;</span> | jq .</span><br></pre></td></tr></table></figure><h3 id="6）用户管理">6）用户管理</h3><table><thead><tr><th style="text-align:left">操作</th><th style="text-align:left">v3</th><th style="text-align:left">v4</th></tr></thead><tbody><tr><td style="text-align:left">按 username 查找</td><td style="text-align:left"><code>GET /api/v3/users?username=&lt;name&gt;</code></td><td style="text-align:left"><code>GET /api/v4/users?username=&lt;name&gt;</code></td></tr><tr><td style="text-align:left">按 search 查找</td><td style="text-align:left"><code>GET /api/v3/users?search=&lt;q&gt;</code></td><td style="text-align:left"><code>GET /api/v4/users?search=&lt;q&gt;</code></td></tr><tr><td style="text-align:left">获取用户详情</td><td style="text-align:left"><code>GET /api/v3/users/:id</code></td><td style="text-align:left"><code>GET /api/v4/users/:id</code></td></tr><tr><td style="text-align:left">创建用户</td><td style="text-align:left"><code>POST /api/v3/users</code></td><td style="text-align:left"><code>POST /api/v4/users</code></td></tr></tbody></table><p>创建用户时本仓库使用的 v4 参数：</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">email=...&amp;username=...&amp;name=...&amp;reset_password=true&amp;skip_confirmation=true</span><br></pre></td></tr></table></figure><table><thead><tr><th style="text-align:left">参数</th><th style="text-align:left">v3</th><th>v4</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left"><code>skip_confirmation</code></td><td style="text-align:left">可能不支持或行为不同</td><td>支持</td><td style="text-align:left">跳过邮箱确认，便于批量迁移</td></tr><tr><td style="text-align:left"><code>reset_password</code></td><td style="text-align:left">视版本而定</td><td>支持</td><td style="text-align:left">发送密码重置邮件</td></tr></tbody></table><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><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># v4：按 username 查找用户</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/users?username=<span class="variable">$USERNAME</span>&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：按 email 搜索用户</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/users?search=han@example.com&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：获取用户详情</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/users/<span class="variable">$USER_ID</span>&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：创建用户</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;email=han@example.com&amp;username=<span class="variable">$USERNAME</span>&amp;name=Han%20Qunfeng&amp;reset_password=true&amp;skip_confirmation=true&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/users&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3：按 username 查找用户</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/users?username=<span class="variable">$USERNAME</span>&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3：获取用户详情</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/users/<span class="variable">$USER_ID</span>&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3：创建用户（参数因旧版本而异，仅供参考）</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;email=han@example.com&amp;username=<span class="variable">$USERNAME</span>&amp;name=Han%20Qunfeng&amp;password=TempPass123&amp;confirmed_at=<span class="subst">$(date -u +%Y-%m-%dT%H:%M:%SZ)</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/users&quot;</span> | jq .</span><br></pre></td></tr></table></figure><h3 id="7）成员（Members）——差异最大">7）成员（Members）——差异最大</h3><table><thead><tr><th style="text-align:left">端点</th><th>v3</th><th>v4</th><th style="text-align:left">返回范围</th></tr></thead><tbody><tr><td style="text-align:left"><code>.../members</code></td><td>支持</td><td>支持</td><td style="text-align:left"><strong>仅直接成员</strong></td></tr><tr><td style="text-align:left"><code>.../members/all</code></td><td><strong>不存在</strong></td><td><strong>支持</strong></td><td style="text-align:left">直接成员 + 继承成员</td></tr></tbody></table><p>适用资源：</p><ul class="lvl-0"><li class="lvl-2"><p>Group：<code>/groups/:id_or_path/members</code> / <code>members/all</code></p></li><li class="lvl-2"><p>Project：<code>/projects/:id_or_path/members</code> / <code>members/all</code></p></li></ul><p><strong>对本仓库迁移的影响</strong>：</p><table><thead><tr><th style="text-align:left">场景</th><th style="text-align:left">v3 做法</th><th style="text-align:left">v4 做法</th></tr></thead><tbody><tr><td style="text-align:left">Group 继承成员</td><td style="text-align:left">必须单独拉 Group members；项目 members 拿不到继承权限</td><td style="text-align:left">可用 <code>members/all</code> 一次拿到继承成员</td></tr><tr><td style="text-align:left">个人项目 members 为空</td><td style="text-align:left">正常；权限在 <strong>owner</strong> 上，需从 <code>project.owner</code> / <code>namespace.owner_id</code> 收集</td><td style="text-align:left">同样可能为空；也可用 owner 信息补充</td></tr></tbody></table><p>本仓库用户步骤 1 的策略：</p><ul class="lvl-0"><li class="lvl-2"><p>v4：优先尝试 <code>members/all</code>，再回退 <code>members</code></p></li><li class="lvl-2"><p>v3：仅使用 <code>members</code>，并额外收集 project owner</p></li></ul><p>access_level 常用值：<code>10=Guest</code>，<code>20=Reporter</code>，<code>30=Developer</code>，<code>40=Maintainer</code>，<code>50=Owner</code></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># --- Group 成员 ---</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：Group 直接成员</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/groups/<span class="variable">$GROUP</span>/members?per_page=100&amp;page=1&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：Group 全部成员（含继承，v4 独有）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/groups/<span class="variable">$GROUP</span>/members/all?per_page=100&amp;page=1&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3：Group 成员（无 members/all）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/groups/<span class="variable">$GROUP</span>/members?per_page=100&amp;page=1&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># --- Project 成员 ---</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：Project 直接成员</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/members?per_page=100&amp;page=1&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：Project 全部成员（含 Group 继承，v4 独有）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/members/all?per_page=100&amp;page=1&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v3：Project 成员</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/members?per_page=100&amp;page=1&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># --- 添加 / 更新成员 ---</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：向 Group 添加成员</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;user_id=<span class="variable">$USER_ID</span>&amp;access_level=30&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/groups/<span class="variable">$GROUP</span>/members&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：向 Project 添加成员</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;user_id=<span class="variable">$USER_ID</span>&amp;access_level=40&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/members&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：提升已有成员权限</span></span><br><span class="line">curl -s --request PUT \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;access_level=40&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/members/<span class="variable">$USER_ID</span>&quot;</span> | jq .</span><br></pre></td></tr></table></figure><h3 id="8）Namespaces（v4-独有，迁移关键）">8）Namespaces（v4 独有，迁移关键）</h3><table><thead><tr><th>项目</th><th>v3</th><th style="text-align:left">v4</th></tr></thead><tbody><tr><td>端点</td><td><strong>无独立 Namespaces API</strong></td><td style="text-align:left"><code>GET /api/v4/namespaces</code></td></tr><tr><td>搜索</td><td>—</td><td style="text-align:left"><code>?search=&lt;username&gt;</code></td></tr><tr><td>返回 <code>kind</code></td><td>—</td><td style="text-align:left"><code>group</code> / <code>user</code></td></tr></tbody></table><p>本仓库用 v4 <code>/namespaces</code> 解析用户个人 <code>namespace_id</code>，以便在个人命名空间下创建项目：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><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"># v4：搜索用户个人命名空间</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/namespaces?search=<span class="variable">$USERNAME</span>&quot;</span> \</span><br><span class="line">  | jq --arg u <span class="string">&quot;<span class="variable">$USERNAME</span>&quot;</span> <span class="string">&#x27;[.[] | select(.path == $u and .kind == &quot;user&quot;)]&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># v4：回退方案——从用户详情获取 namespace_id</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/users?username=<span class="variable">$USERNAME</span>&quot;</span> \</span><br><span class="line">  | jq <span class="string">&#x27;.[0] | &#123;id, username, namespace_id&#125;&#x27;</span></span><br></pre></td></tr></table></figure><p>回退方案：<code>GET /api/v4/users/:id</code> 读取 <code>namespace_id</code> 字段。</p><hr><h2 id="v4-独有能力概览">v4 独有能力概览</h2><p>除上述迁移相关差异外，v4 相对 v3 还引入/完善了大量能力。以下按类别列举，并给出 <strong>v4 完整 curl 示例</strong>（v3 均不可用或极不完整）。</p><h3 id="REST-API-设计与一致性">REST API 设计与一致性</h3><ul class="lvl-0"><li class="lvl-2"><p>更规范的 REST 资源命名与 HTTP 动词</p></li><li class="lvl-2"><p>更统一的错误响应格式（<code>message</code> / <code>error</code> 字段）</p></li><li class="lvl-2"><p>更完善的分页头（<code>X-Total</code>、<code>X-Total-Pages</code>、<code>X-Page</code> 等，视端点而定）</p></li><li class="lvl-2"><p>支持用 <strong>numeric id</strong> 或 <strong>URL 编码 path</strong> 访问多数资源</p></li></ul><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">curl -s -D - -o /dev/null \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects?per_page=10&amp;page=1&quot;</span> \</span><br><span class="line">  | grep -i <span class="string">&#x27;^X-&#x27;</span></span><br></pre></td></tr></table></figure><h3 id="成员与权限">成员与权限</h3><ul class="lvl-0"><li class="lvl-2"><p><code>members/all</code>：获取继承成员（见上文 <a href="#7%E6%88%90%E5%91%98members%E5%B7%AE%E5%BC%82%E6%9C%80%E5%A4%A7">7）成员</a>）</p></li><li class="lvl-2"><p>更细粒度的 access level 与过期时间 <code>expires_at</code></p></li><li class="lvl-2"><p>群组/项目级别的更完整权限 API</p></li></ul><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">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;user_id=<span class="variable">$USER_ID</span>&amp;access_level=30&amp;expires_at=2026-12-31&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/members&quot;</span> | jq .</span><br></pre></td></tr></table></figure><h3 id="命名空间与组织结构">命名空间与组织结构</h3><ul class="lvl-0"><li class="lvl-2"><p><code>/namespaces</code> API：统一查询 Group 与用户命名空间（见上文 <a href="#8namespacesv4-%E7%8B%AC%E6%9C%89%E8%BF%81%E7%A7%BB%E5%85%B3%E9%94%AE">8）Namespaces</a>）</p></li><li class="lvl-2"><p><code>namespace.kind</code> 字段：明确区分 <code>group</code> / <code>user</code></p></li><li class="lvl-2"><p>更完善的 Subgroup、共享 Group 等管理能力</p></li></ul><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"># 列出可访问的 namespaces</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/namespaces?per_page=100&amp;page=1&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建 Subgroup</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;name=mobile&amp;path=mobile&amp;parent_id=12&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/groups&quot;</span> | jq .</span><br></pre></td></tr></table></figure><h3 id="CI-CD-与-DevOps">CI/CD 与 DevOps</h3><ul class="lvl-0"><li class="lvl-2"><p>Pipeline / Job / Artifact / Runner 等现代 CI API</p></li><li class="lvl-2"><p>环境（Environments）、部署（Deployments）</p></li><li class="lvl-2"><p>容器镜像仓库（Container Registry）API</p></li><li class="lvl-2"><p>Package Registry（Maven/NuGet 等）API</p></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 列出项目 Pipeline</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/pipelines?per_page=20&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># 获取 Pipeline 的 Jobs</span></span><br><span class="line">PIPELINE_ID=100</span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/pipelines/<span class="variable">$&#123;PIPELINE_ID&#125;</span>/jobs&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># 列出项目 Runners</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/runners&quot;</span> | jq .</span><br></pre></td></tr></table></figure><h3 id="安全与合规">安全与合规</h3><ul class="lvl-0"><li class="lvl-2"><p>依赖项扫描（Dependency Scanning）</p></li><li class="lvl-2"><p>容器扫描（Container Scanning）</p></li><li class="lvl-2"><p>SAST / DAST API</p></li><li class="lvl-2"><p>漏洞报告（Vulnerability Findings）</p></li></ul><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"># 列出项目漏洞（需相应功能与权限）</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/vulnerabilities?per_page=20&quot;</span> | jq .</span><br></pre></td></tr></table></figure><h3 id="集成与自动化">集成与自动化</h3><ul class="lvl-0"><li class="lvl-2"><p>Webhook 管理更完善</p></li><li class="lvl-2"><p>Deploy Tokens / Deploy Keys</p></li><li class="lvl-2"><p>Project Access Tokens / Group Access Tokens</p></li><li class="lvl-2"><p>更完整的 Integrations API</p></li></ul><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><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"># 列出项目 Webhooks</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/hooks&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建项目 Webhook</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --header <span class="string">&quot;Content-Type: application/json&quot;</span> \</span><br><span class="line">  --data <span class="string">&#x27;&#123;&quot;url&quot;:&quot;https://example.com/hook&quot;,&quot;push_events&quot;:true,&quot;merge_requests_events&quot;:true&#125;&#x27;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/hooks&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># 列出 Deploy Keys</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/deploy_keys&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建 Project Access Token</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --header <span class="string">&quot;Content-Type: application/json&quot;</span> \</span><br><span class="line">  --data <span class="string">&#x27;&#123;&quot;name&quot;:&quot;ci-token&quot;,&quot;scopes&quot;:[&quot;read_repository&quot;,&quot;read_api&quot;],&quot;expires_at&quot;:&quot;2026-12-31&quot;&#125;&#x27;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/access_tokens&quot;</span> | jq .</span><br></pre></td></tr></table></figure><h3 id="其他现代能力">其他现代能力</h3><ul class="lvl-0"><li class="lvl-2"><p><strong>GraphQL API</strong>（<code>/api/graphql</code>）：v4 时代主推的查询方式，适合复杂聚合查询</p></li><li class="lvl-2"><p>Issue/MR 高级操作（时间跟踪、评审规则、审批规则等）</p></li><li class="lvl-2"><p>Wiki、Snippets、Milestones、Labels 等资源的完整 CRUD</p></li><li class="lvl-2"><p>审计事件（Audit Events，EE）</p></li><li class="lvl-2"><p>Geo 复制相关 API（EE）</p></li></ul><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><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"># GraphQL：查询当前用户</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --header <span class="string">&quot;Content-Type: application/json&quot;</span> \</span><br><span class="line">  --data <span class="string">&#x27;&#123;&quot;query&quot;:&quot;&#123; currentUser &#123; id username name &#125; &#125;&quot;&#125;&#x27;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/graphql&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># 列出项目 Issues</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/issues?per_page=20&amp;state=opened&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># 列出项目 Merge Requests</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/merge_requests?per_page=20&amp;state=opened&quot;</span> | jq .</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建 Milestone</span></span><br><span class="line">curl -s --request POST \</span><br><span class="line">  --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> \</span><br><span class="line">  --data <span class="string">&quot;title=v1.0&amp;description=First%20release&amp;due_date=2026-12-31&quot;</span> \</span><br><span class="line">  <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/projects/<span class="variable">$&#123;ENCODED_PATH&#125;</span>/milestones&quot;</span> | jq .</span><br></pre></td></tr></table></figure><blockquote><p>完整端点列表以官方文档为准：<a href="https://docs.gitlab.com/api/rest/">GitLab REST API</a></p></blockquote><hr><h2 id="常见问题">常见问题</h2><h3 id="API-返回-HTML-重定向（You-are-being-redirected）">API 返回 HTML 重定向（<code>You are being redirected</code>）</h3><p>常见原因：</p><ol><li class="lvl-3"><p><strong>路径未 URL 编码</strong>（<code>group/project</code> 须写成 <code>group%2Fproject</code>）</p></li><li class="lvl-3"><p><strong>API 版本与实例不匹配</strong>（如对 11.0+ 实例访问 v3）</p></li><li class="lvl-3"><p><strong>Token 无效或权限不足</strong>，被重定向到登录页</p></li></ol><h3 id="v3-拉取成员为空">v3 拉取成员为空</h3><ul class="lvl-0"><li class="lvl-2"><p><code>/projects/:id/members</code> 只返回<strong>直接成员</strong>，不含 Group 继承</p></li><li class="lvl-2"><p>个人项目 <code>members</code> 常为 <code>[]</code>，需从 <code>owner</code> / <code>namespace.owner_id</code> 收集</p></li><li class="lvl-2"><p>v3 无 <code>members/all</code>，需单独拉 Group members</p></li></ul><h3 id="创建用户报-username-已被使用">创建用户报 username 已被使用</h3><p>GitLab 的 <strong>username 与 Group 顶级 path 共享命名空间</strong>。若误将个人 namespace 建成了 Group（如 Group <code>hanqunfeng</code>），则无法创建同名用户。需在 Admin 删除误建 Group 后重试。</p><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></pre></td><td class="code"><pre><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v4/version&quot;</span> | jq .   <span class="comment"># 有 JSON → v4</span></span><br><span class="line">curl -s --header <span class="string">&quot;PRIVATE-TOKEN: <span class="variable">$TOKEN</span>&quot;</span> <span class="string">&quot;<span class="variable">$GITLAB</span>/api/v3/version&quot;</span> | jq .   <span class="comment"># 有 JSON → v3（仅旧实例）</span></span><br></pre></td></tr></table></figure><hr><h2 id="参考链接">参考链接</h2><ul class="lvl-0"><li class="lvl-2"><p><a href="https://docs.gitlab.com/api/rest/">GitLab REST API 文档</a></p></li><li class="lvl-2"><p><a href="https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/api/_index.md?ref_type=heads">GitLab API v4 说明文档</a></p></li><li class="lvl-2"><p><a href="https://docs.gitlab.com/api/graphql/">GitLab GraphQL API</a></p></li></ul>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/07/09/gitlab-api/</id>
    <link href="https://blog.hanqunfeng.com/2026/07/09/gitlab-api/"/>
    <published>2026-07-09T13:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">本文档介绍 GitLab REST API 的 <strong>v3</strong> 与 <strong>v4</strong> 差异，并结合本仓库迁移工具的实际调用场景说明：如何判断版本、常见接口对比、v4 独有能力，以及排障要点。</li>
<li class="lvl-2">官方背景：GitLab 自 <strong>9.0</strong> 起引入并推荐 <strong>API v4</strong>；<strong>9.5</strong> 起 v3 停止支持；<strong>11.0</strong> 起 v3 被完全移除。现代实例（11.0+）只能使用 v4。</li>
</ul>]]>
    </summary>
    <title>GitLab API 指南（v3 与 v4 对比）</title>
    <updated>2026-07-10T02:23:47.307Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="git" scheme="https://blog.hanqunfeng.com/tags/git/"/>
    <category term="gitlab" scheme="https://blog.hanqunfeng.com/tags/gitlab/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">当 GitLab 实例版本过旧（如 7.x / 8.x / 9.x），官方要求按版本逐级升级，路径长、风险高。本文介绍我开源的 Shell 工具 <code>gitlab-migrate</code>——通过「新装 GitLab + 数据迁移」的方式，自动化完成仓库 mirror 迁移、Group/个人项目区分处理、用户权限同步，支持断点续传与并行推送。</li></ul><span id="more"></span><h2 id="一、背景：为什么需要这个工具？">一、背景：为什么需要这个工具？</h2><p>公司或团队里跑着一个 <strong>GitLab 7.x / 8.x / 9.x</strong> 的老实例，操作系统老旧、PostgreSQL 版本跟不上，想升到 <strong>GitLab 19.x</strong> 却发现：</p><ol><li class="lvl-3"><p><strong>官方不支持跨大版本直接升级</strong>，必须按 <a href="https://docs.gitlab.com/update/">Upgrade GitLab</a> 路径逐级 <code>gitlab-ctl upgrade</code>；</p></li><li class="lvl-3"><p>每一步都伴随数据库迁移、服务重启，一旦失败回滚成本高；</p></li><li class="lvl-3"><p>旧版备份往往也无法直接导入全新实例；</p></li><li class="lvl-3"><p>更麻烦的是，<strong>GitLab 11.0 起 API v3 被完全移除</strong>，很多自动化脚本对接老实例时会踩坑。</p></li></ol><p>如果你的核心诉求是 <strong>「把代码仓库迁到一台新机器上的现代 GitLab」</strong>，而不是完整保留 Issue、Merge Request、Wiki、CI 流水线历史等协作元数据，那么 <strong>「新装 + 迁移」</strong> 通常比原地逐级升级更简单、更可控。</p><p>基于这个场景，我编写并开源了 <strong><code>gitlab-migrate</code></strong>：</p><p><strong>项目地址</strong>：<a href="https://github.com/hanqunfeng/gitlab-migrate">https://github.com/hanqunfeng/gitlab-migrate</a></p><hr><h2 id="二、方案对比：原地升级-vs-新装迁移">二、方案对比：原地升级 vs 新装迁移</h2><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">适用场景</td><td style="text-align:left">相近版本（如 18.x → 19.x）</td><td style="text-align:left">跨多个大版本（如 9.x → 19.x）</td></tr><tr><td style="text-align:left">对旧服务器要求</td><td style="text-align:left">高（OS、PG 等须满足中间版本）</td><td style="text-align:left">低（旧实例只读，新实例独立部署）</td></tr><tr><td style="text-align:left">迁移内容</td><td style="text-align:left">全量（含 Issue、MR 等）</td><td style="text-align:left">Git 仓库 + 用户/成员权限</td></tr><tr><td style="text-align:left">切换窗口</td><td style="text-align:left">升级期间服务可能中断</td><td style="text-align:left">旧实例可继续运行，分批迁移后统一切流量</td></tr><tr><td style="text-align:left">失败恢复</td><td style="text-align:left">回滚复杂</td><td style="text-align:left">分步执行，已完成项自动跳过</td></tr></tbody></table><blockquote><p><strong>说明</strong>：若你只是 18.x → 19.x 这类相近版本升级，请直接用官方文档，无需本工具。</p></blockquote><hr><h2 id="三、工具能做什么？">三、工具能做什么？</h2><h3 id="3-1-仓库迁移（6-步）">3.1 仓库迁移（6 步）</h3><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          步骤 2              步骤 3              步骤 4          步骤 5          步骤 6</span><br><span class="line">拉取项目列表 → 创建 Group      → 创建 Project     → Mirror Clone → Push 到新实例 → 输出汇总</span><br><span class="line">   API         (仅 group)          (group/user)        git            git</span><br></pre></td></tr></table></figure><table><thead><tr><th>步骤</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td>1</td><td style="text-align:left">分页调用 API，生成 <code>repos.txt</code>（含 namespace 类型）</td></tr><tr><td>2</td><td style="text-align:left">仅为 Group 命名空间创建 Group；个人 namespace 自动跳过</td></tr><tr><td>3</td><td style="text-align:left">Group 项目建在 Group 下；个人项目建在用户命名空间下</td></tr><tr><td>4</td><td style="text-align:left">从旧 GitLab <code>git clone --mirror</code> 到本地</td></tr><tr><td>5</td><td style="text-align:left">并行 <code>git push --mirror</code> 到新 GitLab</td></tr><tr><td>6</td><td style="text-align:left">统计成功/失败数量</td></tr></tbody></table><h3 id="3-2-用户迁移（3-步，独立入口）">3.2 用户迁移（3 步，独立入口）</h3><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           用户步骤 2              用户步骤 3</span><br><span class="line">收集项目访问用户  →  在新实例创建本地用户  →  同步 Group/Project 成员</span><br></pre></td></tr></table></figure><p>用户迁移与仓库迁移 <strong>解耦</strong>，可按需执行，不影响仓库步骤编号。</p><h3 id="3-3-迁移范围">3.3 迁移范围</h3><p><strong>包含</strong>：</p><ul class="lvl-0"><li class="lvl-2"><p>Git 仓库数据（分支、标签、完整提交历史）</p></li><li class="lvl-2"><p>用户账号与 Group / Project 成员权限</p></li></ul><p><strong>不包含</strong>：</p><ul class="lvl-0"><li class="lvl-2"><p>Issue、Merge Request、Wiki</p></li><li class="lvl-2"><p>CI/CD 流水线历史、Package Registry 等元数据</p></li></ul><hr><h2 id="四、技术亮点">四、技术亮点</h2><h3 id="4-1-旧版-API-v3-适配">4.1 旧版 API v3 适配</h3><p>GitLab API 版本演进：</p><table><thead><tr><th>版本</th><th style="text-align:left">API v3 状态</th></tr></thead><tbody><tr><td>8.x</td><td style="text-align:left">v3 为默认 API</td></tr><tr><td>9.0</td><td style="text-align:left">引入 v4，v3 仍可用</td></tr><tr><td>9.5</td><td style="text-align:left">v3 停止支持</td></tr><tr><td><strong>11.0</strong></td><td style="text-align:left"><strong>v3 完全移除</strong></td></tr></tbody></table><p>本工具内置 <strong>v3 → v4</strong> 双 API 适配，支持以下两种迁移组合：</p><table><thead><tr><th style="text-align:left">场景</th><th><code>OLD_GITLAB_API_VERSION</code></th><th><code>NEW_GITLAB_API_VERSION</code></th></tr></thead><tbody><tr><td style="text-align:left">旧版 → 新版（常见）</td><td><code>v3</code></td><td><code>v4</code></td></tr><tr><td style="text-align:left">现代实例互迁</td><td><code>v4</code></td><td><code>v4</code></td></tr></tbody></table><h3 id="4-2-Group-与个人项目自动区分">4.2 Group 与个人项目自动区分</h3><p>GitLab 中 <strong>username 与 Group 顶级 path 不能重名</strong>。个人项目（如 <code>hanqunfeng/wifitest</code>）的 namespace 是用户，不应为其创建 Group。</p><p>工具在步骤 1 拉取项目列表时，会识别 <code>namespace_kind</code>：</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">group|android|tool|https://gitlab.example.com/android/tool.git</span><br><span class="line">user|hanqunfeng|wifitest|https://gitlab.example.com/hanqunfeng/wifitest.git</span><br></pre></td></tr></table></figure><p>步骤 2 只为 <code>group</code> 类型创建 Group；<code>user</code> 类型自动跳过，在步骤 3 挂到对应用户命名空间下。</p><h3 id="4-3-断点续传">4.3 断点续传</h3><p>各步骤支持重复执行，已完成的资源自动跳过：</p><table><thead><tr><th>步骤</th><th style="text-align:left">跳过条件</th></tr></thead><tbody><tr><td>2</td><td style="text-align:left">Group 已存在</td></tr><tr><td>3</td><td style="text-align:left">Project 已存在</td></tr><tr><td>4</td><td style="text-align:left">本地 mirror 目录已存在</td></tr><tr><td>用户 2</td><td style="text-align:left">用户已存在（按 username / email 匹配）</td></tr></tbody></table><p>中途失败可单独重试，无需从头来过。</p><h3 id="4-4-并行推送">4.4 并行推送</h3><p>步骤 5 支持配置 <code>CONCURRENCY</code>（默认 6），多仓库并行 mirror push，大幅缩短大批量迁移耗时。</p><hr><h2 id="五、快速上手">五、快速上手</h2><h3 id="5-1-环境要求">5.1 环境要求</h3><ul class="lvl-0"><li class="lvl-2"><p>bash 4+</p></li><li class="lvl-2"><p><code>curl</code>、<code>jq</code>、<code>git</code></p></li></ul><h3 id="5-2-安装与配置">5.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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 克隆仓库</span></span><br><span class="line">git <span class="built_in">clone</span> https://github.com/hanqunfeng/gitlab-migrate.git</span><br><span class="line"><span class="built_in">cd</span> gitlab-migrate</span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建本地配置</span></span><br><span class="line"><span class="built_in">cp</span> scripts/config.example.sh scripts/config.sh</span><br></pre></td></tr></table></figure><p>编辑 <code>scripts/config.sh</code>，填入源/目标 GitLab 地址与 Token：</p><table><thead><tr><th>变量</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td><code>OLD_GITLAB</code></td><td style="text-align:left">源 GitLab 地址</td></tr><tr><td><code>NEW_GITLAB</code></td><td style="text-align:left">目标 GitLab 地址</td></tr><tr><td><code>OLD_TOKEN</code></td><td style="text-align:left">源实例 PAT（<code>read_repository</code> 或 <code>api</code>）</td></tr><tr><td><code>NEW_TOKEN</code></td><td style="text-align:left">目标实例 PAT（<code>api</code> + <code>write_repository</code>）</td></tr><tr><td><code>CONCURRENCY</code></td><td style="text-align:left">并行 push 数量，默认 <code>6</code></td></tr></tbody></table><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"># 添加执行权限</span></span><br><span class="line"><span class="built_in">chmod</span> +x gitlab-migrate.sh gitlab-migrate-users.sh scripts/*.sh</span><br></pre></td></tr></table></figure><h3 id="5-3-仅迁移-Group-项目">5.3 仅迁移 Group 项目</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></pre></td><td class="code"><pre><span class="line">./gitlab-migrate.sh 1    <span class="comment"># 拉取项目列表</span></span><br><span class="line">./gitlab-migrate.sh 2    <span class="comment"># 创建 Group</span></span><br><span class="line">./gitlab-migrate.sh 3    <span class="comment"># 创建 Project</span></span><br><span class="line">./gitlab-migrate.sh 4    <span class="comment"># Mirror clone</span></span><br><span class="line">./gitlab-migrate.sh 5    <span class="comment"># Push</span></span><br><span class="line">./gitlab-migrate.sh 6    <span class="comment"># 查看汇总</span></span><br></pre></td></tr></table></figure><h3 id="5-4-含个人项目的完整流程">5.4 含个人项目的完整流程</h3><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><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">./gitlab-migrate.sh 1              <span class="comment"># 拉列表</span></span><br><span class="line">./gitlab-migrate.sh 2              <span class="comment"># 建 Group（跳过个人 namespace）</span></span><br><span class="line"></span><br><span class="line">./gitlab-migrate-users.sh 1        <span class="comment"># 收集用户</span></span><br><span class="line">./gitlab-migrate-users.sh 2        <span class="comment"># 创建用户（须在步骤 3 之前）</span></span><br><span class="line"></span><br><span class="line">./gitlab-migrate.sh 3              <span class="comment"># 建 Project</span></span><br><span class="line">./gitlab-migrate.sh 4 5 6          <span class="comment"># clone / push / 汇总</span></span><br><span class="line"></span><br><span class="line">./gitlab-migrate-users.sh 3        <span class="comment"># 同步成员关系</span></span><br></pre></td></tr></table></figure><hr><h2 id="六、配套文档">六、配套文档</h2><p>仓库内还附带 GitLab 新实例部署相关文档，方便「新装 + 迁移」一条龙：</p><ul class="lvl-0"><li class="lvl-2"><p><a href="https://github.com/hanqunfeng/gitlab-migrate/blob/main/docs/gitlab-install-rpm.md">GitLab 安装指南（RPM 系）</a></p></li><li class="lvl-2"><p><a href="https://github.com/hanqunfeng/gitlab-migrate/blob/main/docs/gitlab-install-deb.md">GitLab 安装指南（DEB 系）</a></p></li><li class="lvl-2"><p><a href="https://github.com/hanqunfeng/gitlab-migrate/blob/main/docs/gitlab-https-cert.md">HTTPS 证书配置</a></p></li><li class="lvl-2"><p><a href="https://github.com/hanqunfeng/gitlab-migrate/blob/main/docs/gitlab-email-config.md">邮件功能配置</a></p></li></ul><hr><h2 id="七、总结">七、总结</h2><p><code>gitlab-migrate</code> 面向 <strong>GitLab 跨多个大版本迁移</strong> 这一特定场景，用 Shell 脚本把「拉列表 → 建资源 → mirror 迁移 → 同步权限」串成可重复执行、可断点续跑的流水线。相比手工逐个 <code>git clone --mirror</code> + <code>git push --mirror</code>，以及自己处理 v3 API、Group/个人项目差异，这套工具能显著降低出错率和人力成本。</p><p>如果你正面临旧 GitLab 无法升级、又需要把代码迁到现代实例的困境，欢迎试用。</p><ul class="lvl-0"><li class="lvl-2"><p><strong>GitHub</strong>：<a href="https://github.com/hanqunfeng/gitlab-migrate">https://github.com/hanqunfeng/gitlab-migrate</a></p></li><li class="lvl-2"><p><strong>协议</strong>：MIT</p></li><li class="lvl-2"><p>欢迎 Star、提 Issue、PR</p></li></ul>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/07/03/gitlab-migrate/</id>
    <link href="https://blog.hanqunfeng.com/2026/07/03/gitlab-migrate/"/>
    <published>2026-07-03T13:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">当 GitLab 实例版本过旧（如 7.x / 8.x / 9.x），官方要求按版本逐级升级，路径长、风险高。本文介绍我开源的 Shell 工具 <code>gitlab-migrate</code>——通过「新装 GitLab + 数据迁移」的方式，自动化完成仓库 mirror 迁移、Group/个人项目区分处理、用户权限同步，支持断点续传与并行推送。</li>
</ul>]]>
    </summary>
    <title>【开源工具】GitLab 跨大版本迁移实战：告别逐级升级，一键迁移仓库与用户权限</title>
    <updated>2026-07-03T07:32:39.779Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="git" scheme="https://blog.hanqunfeng.com/tags/git/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">Git常用命令(快查手册)</li></ul><span id="more"></span><h2 id="一、仓库初始化与克隆">一、仓库初始化与克隆</h2><table><thead><tr><th>命令</th><th>功能</th><th>示例</th></tr></thead><tbody><tr><td><code>git init</code></td><td>初始化本地仓库</td><td><code>git init</code></td></tr><tr><td><code>git clone &lt;url&gt;</code></td><td>克隆远程仓库</td><td><code>git clone git@github.com:user/repo.git</code></td></tr></tbody></table><h2 id="二、配置类（用户信息）">二、配置类（用户信息）</h2><table><thead><tr><th>命令</th><th>功能</th><th>示例</th></tr></thead><tbody><tr><td><code>git config --global user.name</code></td><td>设置用户名</td><td><code>git config --global user.name &quot;Alice&quot;</code></td></tr><tr><td><code>git config --global user.email</code></td><td>设置邮箱</td><td><code>git config --global user.email &quot;a@b.com&quot;</code></td></tr><tr><td><code>git config --list</code></td><td>查看配置</td><td><code>git config --list</code></td></tr></tbody></table><h2 id="三、状态查看">三、状态查看</h2><table><thead><tr><th>命令</th><th>功能</th><th>示例</th></tr></thead><tbody><tr><td><code>git status</code></td><td>查看工作区状态</td><td><code>git status</code></td></tr><tr><td><code>git log</code></td><td>查看当前分支的提交历史</td><td><code>git log</code></td></tr><tr><td><code>git log --oneline</code></td><td>–oneline：把每个 commit 压缩成一行</td><td><code>git log --oneline</code></td></tr><tr><td><code>git log --oneline --decorate --graph --all</code></td><td>–decorate:显示“引用信息” <br> --graph:用 ASCII 图显示分支结构 <br> --all:显示所有分支</td><td><code>git log --oneline --decorate --graph --all</code></td></tr><tr><td><code>git diff</code></td><td>查看改动内容</td><td><code>git diff</code></td></tr><tr><td><code>git show</code></td><td>查看某次提交详情</td><td><code>git show HEAD</code></td></tr></tbody></table><h2 id="四、文件操作（核心）">四、文件操作（核心）</h2><table><thead><tr><th>命令</th><th>功能</th><th>示例</th></tr></thead><tbody><tr><td><code>git add &lt;file&gt;</code></td><td>添加到暂存区</td><td><code>git add .</code></td></tr><tr><td><code>git commit -m &quot;msg&quot;</code></td><td>提交代码</td><td><code>git commit -m &quot;fix: bug&quot;</code></td></tr><tr><td><code>git rm &lt;file&gt;</code></td><td>删除文件并提交删除</td><td><code>git rm a.txt</code></td></tr><tr><td><code>git mv &lt;old&gt; &lt;new&gt;</code></td><td>重命名文件</td><td><code>git mv a.txt b.txt</code></td></tr></tbody></table><h2 id="五、分支管理（非常重要）">五、分支管理（非常重要）</h2><table><thead><tr><th>命令</th><th>功能</th><th>示例</th></tr></thead><tbody><tr><td><code>git branch</code></td><td>查看本地分支</td><td><code>git branch</code></td></tr><tr><td><code>git branch &lt;name&gt;</code></td><td>创建分支</td><td><code>git branch dev</code></td></tr><tr><td><code>git checkout &lt;branch&gt;</code></td><td>切换分支</td><td><code>git checkout dev</code></td></tr><tr><td><code>git checkout -b &lt;name&gt;</code></td><td>创建并切换</td><td><code>git checkout -b feature/login</code></td></tr><tr><td><code>git switch &lt;branch&gt;</code></td><td>切换分支（新）</td><td><code>git switch dev</code></td></tr><tr><td><code>git switch -c &lt;name&gt;</code></td><td>创建并切换（新）</td><td><code>git switch -c feature/login</code></td></tr><tr><td><code>git merge &lt;branch&gt;</code></td><td>合并分支</td><td><code>git merge dev</code></td></tr><tr><td><code>git branch -d &lt;name&gt;</code></td><td>删除本地分支</td><td><code>git branch -d dev</code></td></tr><tr><td><code>git push -u origin &lt;branch&gt;</code></td><td>首次发布本地分支到远程并建立跟踪关系</td><td><code>git push -u origin dev</code></td></tr><tr><td><code>git push origin &lt;branch&gt;</code></td><td>推送已关联的分支</td><td><code>git push origin dev</code></td></tr><tr><td><code>git branch -vv</code></td><td>查看本地分支与远程分支的关联关系</td><td><code>git branch -vv</code></td></tr><tr><td><code>git fetch origin</code></td><td>获取远程最新分支信息</td><td><code>git fetch origin</code></td></tr><tr><td><code>git branch -r</code></td><td>查看远程分支</td><td><code>git branch -r</code></td></tr><tr><td><code>git branch -a</code></td><td>查看所有本地和远程分支</td><td><code>git branch -a</code></td></tr><tr><td><code>git push --all</code></td><td>将本地所有分支推送到远程仓库</td><td>等价于 <code>git push origin --all</code></td></tr><tr><td><code>git push origin --delete &lt;name&gt;</code></td><td>删除远程分支</td><td><code>git push origin --delete dev</code></td></tr></tbody></table><h2 id="六、远程仓库（GitHub-GitLab）">六、远程仓库（GitHub / GitLab）</h2><table><thead><tr><th>命令</th><th>功能</th><th>示例</th></tr></thead><tbody><tr><td><code>git remote -v</code></td><td>查看远程仓库</td><td><code>git remote -v</code></td></tr><tr><td><code>git remote add origin &lt;url&gt;</code></td><td>添加远程仓库</td><td><code>git remote add origin git@github.com:a/b.git</code></td></tr><tr><td><code>git remote set-url origin &lt;url&gt;</code></td><td>修改远程仓库地址(已经添加过)</td><td><code>git remote set-url origin git@github.com:a/b.git</code></td></tr><tr><td><code>git fetch</code></td><td>拉取远程信息（不合并）</td><td><code>git fetch origin</code></td></tr><tr><td><code>git pull</code></td><td>拉取并合并</td><td><code>git pull origin main</code></td></tr><tr><td><code>git push</code></td><td>推送代码</td><td><code>git push origin main</code></td></tr><tr><td><code>git push -u origin main</code></td><td>首次推送并绑定 upstream</td><td><code>git push -u origin main</code></td></tr></tbody></table><h2 id="七、撤销-回退（高频）">七、撤销 / 回退（高频）</h2><table><thead><tr><th>命令</th><th>功能</th><th>示例</th></tr></thead><tbody><tr><td><code>git restore &lt;file&gt;</code></td><td>撤销工作区修改</td><td><code>git restore a.txt</code></td></tr><tr><td><code>git restore --staged &lt;file&gt;</code></td><td>取消暂存</td><td><code>git restore --staged a.txt</code></td></tr><tr><td><code>git reset --soft HEAD~1</code></td><td>回退提交（保留代码）</td><td><code>git reset --soft HEAD~1</code></td></tr><tr><td><code>git reset --hard HEAD~1</code></td><td>强制回退（丢弃修改）</td><td><code>git reset --hard HEAD~1</code></td></tr><tr><td><code>git revert &lt;commit&gt;</code></td><td>生成反向提交</td><td><code>git revert abc123</code></td></tr></tbody></table><h2 id="八、标签（版本发布）">八、标签（版本发布）</h2><table><thead><tr><th>命令</th><th>功能</th><th>示例</th></tr></thead><tbody><tr><td><code>git tag</code></td><td>查看标签</td><td><code>git tag</code></td></tr><tr><td><code>git tag &lt;name&gt;</code></td><td>创建标签</td><td><code>git tag v1.0.0</code></td></tr><tr><td><code>git tag -a &lt;name&gt; -m &quot;&quot;</code></td><td>创建带说明标签</td><td><code>git tag -a v1.0.0 -m &quot;release&quot;</code></td></tr><tr><td><code>git push origin &lt;tag&gt;</code></td><td>推送标签</td><td><code>git push origin v1.0.0</code></td></tr><tr><td><code>git push --tags</code></td><td>推送所有标签</td><td>等价于 <code>git push origin --tags</code></td></tr></tbody></table><h2 id="九、stash（临时保存现场）">九、stash（临时保存现场）</h2><table><thead><tr><th>命令</th><th>功能</th><th>示例</th></tr></thead><tbody><tr><td><code>git stash</code></td><td>临时保存修改</td><td><code>git stash</code></td></tr><tr><td><code>git stash list</code></td><td>查看 stash 列表</td><td><code>git stash list</code></td></tr><tr><td><code>git stash pop</code></td><td>恢复最近 stash</td><td><code>git stash pop</code></td></tr><tr><td><code>git stash apply</code></td><td>应用 stash（不删除）</td><td><code>git stash apply</code></td></tr></tbody></table>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/06/25/git-command2/</id>
    <link href="https://blog.hanqunfeng.com/2026/06/25/git-command2/"/>
    <published>2026-06-25T13:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">Git常用命令(快查手册)</li>
</ul>]]>
    </summary>
    <title>Git常用命令(快查手册)</title>
    <updated>2026-06-26T03:24:42.797Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="git" scheme="https://blog.hanqunfeng.com/tags/git/"/>
    <category term="svn" scheme="https://blog.hanqunfeng.com/tags/svn/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">本文介绍如何使用<code>git-svn</code>将 SVN 中的项目迁移到 Git</li></ul><span id="more"></span><h2 id="Git-Svn-简介">Git-Svn 简介</h2><ul class="lvl-0"><li class="lvl-2"><p><a href="https://git-scm.com/docs/git-svn/zh_HANS-CN">git-svn</a> 是 Git 官方提供的工具，用于把 SVN 仓库的提交历史逐条转换为 Git commit，并支持后续同步，是 SVN 迁移到 Git 的经典方案。</p></li><li class="lvl-2"><p>用 <code>git-svn</code> 做 SVN → Git 迁移，本质是利用 Git 内置的“SVN 适配器”把 SVN revision 流水线转换成 Git commit。</p></li><li class="lvl-2"><p>它的优势主要体现在<code>完整保留 SVN 线性历史</code>、<code>迁移成本低</code>、<code>可控性强</code>这三个维度。</p></li></ul><h2 id="Git-Svn-安装">Git-Svn 安装</h2><h3 id="Linux-安装">Linux 安装</h3><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"><span class="built_in">sudo</span> dnf install git-svn -y</span><br></pre></td></tr></table></figure><h3 id="Mac-安装">Mac 安装</h3><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">brew install git-svn</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 查看版本</span></span><br><span class="line">git svn --version</span><br><span class="line"><span class="comment"># 查看全部命令</span></span><br><span class="line">git svn <span class="built_in">help</span></span><br><span class="line"><span class="comment"># 查看某个命令的帮助信息，比如这里查看clone</span></span><br><span class="line">git svn <span class="built_in">help</span> <span class="built_in">clone</span></span><br><span class="line"><span class="comment"># 查看手册</span></span><br><span class="line">git svn --<span class="built_in">help</span></span><br></pre></td></tr></table></figure><h2 id="实战迁移">实战迁移</h2><h3 id="查看svn最后一次提交时的-revision">查看svn最后一次提交时的 <code>revision</code></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"># 输入密码后，凭证会被保存在 ~/.subversion/auth/ 中</span></span><br><span class="line">svn info https://svn.test.com/mytools/sometool/trunk --username 你的svn用户名</span><br><span class="line"><span class="comment"># 输出</span></span><br><span class="line">Path: trunk</span><br><span class="line">URL: https://svn.test.com/mytools</span><br><span class="line">Relative URL: ^/callblocker/trunk_as</span><br><span class="line">Repository Root: https://svn.test.com/mytools</span><br><span class="line">Repository UUID: b1d85008-4086-4607-87e8-e67b222846c5</span><br><span class="line">Revision: 7840</span><br><span class="line">Node Kind: directory</span><br><span class="line">Last Changed Author: hanqunfeng</span><br><span class="line">Last Changed Rev: 7840</span><br><span class="line">Last Changed Date: 2026-05-18 16:24:34 +0800 (Mon, 18 May 2026)</span><br><span class="line"></span><br><span class="line"><span class="comment">## 说明</span></span><br><span class="line">Revision: 7840 表示最新的Revision</span><br></pre></td></tr></table></figure><h3 id="迁移trunk-不包含branch和tags">迁移trunk(不包含branch和tags)</h3><ul class="lvl-0"><li class="lvl-2"><p>前台运行</p></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 运行 git svn clone 时，它会自动使用缓存的凭证</span></span><br><span class="line"><span class="comment"># 注意，因为不包含 branch和tags ，所以svn地址要直接定位到trunk路径</span></span><br><span class="line">git svn <span class="built_in">clone</span> \</span><br><span class="line">  https://svn.test.com/mytools/sometool/trunk \</span><br><span class="line">  -r 7835:HEAD \</span><br><span class="line">  --log-window-size=1000 \</span><br><span class="line">  sometool</span><br><span class="line"></span><br><span class="line"><span class="comment">## 参数说明</span></span><br><span class="line">`-r`: 仅仅保留最近多少次的<span class="built_in">log</span>，上面最后一次是7840，这里从7835开始，相当于保留从7835到7840的6次提交，如果保留全部就不加</span><br><span class="line">`--log-window-size`: 每次向 SVN 服务器请求 1000 条 revision 日志，然后再继续请求下一批。默认 100。设置较大可以减少与svn服务器之间的通信次数，降低迁移时间，但同时也会增加内存占用和单次请求时间。一般建议大于2万Revision时才调大设置。</span><br><span class="line">`sometool`: 本地目录名称，就是将svn转化为git后的项目根目录</span><br><span class="line"></span><br><span class="line"><span class="comment"># 可以通过如下命令查看commit历史</span></span><br><span class="line">git <span class="built_in">log</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>后台运行</p></li></ul><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"><span class="built_in">nohup</span> git svn <span class="built_in">clone</span> https://svn.test.com/mytools/sometool/trunk sometool &gt; clone.log 2&gt;&amp;1 &amp;</span><br><span class="line"></span><br><span class="line"><span class="comment"># 如果没有运行过 svn info,即没有缓存过密码，可以通过如下命令进行迁移</span></span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;你的svn密码&quot;</span> | <span class="built_in">nohup</span> git svn <span class="built_in">clone</span> https://svn.test.com/mytools/sometool/trunk sometool --username 你的svn用户名 --no-auth-cache &gt; clone.log 2&gt;&amp;1 &amp;</span><br></pre></td></tr></table></figure><h4 id="发布到git仓库">发布到git仓库</h4><ul class="lvl-0"><li class="lvl-2"><p>注意此时不需要执行 <code>git add .</code> 和 <code>git commit</code>，因为迁移过程中这些commit已经自动生成了</p></li></ul><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">git remote add origin https://gitlab.test.com/android/sometool.git</span><br><span class="line"></span><br><span class="line"><span class="comment"># push</span></span><br><span class="line">git push -u origin master</span><br></pre></td></tr></table></figure><div class="tips"><p><em><strong>小贴士</strong></em></p><ul class="lvl-1"><li class="lvl-2">如果 <code>git push</code> 时报如下错误，说明上传的bady大小超过了限制，可以改用 <code>ssh push</code> 的方式</li></ul><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"># error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413</span><br><span class="line"># ssh push 的方式需要设置 SSH Keys</span><br><span class="line">git remote set-url origin git@gitlab.test.com/android/sometool.git</span><br></pre></td></tr></table></figure><ul class="lvl-1"><li class="lvl-2">生成 key（如果你没有）</li></ul><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">ssh-keygen -t ed25519 -C <span class="string">&quot;your_email@example.com&quot;</span></span><br></pre></td></tr></table></figure><ul class="lvl-1"><li class="lvl-2">添加 key 到 GitLab</li></ul><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"><span class="built_in">cat</span> ~/.ssh/id_ed25519.pub</span><br><span class="line"></span><br><span class="line"><span class="comment"># 然后到 GitLab：</span></span><br><span class="line">User Settings → SSH Keys → Add Key</span><br></pre></td></tr></table></figure></div><h3 id="同时迁移branch和tags">同时迁移<code>branch</code>和<code>tags</code></h3><ul class="lvl-0"><li class="lvl-2"><p>标准svn结构</p></li></ul><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">project/</span><br><span class="line">├── trunk</span><br><span class="line">├── branches</span><br><span class="line">└── tags</span><br></pre></td></tr></table></figure><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">git svn <span class="built_in">clone</span> \</span><br><span class="line">  https://svn.test.com/mytools/sometool \</span><br><span class="line">  --stdlayout \</span><br><span class="line">  sometool</span><br><span class="line"></span><br><span class="line"><span class="comment"># 参数说明</span></span><br><span class="line">--stdlayout：标准布局</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>非标准结构，例如</p></li></ul><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">project/</span><br><span class="line">├── source</span><br><span class="line">├── release</span><br><span class="line">└── version</span><br></pre></td></tr></table></figure><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">git svn <span class="built_in">clone</span> \</span><br><span class="line">  https://svn.test.com/mytools/sometool \</span><br><span class="line">  --trunk=<span class="built_in">source</span> \</span><br><span class="line">  --branches=release \</span><br><span class="line">  --tags=version \</span><br><span class="line">  sometool</span><br><span class="line"></span><br><span class="line"><span class="comment">## 参数说明</span></span><br><span class="line">--trunk: 指定trunk的目录名称，即开发目录</span><br><span class="line">--branches: 指定branches的目录名称，可以设置多个</span><br><span class="line">--tags: 指定tags的目录名称，可以设置多个</span><br></pre></td></tr></table></figure><h4 id="转换-SVN-Tag-和-Branch">转换 SVN Tag 和 Branch</h4><ul class="lvl-0"><li class="lvl-2"><p>git-svn 导入后，查看git分支会看到类似如下的内容</p></li></ul><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">git branch -r</span><br><span class="line">* master</span><br><span class="line">  origin/sometool-Branch</span><br><span class="line">  origin/sometool-V1.0.72</span><br><span class="line">  origin/tags/sometool-V1.0.0.00-b582-p0</span><br><span class="line">  origin/tags/sometool-V1.0.02.00-b591-p0</span><br><span class="line">  origin/tags/sometool-V1.0.02.00-b595-p0</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>实际的branch是：</p></li></ul><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">origin/sometool-Branch</span><br><span class="line">origin/sometool-V1.0.72</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>实际的tags是</p></li></ul><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">origin/tags/sometool-V1.0.0.00-b582-p0</span><br><span class="line">origin/tags/sometool-V1.0.02.00-b591-p0</span><br><span class="line">origin/tags/sometool-V1.0.02.00-b595-p0</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>但此时都显示为branch，所以需要将其转换为tag</p></li></ul><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="keyword">for</span> tag <span class="keyword">in</span> $(git branch -r | grep <span class="string">&#x27;tags/&#x27;</span> | sed <span class="string">&#x27;s|origin/tags/||&#x27;</span>); <span class="keyword">do</span></span><br><span class="line">  git tag <span class="string">&quot;<span class="variable">$tag</span>&quot;</span> <span class="string">&quot;origin/tags/<span class="variable">$tag</span>&quot;</span></span><br><span class="line"><span class="keyword">done</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>然后检查</p></li></ul><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">git tag -l</span><br><span class="line">sometool-V1.0.0.00-b582-p0</span><br><span class="line">sometool-V1.0.02.00-b591-p0</span><br><span class="line">sometool-V1.0.02.00-b595-p0</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>转换 Branch</p></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 这里创建需要保留的分支，如果分支比较少，可以向下面这样手工创建</span></span><br><span class="line">git branch sometool-Branch origin/sometool-Branch</span><br><span class="line">git branch sometool-V1.0.72 origin/sometool-V1.0.72</span><br><span class="line"></span><br><span class="line"><span class="comment"># 如果要保留全部分支，可以向转换tags那样编写一个脚本</span></span><br><span class="line">git branch -r \</span><br><span class="line">| grep -v <span class="string">&#x27;tags/&#x27;</span> \</span><br><span class="line">| grep -v <span class="string">&#x27;HEAD&#x27;</span> \</span><br><span class="line">| <span class="keyword">while</span> <span class="built_in">read</span> branch; <span class="keyword">do</span></span><br><span class="line">    local_branch=<span class="variable">$&#123;branch#origin/&#125;</span></span><br><span class="line">    git branch <span class="string">&quot;<span class="variable">$local_branch</span>&quot;</span> <span class="string">&quot;<span class="variable">$branch</span>&quot;</span></span><br><span class="line"><span class="keyword">done</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>添加远程仓库</p></li></ul><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">git remote add origin https://gitlab.test.com/android/sometool.git</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>推送所有内容</p></li></ul><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 push --all origin</span><br><span class="line"></span><br><span class="line"><span class="comment"># 推送标签：</span></span><br><span class="line">git push --tags origin</span><br><span class="line"></span><br><span class="line"><span class="comment"># 删除本地没用的分支</span></span><br><span class="line">git fetch --prune</span><br><span class="line"></span><br><span class="line"><span class="comment"># 再次查看分支，此时就正确了</span></span><br><span class="line">git branch -a</span><br></pre></td></tr></table></figure><h3 id="把-SVN-用户名映射成-Git-的用户">把 SVN 用户名映射成 Git 的用户</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">git svn <span class="built_in">clone</span> \</span><br><span class="line">  https://svn.test.com/mytools/sometool \</span><br><span class="line">  --stdlayout \</span><br><span class="line">  --authors-file=authors.txt \</span><br><span class="line">  sometool</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p><code>authors.txt</code> 格式</p></li></ul><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">SVN 用户名       Git 用户</span><br><span class="line">zhangsan   =    Zhang San &lt;zhangsan@example.com&gt;</span><br><span class="line">lisi       =    Li Si &lt;lisi@example.com&gt;</span><br></pre></td></tr></table></figure><h3 id="配置-gitignore（关键，防止编译产物进仓库）">配置 <code>.gitignore</code>（关键，防止编译产物进仓库）</h3><ul class="lvl-0"><li class="lvl-2"><p>根据需要配置<code>.gitignore</code>，并发布到远程仓库</p></li></ul><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">git add .gitignore</span><br><span class="line">git commit -m <span class="string">&quot;add android gitignore, clean svn residual&quot;</span></span><br><span class="line">git push</span><br></pre></td></tr></table></figure><h2 id="git-svn-优点与缺点对比表">git-svn 优点与缺点对比表</h2><ul class="lvl-0"><li class="lvl-2"><p>一、核心能力对比</p></li></ul><table><thead><tr><th>维度</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>SVN → Git 迁移能力</td><td>可将 SVN revision 逐条转换为 Git commit</td><td>不支持现代 Git 迁移增强（如智能重写）</td></tr><tr><td>历史保留</td><td>完整保留 commit 顺序、message、时间</td><td>复杂历史（merge / branch）可能失真</td></tr><tr><td>作者信息</td><td>可通过 <code>--authors-file</code> 精确映射 SVN 用户</td><td>不配置时作者信息可能不规范</td></tr><tr><td>revision 映射</td><td>保留 SVN revision（git-svn-id）</td><td>Git commit 与 SVN 强绑定，历史较“冗余”</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>二、分支与标签支持</p></li></ul><table><thead><tr><th>维度</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>trunk 映射</td><td>自动映射为主分支</td><td>无明显缺点</td></tr><tr><td>branches 支持</td><td>支持 SVN branches → Git branches</td><td>转换后通常需要手动整理</td></tr><tr><td>tags 支持</td><td>支持 SVN tags → Git tags</td><td>需要额外脚本转换为真正 tag</td></tr><tr><td>灵活性</td><td>可选择只迁移 trunk</td><td>多分支结构处理较繁琐</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>三、迁移方式与成本</p></li></ul><table><thead><tr><th>维度</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>使用复杂度</td><td>命令简单（git svn clone）</td><td>参数较多时容易踩坑</td></tr><tr><td>工具依赖</td><td>Git 官方工具，无需第三方软件</td><td>Windows 环境可能缺组件</td></tr><tr><td>学习成本</td><td>对 Git 用户友好</td><td>对 SVN 复杂仓库理解要求高</td></tr><tr><td>部署成本</td><td>无需额外服务</td><td>无 GUI 可视化迁移工具</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>四、性能与规模</p></li></ul><table><thead><tr><th>维度</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>中小型仓库</td><td>表现稳定</td><td>—</td></tr><tr><td>大型仓库</td><td>可通过 <code>--log-window-size</code> 优化</td><td>10万+ revision 性能较慢</td></tr><tr><td>网络依赖</td><td>支持断点式 fetch/rebase</td><td>强依赖 SVN server 性能</td></tr><tr><td>内存使用</td><td>可控</td><td>大历史导入时较高</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>五、功能扩展能力</p></li></ul><table><thead><tr><th>维度</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>增量同步</td><td>支持 <code>git svn fetch</code></td><td>维护复杂</td></tr><tr><td>双向同步</td><td>支持 <code>dcommit</code> 回写 SVN</td><td>实际工程中很少使用</td></tr><tr><td>历史可追溯性</td><td>SVN revision 与 Git commit 可对照</td><td>Git history 不够“现代化”</td></tr><tr><td>生态兼容</td><td>Git 原生工具链可用</td><td>不适合 Git-centric 工作流重构</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>六、适用场景总结</p></li></ul><table><thead><tr><th>场景</th><th>是否适合 git-svn</th></tr></thead><tbody><tr><td>SVN → Git 一次性迁移</td><td>✔ 非常适合</td></tr><tr><td>保留完整 SVN 历史</td><td>✔ 最常用方案</td></tr><tr><td>只迁移 trunk 简化历史</td><td>✔ 很适合</td></tr><tr><td>大规模企业 Git 重构</td><td>⚠ 可用但不最佳</td></tr><tr><td>复杂 Git 历史重写</td><td>❌ 不适合</td></tr><tr><td>长期 SVN + Git 双向开发</td><td>⚠ 可用但维护成本高</td></tr></tbody></table>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/06/24/git-svn/</id>
    <link href="https://blog.hanqunfeng.com/2026/06/24/git-svn/"/>
    <published>2026-06-24T13:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">本文介绍如何使用<code>git-svn</code>将 SVN 中的项目迁移到 Git</li>
</ul>]]>
    </summary>
    <title>SVN 迁移到 Git</title>
    <updated>2026-06-25T09:21:13.938Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="vibecoding" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/vibecoding/"/>
    <category term="claude code" scheme="https://blog.hanqunfeng.com/tags/claude-code/"/>
    <category term="opencode" scheme="https://blog.hanqunfeng.com/tags/opencode/"/>
    <category term="codex-cli" scheme="https://blog.hanqunfeng.com/tags/codex-cli/"/>
    <content>
      <![CDATA[<!-- **加粗** *斜体* ***加粗并斜体*** ~~删除线~~ ==突出显示== `突出显示(推荐)` ++下划线++ ~下标~ ^上标^ 脚注，参考文献[^1]，然后在文档最下方要添加这个1对应的内容，如：[^1]: My reference. 图片设置宽度和高度(通过uPic上传后，需要将upic修改过为blog，用于添加水印) ![](https://upic-oss.oss-cn-beijing.aliyuncs.com/blog/innodb_buffer_pool.png =900x600) +++ **点击折叠** 这是被隐藏的内容 +++::: tips success warning danger这里是容器内的内容:::% note info % success warning danger这里是容器内的内容% endnote %引用本地其它文章连接{} 大括号开始% post_link 文件名称(不包含.md) %大括号结束 --><h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2"><p>记录 <strong>Claude Code</strong>、<strong>OpenCode</strong> 与 <strong>Codex CLI</strong> 的 API 流量。在自包含 HTML 查看器中查看系统提示词、工具输出、思考块以及完整请求/响应数据。</p></li><li class="lvl-2"><p><strong><a href="https://github.com/badlogic/lemmy/tree/main/apps/claude-trace">mariozechner/claude-trace</a> 的分支版本</strong>，扩展支持 <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code V2+</a> 原生二进制、独立的 <strong><a href="https://opencode.ai">OpenCode</a></strong> 命令（多 provider 拦截，Anthropic 与 OpenAI API 格式），以及 <strong><a href="https://developers.openai.com/codex/cli">Codex CLI</a> ChatGPT OAuth</strong> 追踪（通过 ChatGPT 账号登录 —— 多数用户的默认 Codex 认证方式）。</p></li></ul><span id="more"></span><h2 id="支持的工具">支持的工具</h2><table><thead><tr><th>工具</th><th>CLI 命令</th><th>日志目录</th><th>拦截方式</th></tr></thead><tbody><tr><td><strong><a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a></strong></td><td><code>claude-trace</code></td><td><code>.claude-trace/</code></td><td>V1：<code>fetch()</code> 钩子 · V2+：反向代理（<code>ANTHROPIC_BASE_URL</code>）</td></tr><tr><td><strong><a href="https://opencode.ai">OpenCode</a></strong></td><td><code>opencode-trace</code></td><td><code>.opencode-trace/</code></td><td>反向代理 + 模型路由；支持 Anthropic 与 OpenAI 格式</td></tr><tr><td><strong><a href="https://developers.openai.com/codex/cli">Codex CLI</a></strong></td><td><code>codex-trace</code></td><td><code>.codex-trace/</code></td><td>反向代理（<code>CODEX_HOME</code> 覆盖）；<strong>ChatGPT OAuth</strong> 与 OpenAI API Key（Responses API）</td></tr></tbody></table><p>三条命令共用同一套 HTML 报告界面、JSONL/JSON 导出，以及 <code>--index</code> 会话摘要功能。</p><h2 id="快速开始">快速开始</h2><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">npm install -g @hanqunfeng/claude-trace</span><br><span class="line"></span><br><span class="line"><span class="comment"># Claude Code</span></span><br><span class="line">claude-trace</span><br><span class="line"></span><br><span class="line"><span class="comment"># OpenCode</span></span><br><span class="line">opencode-trace</span><br><span class="line"></span><br><span class="line"><span class="comment"># Codex CLI</span></span><br><span class="line">codex-trace</span><br></pre></td></tr></table></figure><p>会话结束后会自动在浏览器中打开最新 HTML 报告（可用 <code>--no-open</code> 关闭）。</p><h2 id="安装">安装</h2><h3 id="从-npm-安装">从 npm 安装</h3><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">npm install -g @hanqunfeng/claude-trace</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></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/hanqunfeng/claude-trace.git</span><br><span class="line"><span class="built_in">cd</span> claude-trace</span><br><span class="line">npm run setup   <span class="comment"># 安装根目录 + frontend 依赖</span></span><br><span class="line">npm run build</span><br><span class="line">npm <span class="built_in">link</span>        <span class="comment"># 可选：全局可用 claude-trace、opencode-trace 与 codex-trace</span></span><br><span class="line"><span class="comment"># 不 link 则用 node dist/cli/cli.js / node dist/cli/opencode-cli.js / node dist/cli/codex-cli.js</span></span><br></pre></td></tr></table></figure><h2 id="Claude-Code（claude-trace）">Claude Code（<code>claude-trace</code>）</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><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 启动 Claude Code 并记录日志（自动识别 V1 JS / V2+ 原生二进制）</span></span><br><span class="line">claude-trace</span><br><span class="line"></span><br><span class="line"><span class="comment"># 记录所有 API 请求（代理模式默认只记录 /v1/messages）</span></span><br><span class="line">claude-trace --include-all-requests</span><br><span class="line"></span><br><span class="line"><span class="comment"># 记录敏感 header 且不脱敏（请谨慎使用）</span></span><br><span class="line">claude-trace --include-sensitive-headers</span><br><span class="line"></span><br><span class="line"><span class="comment"># 向 Claude 传递参数</span></span><br><span class="line">claude-trace --run-with chat --model sonnet-3.5</span><br><span class="line"></span><br><span class="line"><span class="comment"># 指定 Claude 二进制路径</span></span><br><span class="line">claude-trace --claude-path /usr/local/Caskroom/claude-code/2.1.153/claude</span><br><span class="line"></span><br><span class="line"><span class="comment"># 提取 OAuth token（V1 Node.js 路径）</span></span><br><span class="line">claude-trace --extract-token</span><br><span class="line"></span><br><span class="line"><span class="comment"># 从已有 .jsonl 生成 HTML 报告</span></span><br><span class="line">claude-trace --generate-html logs.jsonl report.html</span><br><span class="line"></span><br><span class="line"><span class="comment"># 生成会话摘要与可搜索索引</span></span><br><span class="line">claude-trace --index</span><br><span class="line"></span><br><span class="line">claude-trace --<span class="built_in">help</span></span><br></pre></td></tr></table></figure><p>日志路径：当前目录 <code>.claude-trace/log-YYYY-MM-DD-HH-MM-SS.{jsonl,json,html}</code>。</p><h3 id="CLI-选项">CLI 选项</h3><table><thead><tr><th>参数</th><th>说明</th></tr></thead><tbody><tr><td><code>--include-all-requests</code></td><td>记录所有 API 流量，不仅限于 <code>/v1/messages</code></td></tr><tr><td><code>--include-sensitive-headers</code></td><td>记录 auth token、cookie 等，不做脱敏</td></tr><tr><td><code>--log NAME</code></td><td>自定义日志文件名（不含扩展名）</td></tr><tr><td><code>--claude-path PATH</code></td><td>Claude 二进制路径（省略则自动检测）</td></tr><tr><td><code>--no-open</code></td><td>生成 HTML 后不自动打开浏览器</td></tr><tr><td><code>--run-with ARGS...</code></td><td>将后续参数传给 Claude</td></tr><tr><td><code>--extract-token</code></td><td>提取 OAuth token 后退出</td></tr><tr><td><code>--generate-html FILE [OUT]</code></td><td>从 JSONL 生成 HTML 报告</td></tr><tr><td><code>--index</code></td><td>生成会话摘要与索引</td></tr></tbody></table><h3 id="Claude-Code-V2-（原生二进制）">Claude Code V2+（原生二进制）</h3><p>Claude Code V2 以<strong>原生二进制</strong>分发（macOS Mach-O / Linux ELF / Windows PE），不再是 Node.js 脚本。原有 <code>node --require interceptor claude</code> 方式已不可用。</p><table><thead><tr><th>Claude Code 版本</th><th>二进制类型</th><th>拦截模式</th></tr></thead><tbody><tr><td>V1.x</td><td>Node.js 脚本</td><td>通过 <code>--require</code> 注入 <code>interceptor-loader.js</code></td></tr><tr><td><strong>V2+</strong></td><td>原生二进制</td><td>本地反向代理；重定向 <code>ANTHROPIC_BASE_URL</code></td></tr></tbody></table><p>流程：</p><ol><li class="lvl-3"><p>在 <code>127.0.0.1</code> 启动本地 HTTP 反向代理</p></li><li class="lvl-3"><p>通过 <code>ANTHROPIC_BASE_URL</code> 将 Claude Code 指向代理</p></li><li class="lvl-3"><p>转发流量到真实上游（<code>~/.claude/settings.json</code> 或环境变量）</p></li><li class="lvl-3"><p>实时写入 <code>.claude-trace/</code></p></li></ol><p>若 <code>~/.claude/settings.json</code> 已设置 <code>ANTHROPIC_BASE_URL</code>，会使用持久化配置 overlay（<code>~/.claude-trace/claude-config-overlay/</code>）：仅重写 <code>settings.json</code> 去掉该项，其余条目尽量通过符号链接指向原配置（Windows 目录用 junction，文件 symlink 失败则复制）。<strong>单项链接失败不会阻断启动</strong>，代理仍可用；跳过的条目仅在 <code>CLAUDE_TRACE_DEBUG=1</code> 时输出到 stderr。</p><h3 id="第三方模型（CC-Switch-与自定义端点）">第三方模型（CC-Switch 与自定义端点）</h3><p>支持任何通过自定义 <code>ANTHROPIC_BASE_URL</code> 路由 Claude Code 的方案——<a href="https://github.com/farion1231/cc-switch">CC-Switch</a>、LiteLLM、企业网关、自托管代理等。</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">Claude Code  →  claude-trace 代理（记录）  →  CC-Switch / 自定义端点  →  模型提供商</span><br></pre></td></tr></table></figure><p>CC-Switch 示例：</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"># CC-Switch 写入 ~/.claude/settings.json 后：</span></span><br><span class="line">claude-trace</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><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">export</span> ANTHROPIC_BASE_URL=<span class="string">&quot;https://your-gateway.example.com&quot;</span></span><br><span class="line">claude-trace</span><br></pre></td></tr></table></figure><p>注意事项：</p><ul class="lvl-0"><li class="lvl-2"><p>上游需支持 <strong>Anthropic Messages API</strong>（<code>/v1/messages</code>），或使用能转换为该格式的网关</p></li><li class="lvl-2"><p>API Key 及 settings 中的其他 <code>env</code> 项会保留——仅在本地覆盖 <code>ANTHROPIC_BASE_URL</code></p></li><li class="lvl-2"><p>HTML 日志会显示实际上游 URL 和每次请求的模型名</p></li></ul><h3 id="请求过滤（Claude）">请求过滤（Claude）</h3><p><strong>代理模式（V2+）：</strong> 默认 <code>/v1/messages</code>；<code>--include-all-requests</code> 记录所有代理流量。</p><p><strong>拦截模式（V1，Node.js）：</strong> 默认记录上下文中消息数 &gt; 2 的 <code>/v1/messages</code>；<code>--include-all-requests</code> 记录所有 <code>api.anthropic.com</code> 请求。</p><hr><h2 id="OpenCode（opencode-trace）">OpenCode（<code>opencode-trace</code>）</h2><h3 id="使用-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><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"># 启动 OpenCode TUI 并记录日志</span></span><br><span class="line">opencode-trace</span><br><span class="line"></span><br><span class="line"><span class="comment"># 单次 prompt</span></span><br><span class="line">opencode-trace --run-with run <span class="string">&quot;Explain async/await&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 指定模型</span></span><br><span class="line">opencode-trace --run-with run -m my-deepseek/deepseek-v4-flash <span class="string">&quot;Refactor this module&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 从已有会话生成 HTML</span></span><br><span class="line">opencode-trace --generate-html .opencode-trace/log-2025-01-01-12-00-00.jsonl</span><br><span class="line"></span><br><span class="line"><span class="comment"># 生成会话索引</span></span><br><span class="line">opencode-trace --index</span><br><span class="line"></span><br><span class="line">opencode-trace --<span class="built_in">help</span></span><br></pre></td></tr></table></figure><p>日志路径：当前目录 <code>.opencode-trace/log-YYYY-MM-DD-HH-MM-SS.{jsonl,json,html}</code>。代理运行时错误追加写入 <code>.opencode-trace/proxy-errors.log</code>。</p><h3 id="拦截原理">拦截原理</h3><p>OpenCode 为原生二进制。<code>opencode-trace</code> 启动本地反向代理，并通过 <code>OPENCODE_CONFIG_CONTENT</code> 注入运行时配置覆盖——<strong>不会修改原始 <code>opencode.json</code></strong>。</p><p>配置中所有 provider 的 <code>baseURL</code> 均指向本地代理。代理从请求体读取 <code>model</code> 字段，映射到对应 provider 与真实上游 URL，再转发请求。支持 <strong>Anthropic</strong>（<code>/v1/messages</code>）与 <strong>OpenAI</strong> 格式（<code>/v1/chat/completions</code>、<code>/v1/responses</code>，即 <code>@ai-sdk/openai-compatible</code> 与 <code>@ai-sdk/openai</code>）。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">OpenCode  →  opencode-trace 代理（记录）  →  provider baseURL（DeepSeek、MiniMax 等）</span><br></pre></td></tr></table></figure><p>配置读取顺序：</p><ol><li class="lvl-3"><p>环境变量 <code>OPENCODE_CONFIG</code></p></li><li class="lvl-3"><p><code>OPENCODE_CONFIG_DIR/opencode.json</code></p></li><li class="lvl-3"><p><code>~/.config/opencode/opencode.json</code></p></li><li class="lvl-3"><p>当前目录 <code>.opencode/opencode.json</code></p></li></ol><h3 id="支持的-API-格式">支持的 API 格式</h3><table><thead><tr><th>OpenCode <code>npm</code> 包</th><th>API 格式</th><th>端点</th><th>对话视图标签</th></tr></thead><tbody><tr><td><code>@ai-sdk/anthropic</code></td><td>Anthropic Messages</td><td><code>/v1/messages</code></td><td>Anthropic Messages</td></tr><tr><td><code>@ai-sdk/openai-compatible</code></td><td>OpenAI Chat Completions</td><td><code>/v1/chat/completions</code></td><td>OpenAI Chat</td></tr><tr><td><code>@ai-sdk/openai</code></td><td>OpenAI Responses</td><td><code>/v1/responses</code></td><td>OpenAI Responses</td></tr></tbody></table><p>代理从请求体读取 <code>model</code> 字段，路由到对应 provider 的 <code>baseURL</code>。同一 provider 下可通过 per-model <code>npm</code> 混用 chat 与 responses API。未在 <code>opencode.json</code> 中显式声明的模型，可通过 provider 级回退路由（<code>providerId/*</code>）匹配。</p><h3 id="CLI-选项-2">CLI 选项</h3><table><thead><tr><th>参数</th><th>说明</th></tr></thead><tbody><tr><td><code>--opencode-path PATH</code></td><td>OpenCode 二进制路径（省略则自动检测）</td></tr><tr><td><code>--include-all-requests</code></td><td>记录所有代理流量，不仅限于 messages 端点</td></tr><tr><td><code>--include-sensitive-headers</code></td><td>不脱敏记录 auth 头</td></tr><tr><td><code>--log NAME</code></td><td>自定义日志文件名</td></tr><tr><td><code>--no-open</code></td><td>不自动打开 HTML</td></tr><tr><td><code>--run-with ARGS...</code></td><td>传递给 OpenCode 的参数</td></tr></tbody></table><h3 id="调试">调试</h3><p>默认情况下运行时日志<strong>静默输出</strong>，避免污染 OpenCode TUI 输入框。</p><table><thead><tr><th>输出内容</th><th>默认行为</th><th>设置 <code>OPENCODE_TRACE_DEBUG=1</code> 后</th></tr></thead><tbody><tr><td>每次请求路由（model → provider → 上游 URL）</td><td>不输出</td><td>打印到 stderr</td></tr><tr><td>代理错误（如上游 TLS 连接失败）</td><td>写入 <code>.opencode-trace/proxy-errors.log</code></td><td>同时打印到 stderr</td></tr></tbody></table><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">OPENCODE_TRACE_DEBUG=1 opencode-trace</span><br></pre></td></tr></table></figure><p>当模型未正确路由、日志缺少请求，或上游连接失败时，可使用此命令排查。</p><h3 id="OpenCode-当前限制">OpenCode 当前限制</h3><ul class="lvl-0"><li class="lvl-2"><p><strong>对话视图</strong>支持 Anthropic 格式（<code>@ai-sdk/anthropic</code>）与 OpenAI 格式（<code>@ai-sdk/openai-compatible</code>、<code>@ai-sdk/openai</code>）provider；复杂字段（多模态、reasoning 等）可能仅在 Raw/JSON 视图中完整展示。</p></li><li class="lvl-2"><p>尚未拦截未在 <code>opencode.json</code> 中定义的内置 <code>models.dev</code> provider。</p></li></ul><hr><h2 id="Codex-CLI（codex-trace）">Codex CLI（<code>codex-trace</code>）</h2><p><strong>主要认证方式：ChatGPT OAuth。</strong> 若你通过 ChatGPT 账号登录 Codex（多数安装的默认方式），<code>codex-trace</code> 可完整追踪该路径 —— 多轮对话、zstd 压缩请求与 SSE 流式响应均能在 HTML 报告中正确展示。</p><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 启动 Codex TUI 并记录流量（支持 ChatGPT OAuth 或 API Key）</span></span><br><span class="line">codex-trace</span><br><span class="line"></span><br><span class="line"><span class="comment"># 无头单次执行</span></span><br><span class="line">codex-trace --run-with <span class="built_in">exec</span> <span class="string">&quot;Explain async/await&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 从历史日志生成 HTML</span></span><br><span class="line">codex-trace --generate-html .codex-trace/log-2025-01-01-12-00-00.jsonl</span><br><span class="line"></span><br><span class="line"><span class="comment"># 生成会话索引</span></span><br><span class="line">codex-trace --index</span><br><span class="line"></span><br><span class="line">codex-trace --<span class="built_in">help</span></span><br></pre></td></tr></table></figure><p>日志路径：当前目录 <code>.codex-trace/log-YYYY-MM-DD-HH-MM-SS.{jsonl,json,html}</code>。</p><h3 id="ChatGPT-OAuth-模式（推荐）">ChatGPT OAuth 模式（推荐）</h3><p>多数用户通过 <strong>ChatGPT 登录</strong> 使用 Codex（<code>codex login</code> 或 TUI 内登录）。<code>codex-trace</code> 读取 <code>~/.codex/auth.json</code>，当 <code>auth_mode</code> 为 <code>&quot;chatgpt&quot;</code> 时，将所有 LLM 流量路由到 ChatGPT OAuth 上游（<code>chatgpt.com/backend-api/codex</code>）——<strong>即使 shell 中为其他工具设置了 <code>OPENAI_BASE_URL</code></strong>（如 Cursor、LiteLLM 等）也不受影响。</p><table><thead><tr><th>检查项</th><th>预期</th></tr></thead><tbody><tr><td>认证文件</td><td><code>~/.codex/auth.json</code> 含 <code>&quot;auth_mode&quot;: &quot;chatgpt&quot;</code></td></tr><tr><td>会话数据</td><td>从真实 <code>$CODEX_HOME</code> symlink，OAuth token 不会被改写</td></tr><tr><td>HTML 报告</td><td>多轮对话完整展示每条 assistant 回复，含最后一轮</td></tr></tbody></table><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">Codex CLI（ChatGPT OAuth）  →  codex-trace 代理（记录）  →  chatgpt.com/backend-api/codex</span><br></pre></td></tr></table></figure><p><strong>提示：</strong> 请先完成 ChatGPT 登录，再启动 <code>codex-trace</code>。若在 trace 会话中途于 Codex 内切换认证方式，建议重新运行 <code>codex-trace</code>。</p><h3 id="OpenAI-API-Key-模式">OpenAI API Key 模式</h3><p>若使用 <strong>OpenAI API Key</strong> 而非 ChatGPT 登录，流量经 <code>openai_base_url</code> / <code>OPENAI_BASE_URL</code> / <code>api.openai.com</code>（或自定义网关）路由。请勿将 ChatGPT OAuth token 与自定义 <code>OPENAI_BASE_URL</code> 网关混用 —— 同一时间只应使用一种认证方式。</p><h3 id="拦截原理-2">拦截原理</h3><p>Codex CLI 为 Rust 原生二进制。<code>codex-trace</code> 启动本地反向代理，并在 <code>~/.claude-trace/codex-config-overlay/</code> 构建配置覆盖层——<strong>不会修改原始 <code>~/.codex/config.toml</code></strong>。</p><p>覆盖层将 <code>openai_base_url</code>、<code>chatgpt_base_url</code> 及自定义 <code>model_providers.*.base_url</code> 改写为代理地址。<code>auth.json</code> 与会话数据通过 symlink 保留，ChatGPT OAuth 可继续使用。代理根据 <strong><code>auth.json</code> 的 auth 模式</strong>（ChatGPT OAuth 或 API Key）与请求路径选择上游：</p><ul class="lvl-0"><li class="lvl-2"><p><strong>ChatGPT OAuth</strong>（<code>auth_mode: &quot;chatgpt&quot;</code>）：<code>/responses</code>、<code>/v1/responses</code>、<code>/backend-api/codex/...</code> → <code>chatgpt.com</code></p></li><li class="lvl-2"><p><strong>ChatGPT Apps MCP</strong>（<code>codex_apps</code>）：<code>/api/codex/apps</code> → <code>chatgpt.com/backend-api/wham/apps</code>；<code>/backend-api/wham/...</code> → <code>chatgpt.com</code>（站点根路径）</p></li><li class="lvl-2"><p><strong>OpenAI API Key</strong>：<code>/v1/responses</code>、<code>/responses</code> → <code>openai_base_url</code> / <code>OPENAI_BASE_URL</code> / 默认 OpenAI 主机</p></li><li class="lvl-2"><p><strong>自定义 <code>model_providers</code></strong>：非保留 provider id 且配置了 <code>base_url</code> 的条目</p></li></ul><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">Codex CLI  →  codex-trace 代理（记录）  →  chatgpt.com（OAuth）/ api.openai.com / 自定义 provider</span><br></pre></td></tr></table></figure><p>配置查找：<code>CODEX_HOME</code>（覆盖层）或 <code>~/.codex/config.toml</code>。</p><h3 id="CLI-选项-3">CLI 选项</h3><table><thead><tr><th>参数</th><th>说明</th></tr></thead><tbody><tr><td><code>--codex-path PATH</code></td><td>Codex 二进制路径（省略则自动检测）</td></tr><tr><td><code>--include-all-requests</code></td><td>记录所有代理流量，不仅限于 LLM API 路径</td></tr><tr><td><code>--include-sensitive-headers</code></td><td>不脱敏记录认证头</td></tr><tr><td><code>--log NAME</code></td><td>自定义日志文件名前缀</td></tr><tr><td><code>--no-open</code></td><td>不自动打开 HTML</td></tr><tr><td><code>--run-with ARGS...</code></td><td>将后续参数传给 Codex</td></tr></tbody></table><h3 id="Codex-限制">Codex 限制</h3><ul class="lvl-0"><li class="lvl-2"><p>覆盖层强制 <code>supports_websockets = false</code>，确保 HTTP/SSE 流量可被记录。</p></li><li class="lvl-2"><p>内置 provider ID（<code>openai</code>、<code>ollama</code>、<code>lmstudio</code>）不能通过 <code>model_providers</code> 覆盖；ChatGPT OAuth 使用 <code>chatgpt_base_url</code>，API Key 模式使用 <code>openai_base_url</code>。</p></li><li class="lvl-2"><p><strong>Ollama / LM Studio</strong> 内置 provider 不会被拦截（流量绕过代理）。</p></li><li class="lvl-2"><p>旧版 <code>auth.json</code> 若无 <code>auth_mode</code> 字段，会回退到旧启发式；若 OAuth 路由异常，请重新用 ChatGPT 登录。</p></li><li class="lvl-2"><p><strong>Node.js：</strong> Codex ChatGPT OAuth 追踪在 <strong>Node.js 16+</strong> 即可正常使用（代理原样转发 zstd 请求体）。建议 <strong>Node.js 22+</strong>，以便在日志与 HTML 中解压展示 zstd 压缩的请求体；Node 16–21 下请求条目会显示占位符而非解析后的 JSON（响应解析与代理行为不受影响）。</p></li></ul><hr><h2 id="共用功能">共用功能</h2><h3 id="HTML-报告">HTML 报告</h3><p>每次会话生成自包含 HTML 文件（内嵌 CSS/JS），可离线打开。退出时默认自动打开浏览器，可用 <code>--no-open</code> 关闭。</p><h3 id="你将看到的内容">你将看到的内容</h3><ul class="lvl-0"><li class="lvl-2"><p><strong>系统提示词</strong> — 发送给模型的隐藏指令</p></li><li class="lvl-2"><p><strong>工具定义与输出</strong> — 参数及原始工具结果</p></li><li class="lvl-2"><p><strong>思考块</strong> — 内部推理过程（如有）</p></li><li class="lvl-2"><p><strong>Token 用量</strong> — 含缓存命中的详细统计</p></li><li class="lvl-2"><p><strong>原始 JSONL 日志</strong> — 完整请求/响应对</p></li><li class="lvl-2"><p><strong>交互式查看器</strong> — 对话、原始 HTTP、JSON 调试等视图</p></li><li class="lvl-2"><p><strong>API 格式标签</strong> — 每个会话头部显示请求格式（如 <code>8 messages · OpenAI Chat</code>）</p></li></ul><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">claude-trace --index</span><br><span class="line"><span class="comment"># 或</span></span><br><span class="line">opencode-trace --index</span><br><span class="line"><span class="comment"># 或</span></span><br><span class="line">codex-trace --index</span><br></pre></td></tr></table></figure><p>扫描日志文件，通过 Claude CLI 为有意义会话生成摘要，并输出可搜索的 <code>index.html</code>。<strong>注意：</strong> 索引会产生额外 API token 消耗。</p><h2 id="环境要求">环境要求</h2><ul class="lvl-0"><li class="lvl-2"><p>Node.js 16+（Codex OAuth 请求体完整日志展示建议 Node.js 22+）</p></li><li class="lvl-2"><p><strong>Claude Code</strong> CLI（V1 Node.js 或 V2+ 原生二进制），用于 <code>claude-trace</code></p></li><li class="lvl-2"><p><strong>OpenCode</strong> CLI，用于 <code>opencode-trace</code></p></li><li class="lvl-2"><p><strong>Codex CLI</strong>，用于 <code>codex-trace</code></p></li></ul><h2 id="开发">开发</h2><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">npm run setup    <span class="comment"># 首次安装</span></span><br><span class="line">npm run dev      <span class="comment"># watch 模式；预览 http://localhost:8080/test</span></span><br><span class="line">npm run build</span><br><span class="line">npm run typecheck</span><br><span class="line">npm run <span class="built_in">test</span>:unit</span><br></pre></td></tr></table></figure><h3 id="架构">架构</h3><p><strong>后端</strong>（<code>src/</code>）：</p><ul class="lvl-0"><li class="lvl-2"><p><strong>CLI</strong>（<code>cli/cli.ts</code>、<code>cli/opencode-cli.ts</code>、<code>cli/codex-cli.ts</code>、<code>cli/cli-common.ts</code>）— 薄入口与共用参数解析</p></li><li class="lvl-2"><p><strong>Trace Runner</strong>（<code>cli/trace-runner.ts</code>）— 通用启动与代理/拦截器分发</p></li><li class="lvl-2"><p><strong>Tool Profiles</strong>（<code>tools/claude.ts</code>、<code>tools/opencode.ts</code>、<code>tools/codex.ts</code>、<code>tools/binary-utils.ts</code>）— 各工具配置、二进制检测、上游解析</p></li><li class="lvl-2"><p><strong>Config Overlays</strong>（<code>config/claude-config-overlay.ts</code>、<code>config/codex-config-overlay.ts</code>）— 持久化代理覆盖层，不修改用户原始配置</p></li><li class="lvl-2"><p><strong>Reverse Proxy</strong>（<code>intercept/reverse-proxy.ts</code>）— 原生二进制拦截，实时 HTML 生成</p></li><li class="lvl-2"><p><strong>Interceptor</strong>（<code>intercept/interceptor.ts</code>）+ <strong>Loader</strong>（<code>intercept/interceptor-loader.js</code>、<code>intercept/token-extractor.js</code>）— Claude Code V1</p></li><li class="lvl-2"><p><strong>Routing</strong>（<code>routing/proxy-routing.ts</code>、<code>routing/codex-routing.ts</code>）— OpenCode 模型路由与 Codex 路径/auth 上游选择</p></li><li class="lvl-2"><p><strong>API Format</strong>（<code>adapt/api-format.ts</code>）— 格式检测与展示标签</p></li><li class="lvl-2"><p><strong>OpenAI Adapter</strong>（<code>adapt/openai-adapter.ts</code>）— OpenAI 请求/响应适配为 Anthropic <code>Message</code>，供查看器展示</p></li><li class="lvl-2"><p><strong>Report</strong>（<code>report/html-generator.ts</code>、<code>report/index-generator.ts</code>、<code>report/shared-conversation-processor.ts</code>）— HTML 生成与会话解析</p></li></ul><p><strong>前端</strong>（<code>frontend/src/</code>）：Lit + Tailwind 交互查看器，嵌入 HTML 报告。</p><h2 id="许可证">许可证</h2><p>MIT — 原作者 <a href="https://github.com/badlogic/lemmy/tree/main/apps/claude-trace">Mario Zechner</a>，由 <a href="https://github.com/hanqunfeng/claude-trace">@hanqunfeng/claude-trace</a> 维护。</p>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/06/15/vibe-coding-trace/</id>
    <link href="https://blog.hanqunfeng.com/2026/06/15/vibe-coding-trace/"/>
    <published>2026-06-15T13:30:05.000Z</published>
    <summary>
      <![CDATA[<!--
 **加粗**
 *斜体*
 ***加粗并斜体***
 ~~删除线~~
 ==突出显示==
 `突出显示(推荐)`
 ++下划线++
 ~下标~
 ^上标^
 脚注，参考文献[^1]，然后在文档最下方要添加这个1对应的内容，如：[^1]: My reference.
 图片设置宽度和高度(通过uPic上传后，需要将upic修改过为blog，用于添加水印) ![](https://upic-oss.oss-cn-beijing.aliyuncs.com/blog/innodb_buffer_pool.png =900x600)

 +++ **点击折叠**
 这是被隐藏的内容
 +++

::: tips success warning danger
这里是容器内的内容
:::

% note info % success warning danger
这里是容器内的内容
% endnote %

引用本地其它文章连接{}
 大括号开始% post_link 文件名称(不包含.md) %大括号结束
 -->
<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">
<p>记录 <strong>Claude Code</strong>、<strong>OpenCode</strong> 与 <strong>Codex CLI</strong> 的 API 流量。在自包含 HTML 查看器中查看系统提示词、工具输出、思考块以及完整请求/响应数据。</p>
</li>
<li class="lvl-2">
<p><strong><a href="https://github.com/badlogic/lemmy/tree/main/apps/claude-trace">mariozechner/claude-trace</a> 的分支版本</strong>，扩展支持 <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code V2+</a> 原生二进制、独立的 <strong><a href="https://opencode.ai">OpenCode</a></strong> 命令（多 provider 拦截，Anthropic 与 OpenAI API 格式），以及 <strong><a href="https://developers.openai.com/codex/cli">Codex CLI</a> ChatGPT OAuth</strong> 追踪（通过 ChatGPT 账号登录 —— 多数用户的默认 Codex 认证方式）。</p>
</li>
</ul>]]>
    </summary>
    <title>记录 Claude Code、OpenCode、Codex-Cli 的 请求/响应 数据</title>
    <updated>2026-06-15T06:26:15.815Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="mysql" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/mysql/"/>
    <category term="mysql" scheme="https://blog.hanqunfeng.com/tags/mysql/"/>
    <content>
      <![CDATA[<!-- **加粗** *斜体* ***加粗并斜体*** ~~删除线~~ ==突出显示== `突出显示(推荐)` ++下划线++ ~下标~ ^上标^ 脚注，参考文献[^1]，然后在文档最下方要添加这个1对应的内容，如：[^1]: My reference. 图片设置宽度和高度(通过uPic上传后，需要将upic修改过为blog，用于添加水印) ![](https://upic-oss.oss-cn-beijing.aliyuncs.com/blog/innodb_buffer_pool.png =900x600) +++ **点击折叠** 这是被隐藏的内容 +++ --><h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2"><p>MySql知识点介绍:将大数据量的表进行拆分</p></li><li class="lvl-2"><p>本文基于<code>mysql-5.7.44</code></p></li></ul><span id="more"></span><h2 id="背景介绍">背景介绍</h2><ul class="lvl-0"><li class="lvl-2"><p>数据库中有一张大表，当前数据量超六千万条，占用空间超13G，每天新增2~3万条，大约每年新增一千万条记录</p></li><li class="lvl-2"><p>业务中日常查询主要集中在一年内的数据，偶尔需要查询一年前的数据</p></li><li class="lvl-2"><p>表结构</p></li></ul><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="keyword">CREATE TABLE</span> `ad_admob_network_by_day` (</span><br><span class="line">  `id` <span class="type">int</span>(<span class="number">11</span>) <span class="keyword">NOT NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;主键，自增&#x27;</span>,</span><br><span class="line">  `ad_date` <span class="type">date</span> <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;统计日期&#x27;</span>,</span><br><span class="line">  `app_value` <span class="type">varchar</span>(<span class="number">100</span>) <span class="keyword">COLLATE</span> utf8mb4_bin <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;app_id&#x27;</span>,</span><br><span class="line">  `app_name` <span class="type">varchar</span>(<span class="number">100</span>) <span class="keyword">COLLATE</span> utf8mb4_bin <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;app显示名称&#x27;</span>,</span><br><span class="line">  `ad_unit_value` <span class="type">varchar</span>(<span class="number">100</span>) <span class="keyword">COLLATE</span> utf8mb4_bin <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;广告单元id&#x27;</span>,</span><br><span class="line">  `ad_unit_name` <span class="type">varchar</span>(<span class="number">100</span>) <span class="keyword">COLLATE</span> utf8mb4_bin <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;广告单元名称&#x27;</span>,</span><br><span class="line">  `country_value` <span class="type">varchar</span>(<span class="number">10</span>) <span class="keyword">COLLATE</span> utf8mb4_bin <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;国家码&#x27;</span>,</span><br><span class="line">  `estimated_earnings` <span class="type">int</span>(<span class="number">20</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;预估收入，microsValue&#x27;</span>,</span><br><span class="line">  `impression_rpm` <span class="keyword">double</span>(<span class="number">30</span>,<span class="number">18</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;千次展示预估收入，doubleValue&#x27;</span>,</span><br><span class="line">  `ad_requests` <span class="type">int</span>(<span class="number">10</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;请求数，integerValue&#x27;</span>,</span><br><span class="line">  `match_rate` <span class="keyword">double</span>(<span class="number">22</span>,<span class="number">18</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;匹配率，doubleValue&#x27;</span>,</span><br><span class="line">  `matched_requests` <span class="type">int</span>(<span class="number">10</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;匹配请求数，integerValue&#x27;</span>,</span><br><span class="line">  `impressions` <span class="type">int</span>(<span class="number">10</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;展示次数，integerValue&#x27;</span>,</span><br><span class="line">  `impression_ctr` <span class="keyword">double</span>(<span class="number">22</span>,<span class="number">18</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;展示点击率，doubleValue&#x27;</span>,</span><br><span class="line">  `clicks` <span class="type">int</span>(<span class="number">10</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;点击次数&#x27;</span>,</span><br><span class="line">  `source_account` <span class="type">varchar</span>(<span class="number">100</span>) <span class="keyword">COLLATE</span> utf8mb4_bin <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;数据来源帐号,pub-xxxx&#x27;</span>,</span><br><span class="line">  <span class="keyword">PRIMARY KEY</span> (`id`),</span><br><span class="line">  <span class="keyword">UNIQUE</span> KEY `index_unique` (`ad_date`,`app_value`,`ad_unit_value`,`country_value`),</span><br><span class="line">  KEY `index_app_id` (`app_value`),</span><br><span class="line">  KEY `index_country` (`country_value`),</span><br><span class="line">  KEY `index_account` (`source_account`)</span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB AUTO_INCREMENT<span class="operator">=</span><span class="number">62660570</span> <span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8mb4 <span class="keyword">COLLATE</span><span class="operator">=</span>utf8mb4_bin COMMENT<span class="operator">=</span><span class="string">&#x27;admob数据统计-按天&#x27;</span>;</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>数据量日益增加，导致表空间和索引文件日益增大，影响查询效率</p></li></ul><h2 id="解决方案">解决方案</h2><h3 id="方案1：水平拆分">方案1：水平拆分</h3><ul class="lvl-0"><li class="lvl-2"><p>计划对该表内的数据进行拆分存储，按年拆分到不同的表中</p></li><li class="lvl-2"><p>为了方便以后运行，数据拆分时可以编写存储过程实现，如下：</p></li></ul><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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 按年拆分，按天迁移数据，新的表名称为 ad_admob_network_by_day_年份</span></span><br><span class="line"><span class="keyword">CREATE</span> DEFINER<span class="operator">=</span>`admin`@`<span class="operator">%</span>` <span class="keyword">PROCEDURE</span> `archive_admob_by_year`(<span class="keyword">IN</span> p_year <span class="type">INT</span>)</span><br><span class="line"><span class="keyword">BEGIN</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">DECLARE</span> v_start_date <span class="type">DATE</span>;</span><br><span class="line">    <span class="keyword">DECLARE</span> v_end_date <span class="type">DATE</span>;</span><br><span class="line">    <span class="keyword">DECLARE</span> v_table_name <span class="type">VARCHAR</span>(<span class="number">100</span>);</span><br><span class="line">    <span class="keyword">DECLARE</span> v_sql TEXT;</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 开始日期</span></span><br><span class="line">    <span class="keyword">SET</span> v_start_date <span class="operator">=</span> STR_TO_DATE(CONCAT(p_year, <span class="string">&#x27;-01-01&#x27;</span>), <span class="string">&#x27;%Y-%m-%d&#x27;</span>);</span><br><span class="line"><span class="comment">-- 下一年的第一天</span></span><br><span class="line">    <span class="keyword">SET</span> v_end_date <span class="operator">=</span> DATE_ADD(v_start_date, <span class="type">INTERVAL</span> <span class="number">1</span> <span class="keyword">YEAR</span>);</span><br><span class="line">    <span class="comment">-- 年表名</span></span><br><span class="line">    <span class="keyword">SET</span> v_table_name <span class="operator">=</span> CONCAT(<span class="string">&#x27;ad_admob_network_by_day_&#x27;</span>, p_year);</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 创建归档表</span></span><br><span class="line">    <span class="keyword">SET</span> v_sql <span class="operator">=</span> CONCAT(</span><br><span class="line">        <span class="string">&#x27;CREATE TABLE IF NOT EXISTS &#x27;</span>,</span><br><span class="line">        v_table_name,</span><br><span class="line">        <span class="string">&#x27; LIKE ad_admob_network_by_day&#x27;</span></span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="keyword">SET</span> <span class="variable">@sql</span> <span class="operator">=</span> v_sql;</span><br><span class="line">    <span class="keyword">PREPARE</span> stmt <span class="keyword">FROM</span> <span class="variable">@sql</span>;</span><br><span class="line">    <span class="keyword">EXECUTE</span> stmt;</span><br><span class="line">    <span class="keyword">DEALLOCATE</span> <span class="keyword">PREPARE</span> stmt;</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 按天迁移，小事务</span></span><br><span class="line">    WHILE v_start_date <span class="operator">&lt;</span> v_end_date DO</span><br><span class="line"></span><br><span class="line">        <span class="keyword">SELECT</span> CONCAT(<span class="string">&#x27;ARCHIVE: &#x27;</span>, v_start_date);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">SET</span> v_sql <span class="operator">=</span> CONCAT(</span><br><span class="line">            <span class="string">&#x27;INSERT IGNORE INTO &#x27;</span>,</span><br><span class="line">            v_table_name,</span><br><span class="line">            <span class="string">&#x27; SELECT * FROM ad_admob_network_by_day &#x27;</span>,</span><br><span class="line">            <span class="string">&#x27;WHERE ad_date = &#x27;&#x27;&#x27;</span>, v_start_date, <span class="string">&#x27;&#x27;&#x27;&#x27;</span></span><br><span class="line">        );</span><br><span class="line"></span><br><span class="line">        <span class="keyword">SET</span> <span class="variable">@sql</span> <span class="operator">=</span> v_sql;</span><br><span class="line">        <span class="keyword">PREPARE</span> stmt <span class="keyword">FROM</span> <span class="variable">@sql</span>;</span><br><span class="line">        <span class="keyword">EXECUTE</span> stmt;</span><br><span class="line">        <span class="keyword">DEALLOCATE</span> <span class="keyword">PREPARE</span> stmt;</span><br><span class="line"></span><br><span class="line">        <span class="comment">-- 下一天</span></span><br><span class="line">        <span class="keyword">SET</span> v_start_date <span class="operator">=</span> DATE_ADD(v_start_date, <span class="type">INTERVAL</span> <span class="number">1</span> <span class="keyword">DAY</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">END</span> WHILE;</span><br><span class="line"></span><br><span class="line"><span class="keyword">END</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>调用方式：按年调用，将所有年份的数据迁移到新表中，包括本年的数据</p></li></ul><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></pre></td><td class="code"><pre><span class="line"># 每次运行时间大约 <span class="number">5</span> 分钟</span><br><span class="line">mysql<span class="operator">&gt;</span> <span class="keyword">CALL</span> archive_admob_by_year(<span class="number">2020</span>);</span><br><span class="line">mysql<span class="operator">&gt;</span> <span class="keyword">CALL</span> archive_admob_by_year(<span class="number">2021</span>);</span><br><span class="line">mysql<span class="operator">&gt;</span> <span class="keyword">CALL</span> archive_admob_by_year(<span class="number">2022</span>);</span><br><span class="line">…………………………</span><br><span class="line">mysql<span class="operator">&gt;</span> <span class="keyword">CALL</span> archive_admob_by_year(<span class="number">2026</span>);</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>调用后检查两边数据量是否一致</p></li></ul><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">mysql<span class="operator">&gt;</span> <span class="keyword">select</span> <span class="built_in">count</span>(<span class="operator">*</span>) <span class="keyword">from</span> ad_admob_network_by_day t <span class="keyword">WHERE</span> t.ad_date <span class="keyword">BETWEEN</span> <span class="string">&#x27;2022-01-01&#x27;</span> <span class="keyword">AND</span> <span class="string">&#x27;2022-12-31&#x27;</span>;</span><br><span class="line"><span class="operator">+</span><span class="comment">----------+</span></span><br><span class="line"><span class="operator">|</span> <span class="built_in">count</span>(<span class="operator">*</span>) <span class="operator">|</span></span><br><span class="line"><span class="operator">+</span><span class="comment">----------+</span></span><br><span class="line"><span class="operator">|</span>  <span class="number">9977356</span> <span class="operator">|</span></span><br><span class="line"><span class="operator">+</span><span class="comment">----------+</span></span><br><span class="line"><span class="number">1</span> <span class="type">row</span> <span class="keyword">in</span> <span class="keyword">set</span> (<span class="number">6.32</span> sec)</span><br><span class="line">mysql<span class="operator">&gt;</span> <span class="keyword">select</span> <span class="built_in">count</span>(<span class="operator">*</span>) <span class="keyword">from</span> ad_admob_network_by_day_2022 t <span class="keyword">WHERE</span> t.ad_date <span class="keyword">BETWEEN</span> <span class="string">&#x27;2022-01-01&#x27;</span> <span class="keyword">AND</span> <span class="string">&#x27;2022-12-31&#x27;</span>;</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>清空并删除原始表</p></li></ul><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></pre></td><td class="code"><pre><span class="line">mysql<span class="operator">&gt;</span> <span class="keyword">TRUNCATE</span> <span class="keyword">TABLE</span> ad_admob_network_by_day;</span><br><span class="line">Query OK, <span class="number">0</span> <span class="keyword">rows</span> affected (<span class="number">5.73</span> sec)</span><br><span class="line"></span><br><span class="line">mysql<span class="operator">&gt;</span> <span class="keyword">DROP</span> <span class="keyword">TABLE</span> ad_admob_network_by_day;</span><br><span class="line">Query OK, <span class="number">0</span> <span class="keyword">rows</span> affected (<span class="number">0.40</span> sec)</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>将本年度的迁移表重命名为原始表</p></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">mysql<span class="operator">&gt;</span> RENAME <span class="keyword">TABLE</span> ad_admob_network_by_day_2026 <span class="keyword">TO</span> ad_admob_network_by_day;</span><br><span class="line">Query OK, <span class="number">0</span> <span class="keyword">rows</span> affected (<span class="number">1.19</span> sec)</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>以后每年按上面的逻辑迁移一次</p></li><li class="lvl-2"><p>优缺点</p></li></ul><table><thead><tr><th style="text-align:left">优点</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">易于维护</td><td style="text-align:left">每年只需迁移当年数据，迁移范围小，操作简单</td></tr><tr><td style="text-align:left">迁移效率高</td><td style="text-align:left">单年数据量相对较小，迁移速度较快，不易造成锁表</td></tr><tr><td style="text-align:left">减少影响</td><td style="text-align:left">不影响历史归档分表，生产风险低</td></tr><tr><td style="text-align:left">便于备份恢复</td><td style="text-align:left">单年度表结构和数据清晰，易于独立备份和恢复</td></tr><tr><td style="text-align:left">扩展灵活</td><td style="text-align:left">可根据业务增长独立新增/扩展年度表</td></tr><tr><td style="text-align:left">节省存储空间</td><td style="text-align:left">可单独对历史分表压缩或归档，提升整体表空间利用率</td></tr></tbody></table><table><thead><tr><th style="text-align:left">缺点</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">管理复杂度较高</td><td style="text-align:left">多年度分表增多时，表数量增大，管理复杂</td></tr><tr><td style="text-align:left">历史数据跨年分析麻烦</td><td style="text-align:left">查询跨年度历史数据需 union 多张年度表，SQL复杂</td></tr><tr><td style="text-align:left">存在数据一致性风险</td><td style="text-align:left">迁移过程如有中断/异常，可能造成数据不一致</td></tr><tr><td style="text-align:left">需要规划年度切换操作</td><td style="text-align:left">每年需按时迁移数据和重命名新表，增加运维要求</td></tr></tbody></table><h3 id="方案2：对原始表进行分区重构">方案2：对原始表进行分区重构</h3><ul class="lvl-0"><li class="lvl-2"><p>创建一个新表，按年进行<code>分区</code></p></li></ul><blockquote><p>分区表简单理解就是将一张大表拆分为多张小表，每张小表是一个物理表，大表是逻辑表(MySql5.7)<br>这里创建新表而不是在原始表上直接构建分区，是因为当前表已经存在超六千万条记录，构建分区时会锁表，这个数据量预计会锁表30分钟<br>另外，分区表要求 PRIMARY KEY 和 UNIQUE KEY 中必须包含 分区字段，本利中就是按年份分区，即分区字段是 ad_date</p></blockquote><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><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE TABLE</span> `ad_admob_network_by_day_partition` (</span><br><span class="line">  `id` <span class="type">BIGINT</span> <span class="keyword">NOT NULL</span> AUTO_INCREMENT COMMENT <span class="string">&#x27;主键，自增&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `ad_date` <span class="type">DATE</span> <span class="keyword">NOT NULL</span> COMMENT <span class="string">&#x27;统计日期&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `app_value` <span class="type">VARCHAR</span>(<span class="number">100</span>) <span class="keyword">COLLATE</span> utf8mb4_bin <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;app_id&#x27;</span>,</span><br><span class="line">  `app_name` <span class="type">VARCHAR</span>(<span class="number">100</span>) <span class="keyword">COLLATE</span> utf8mb4_bin <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;app显示名称&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `ad_unit_value` <span class="type">VARCHAR</span>(<span class="number">100</span>) <span class="keyword">COLLATE</span> utf8mb4_bin <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;广告单元id&#x27;</span>,</span><br><span class="line">  `ad_unit_name` <span class="type">VARCHAR</span>(<span class="number">100</span>) <span class="keyword">COLLATE</span> utf8mb4_bin <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;广告单元名称&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `country_value` <span class="type">VARCHAR</span>(<span class="number">10</span>) <span class="keyword">COLLATE</span> utf8mb4_bin <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;国家码&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `estimated_earnings` <span class="type">BIGINT</span> <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;预估收入，microsValue&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `impression_rpm` <span class="keyword">DOUBLE</span>(<span class="number">30</span>,<span class="number">18</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;千次展示预估收入&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `ad_requests` <span class="type">INT</span> <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;请求数&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `match_rate` <span class="keyword">DOUBLE</span>(<span class="number">22</span>,<span class="number">18</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;匹配率&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `matched_requests` <span class="type">INT</span> <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;匹配请求数&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `impressions` <span class="type">INT</span> <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;展示次数&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `impression_ctr` <span class="keyword">DOUBLE</span>(<span class="number">22</span>,<span class="number">18</span>) <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;点击率&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `clicks` <span class="type">INT</span> <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;点击次数&#x27;</span>,</span><br><span class="line"></span><br><span class="line">  `source_account` <span class="type">VARCHAR</span>(<span class="number">100</span>) <span class="keyword">COLLATE</span> utf8mb4_bin <span class="keyword">DEFAULT</span> <span class="keyword">NULL</span> COMMENT <span class="string">&#x27;数据来源帐号&#x27;</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">PRIMARY KEY</span> (`id`, `ad_date`),</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">UNIQUE</span> KEY `uk_admob`</span><br><span class="line">  (`ad_date`, `app_value`, `ad_unit_value`, `country_value`),</span><br><span class="line"></span><br><span class="line">  <span class="comment">-- =========================</span></span><br><span class="line">  <span class="comment">-- 业务索引（去掉冗余 index_date）</span></span><br><span class="line">  <span class="comment">-- =========================</span></span><br><span class="line">  KEY `idx_app_value` (`app_value`),</span><br><span class="line">  KEY `idx_country` (`country_value`),</span><br><span class="line">  KEY `idx_source_account` (`source_account`)</span><br><span class="line"></span><br><span class="line">) ENGINE<span class="operator">=</span>InnoDB</span><br><span class="line"><span class="keyword">DEFAULT</span> CHARSET<span class="operator">=</span>utf8mb4</span><br><span class="line"><span class="keyword">COLLATE</span><span class="operator">=</span>utf8mb4_bin</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">PARTITION</span> <span class="keyword">BY</span> <span class="keyword">RANGE</span> (<span class="keyword">YEAR</span>(ad_date)) (</span><br><span class="line"></span><br><span class="line">  <span class="keyword">PARTITION</span> p2020 <span class="keyword">VALUES</span> LESS THAN (<span class="number">2021</span>),</span><br><span class="line">  <span class="keyword">PARTITION</span> p2021 <span class="keyword">VALUES</span> LESS THAN (<span class="number">2022</span>),</span><br><span class="line">  <span class="keyword">PARTITION</span> p2022 <span class="keyword">VALUES</span> LESS THAN (<span class="number">2023</span>),</span><br><span class="line">  <span class="keyword">PARTITION</span> p2023 <span class="keyword">VALUES</span> LESS THAN (<span class="number">2024</span>),</span><br><span class="line">  <span class="keyword">PARTITION</span> p2024 <span class="keyword">VALUES</span> LESS THAN (<span class="number">2025</span>),</span><br><span class="line">  <span class="keyword">PARTITION</span> p2025 <span class="keyword">VALUES</span> LESS THAN (<span class="number">2026</span>),</span><br><span class="line"></span><br><span class="line">  <span class="keyword">PARTITION</span> pmax <span class="keyword">VALUES</span> LESS THAN MAXVALUE</span><br><span class="line">);</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>然后将原始表中的数据导入到上面创建的新表中，导入时可以参考上面<code>方案1</code>中的存储过程，<code>按年迁移，按天导入</code>的方式，过程类似，这里不再赘述</p></li><li class="lvl-2"><p>同样清空并删除原始表</p></li><li class="lvl-2"><p>重命名新表名称为原始表名称</p></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">mysql<span class="operator">&gt;</span> RENAME <span class="keyword">TABLE</span> ad_admob_network_by_day_partition <span class="keyword">TO</span> ad_admob_network_by_day;</span><br><span class="line">Query OK, <span class="number">0</span> <span class="keyword">rows</span> affected (<span class="number">1.19</span> sec)</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>以后每年新增一个分区即可</p></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">ALTER TABLE</span> ad_admob_network_by_day</span><br><span class="line">REORGANIZE <span class="keyword">PARTITION</span> pmax <span class="keyword">INTO</span> (</span><br><span class="line">  <span class="keyword">PARTITION</span> p2026 <span class="keyword">VALUES</span> LESS THAN (<span class="number">2027</span>),</span><br><span class="line">  <span class="keyword">PARTITION</span> pmax <span class="keyword">VALUES</span> LESS THAN MAXVALUE</span><br><span class="line">);</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>创建分区表的好处如下：</p></li></ul><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">❗ 不需要改代码结构</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>分区表删除分区数据极快，秒级删除</p></li></ul><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">ALTER TABLE</span> ad_admob_network_by_day <span class="keyword">DROP</span> <span class="keyword">PARTITION</span> p2025;</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>优缺点</p></li></ul><table><thead><tr><th style="text-align:left">优点</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">删除分区数据速度极快，效率高</td><td style="text-align:left">删除指定分区中的大量数据时只需元数据操作，几乎瞬间完成</td></tr><tr><td style="text-align:left">不需要建新表，无需迁移原有数据</td><td style="text-align:left">可以直接在原有表基础上分区，使用和维护方便</td></tr><tr><td style="text-align:left">不需要改动现有代码结构</td><td style="text-align:left">分区表与普通表使用相同，业务代码无需调整</td></tr><tr><td style="text-align:left">未来每年仅需新增一个分区，维护简单</td><td style="text-align:left">只需定期添加新分区即可，无需复杂操作</td></tr><tr><td style="text-align:left">数据按年分区，管理更灵活</td><td style="text-align:left">可针对不同年份单独管理、归档或清理</td></tr><tr><td style="text-align:left">支持按分区归档和扩展，便于扩展容量</td><td style="text-align:left">分区粒度支持扩容及归档，有利于大规模数据管理</td></tr></tbody></table><table><thead><tr><th style="text-align:left">缺点</th><th style="text-align:left">说明</th></tr></thead><tbody><tr><td style="text-align:left">需手动维护分区，每年要新增/删除分区</td><td style="text-align:left">运维时需定期手动添加或清理过期分区，自动化要求高</td></tr><tr><td style="text-align:left">某些SQL特性（如外键关联）不支持分区表</td><td style="text-align:left">分区表本身不支持外键等特性，设计受限</td></tr><tr><td style="text-align:left">分区键选择不当会影响查询效率</td><td style="text-align:left">错误的分区键可能造成跨分区查询，导致性能下降</td></tr><tr><td style="text-align:left">分区数量过多可能影响数据库性能及元数据维护</td><td style="text-align:left">分区太细粒度会影响DDL/DML效率和元数据开销</td></tr><tr><td style="text-align:left">不是所有类型的查询都能利用分区优势</td><td style="text-align:left">部分查询无法下推到分区级别，优化效果有限</td></tr></tbody></table><h4 id="适合采用分区表的典型场景">适合采用分区表的典型场景</h4><ol><li class="lvl-3"><p><strong>超大数据量按时间或范围型分布</strong><br>例如按日期（年/月/日）、编号区间、地区ID等离散或有序字段进行分区。典型如：</p><ul class="lvl-2"><li class="lvl-5">日志、订单、账单、统计等大表，数据持续增量按天/月/年分布</li><li class="lvl-5">业务分库分表不便时，用分区表高效管理历史数据</li></ul></li><li class="lvl-3"><p><strong>需要定期归档、清理历史数据</strong><br>如业务要求定期删除早期（如一年前）数据，分区表可以通过<code>DROP PARTITION</code>一键快速清理，效率极高。</p></li><li class="lvl-3"><p><strong>查询经常只关注某个区间/分区的数据</strong><br>比如经常仅查询最近一年、最近一个月的数据，通过分区裁剪极大提升查询性能。</p></li><li class="lvl-3"><p><strong>单表太大，超出数据量瓶颈</strong><br>单表百万、千万、甚至上亿行，分区表可缓解单表大小带来的性能与管理压力。</p></li><li class="lvl-3"><p><strong>不同数据周期有不同存储和管理需求</strong><br>比如冷数据、热数据分区后，分布到不同磁盘或存储介质，方便归档和迁移。</p></li><li class="lvl-3"><p><strong>运维操作需要高效的数据范围管理</strong><br>如快速导入导出、统计、备份特定分区内的数据等，提升管理效率。</p></li></ol><h4 id="不适合分区的情况">不适合分区的情况</h4><ul class="lvl-0"><li class="lvl-2"><p>小表、数据量较小的表，无需分区，分区反而带来运维复杂度</p></li><li class="lvl-2"><p>频繁跨分区联合查询，分区优势不明显甚至引入性能损耗</p></li><li class="lvl-2"><p>必须强依赖外键约束的业务（MySQL分区表不支持外键）</p></li></ul><blockquote><p>总之，当你遇到“大表、周期性、大区间清理/归档、基于字段分组查询”等需求时，可以优先考虑分区表方案。</p></blockquote><h2 id="后记">后记</h2><ul class="lvl-0"><li class="lvl-2"><p>在MySQL 5.7中对一张大表进行首次创建分区的操作（例如执行ALTER TABLE … PARTITION BY …）会长时间锁住整张表，直到分区创建完成。</p><ul class="lvl-2"><li class="lvl-5">逻辑分区导致的全表锁：在5.7版本中，分区是“逻辑”的，InnoDB存储引擎层无法感知分区边界。执行ALTER TABLE时，Server层会向InnoDB层发送“锁定整表”的信号，导致在执行期间无法对表进行任何修改或查询。并且这个操作会重建整个表，持有MDL_EXCLUSIVE锁，完全阻塞业务写入。</li><li class="lvl-5">分区操作期间，所有涉及该表的操作都将被阻塞，表现为“Waiting for table metadata lock”。</li><li class="lvl-5">一旦分区建好，后续的增删查改等DML操作并非都会锁全表。利用“分区裁剪(Partition Pruning)”特性，多数操作可以只锁定需要访问的特定分区。</li></ul></li><li class="lvl-2"><p>升级到MySQL 8.0后对表进行首次分区操作（ALTER TABLE … PARTITION BY …），并不是完全不锁表，但其影响会被降到最低，锁表时间将从分钟/小时级大幅缩短到秒/毫秒级。这背后的关键是MySQL 8.0的两项核心改进：分区级锁定和在线DDL（Online DDL）</p><table><thead><tr><th style="text-align:left">对比项</th><th style="text-align:left">MySQL 5.7</th><th style="text-align:left">MySQL 8.0</th></tr></thead><tbody><tr><td style="text-align:left">锁粒度</td><td style="text-align:left">逻辑分区，Server层与InnoDB层交互有限，对全表加MDL_EXCLUSIVE锁，无法分区级锁定</td><td style="text-align:left">分区元数据集成InnoDB，锁管理可感知分区边界，支持分区级锁定与真正行级锁</td></tr><tr><td style="text-align:left">首次建分区</td><td style="text-align:left">复制全表数据并重建，锁表时间长</td><td style="text-align:left">默认ALGORITHM=INPLACE, LOCK=NONE，支持并发读写，仅在切换瞬间锁表</td></tr><tr><td style="text-align:left">增删分区</td><td style="text-align:left">仅部分操作支持在线，部分情况仍需长时间锁表</td><td style="text-align:left">仅元数据变更，锁表时间从分钟级降至毫秒级，无需重建表数据</td></tr><tr><td style="text-align:left">重组/交换分区</td><td style="text-align:left">长时间锁表</td><td style="text-align:left">仍需短暂全表锁，作用范围仅限相关分区</td></tr><tr><td style="text-align:left">总体影响</td><td style="text-align:left">耗时操作需全表锁，业务阻塞明显</td><td style="text-align:left">数据重建过程变为在线操作，仅元数据替换瞬间锁表，对业务影响极小</td></tr></tbody></table></li></ul>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/05/18/mysql-partition/</id>
    <link href="https://blog.hanqunfeng.com/2026/05/18/mysql-partition/"/>
    <published>2026-05-18T13:35:05.000Z</published>
    <summary>
      <![CDATA[<!--
 **加粗**
 *斜体*
 ***加粗并斜体***
 ~~删除线~~
 ==突出显示==
 `突出显示(推荐)`
 ++下划线++
 ~下标~
 ^上标^
 脚注，参考文献[^1]，然后在文档最下方要添加这个1对应的内容，如：[^1]: My reference.
 图片设置宽度和高度(通过uPic上传后，需要将upic修改过为blog，用于添加水印) ![](https://upic-oss.oss-cn-beijing.aliyuncs.com/blog/innodb_buffer_pool.png =900x600)

 +++ **点击折叠**
 这是被隐藏的内容
 +++
 -->
<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">
<p>MySql知识点介绍:将大数据量的表进行拆分</p>
</li>
<li class="lvl-2">
<p>本文基于<code>mysql-5.7.44</code></p>
</li>
</ul>]]>
    </summary>
    <title>MySql--将大数据量的表进行拆分</title>
    <updated>2026-05-19T03:37:06.950Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/redis/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/tags/redis/"/>
    <category term="redis cluster" scheme="https://blog.hanqunfeng.com/tags/redis-cluster/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">本文介绍如何通过 OpenResty 实现 Nginx + Lua 访问 Redis</li><li class="lvl-2">本文基于<code>redis-7.4.7</code></li><li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li><li class="lvl-2">OpenResty官网：<a href="https://openresty.org/">https://openresty.org/</a></li><li class="lvl-2"><a href="https://www.runoob.com/lua/lua-tutorial.html">Lua语法参考</a></li></ul><span id="more"></span><h2 id="OpenResty-简介">OpenResty 简介</h2><ul class="lvl-0"><li class="lvl-2"><p>OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台，其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。</p></li><li class="lvl-2"><p>OpenResty® 通过汇聚各种设计精良的 Nginx 模块（主要由 OpenResty 团队自主开发），从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样，Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块，快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。</p></li><li class="lvl-2"><p>OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部，充分利用 Nginx 的非阻塞 I/O 模型，不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。</p></li><li class="lvl-2"><p>一句话：OpenResty 就是加载了 Lua 模块的 Nginx。</p></li></ul><h2 id="OpenResty-安装">OpenResty 安装</h2><h3 id="MacOS">MacOS</h3><ul class="lvl-0"><li class="lvl-2"><p><a href="https://openresty.org/cn/download.html">官网参考</a></p></li><li class="lvl-2"><p>MacOS 版本：<code>15.7.3</code></p></li><li class="lvl-2"><p>通过 Homebrew 安装</p></li></ul><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">brew tap openresty/brew</span><br><span class="line">brew install openresty</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>安装时报如下错误，提示找不到<code>GeoIP</code></p></li></ul><blockquote><p>GeoIP 是一个 IP 地址 → 地理位置映射库和模块，核心作用是：根据客户端 IP，解析出国家、城市、经纬度、运营商等地理信息。</p></blockquote><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></pre></td><td class="code"><pre><span class="line">checking <span class="keyword">for</span> GeoIP library ... not found</span><br><span class="line">checking <span class="keyword">for</span> GeoIP library <span class="keyword">in</span> /usr/local/ ... not found</span><br><span class="line">checking <span class="keyword">for</span> GeoIP library <span class="keyword">in</span> /usr/pkg/ ... not found</span><br><span class="line">checking <span class="keyword">for</span> GeoIP library <span class="keyword">in</span> /opt/local/ ... not found</span><br><span class="line">checking <span class="keyword">for</span> GeoIP library <span class="keyword">in</span> /opt/homebrew/ ... not found</span><br><span class="line"></span><br><span class="line">./configure: error: the GeoIP module requires the GeoIP library.</span><br><span class="line">You can either <span class="keyword">do</span> not <span class="built_in">enable</span> the module or install the library.</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>解决办法：这是因为 Homebrew 官方仓库已经不再支持 GeoIP，而是使用 GeoIP2 替代，但 Openresty 暂不支持 GeoIP2，所以这里只能选择不启用 GeoIP 模块。</p></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 编辑 openresty 的安装脚本</span></span><br><span class="line">brew edit openresty/brew/openresty</span><br><span class="line"></span><br><span class="line"><span class="comment"># 将下面的行注释掉后保存并退出</span></span><br><span class="line"><span class="comment"># args &lt;&lt; &quot;--with-http_geoip_module&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 重新安装</span></span><br><span class="line">brew reinstall openresty</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>OpenResty 命令说明</p></li></ul><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><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">openresty -h</span><br><span class="line">nginx version: openresty/1.29.2.1</span><br><span class="line">Usage: nginx [-?hvVtTq] [-s signal] [-p prefix]</span><br><span class="line">             [-e filename] [-c filename] [-g directives]</span><br><span class="line"></span><br><span class="line">Options:</span><br><span class="line">  -?,-h         : this <span class="built_in">help</span></span><br><span class="line">  -v            : show version and <span class="built_in">exit</span></span><br><span class="line">  -V            : show version and configure options <span class="keyword">then</span> <span class="built_in">exit</span></span><br><span class="line">  -t            : <span class="built_in">test</span> configuration and <span class="built_in">exit</span></span><br><span class="line">  -T            : <span class="built_in">test</span> configuration, dump it and <span class="built_in">exit</span></span><br><span class="line">  -q            : suppress non-error messages during configuration testing</span><br><span class="line">  -s signal     : send signal to a master process: stop, quit, reopen, reload</span><br><span class="line">  -p prefix     : <span class="built_in">set</span> prefix path (default: /usr/local/Cellar/openresty/1.29.2.1_1/nginx/)</span><br><span class="line">  -e filename   : <span class="built_in">set</span> error <span class="built_in">log</span> file (default: /usr/local/var/log/nginx/error.log)</span><br><span class="line">  -c filename   : <span class="built_in">set</span> configuration file (default: /usr/local/etc/openresty/nginx.conf)</span><br><span class="line">  -g directives : <span class="built_in">set</span> global directives out of configuration file</span><br></pre></td></tr></table></figure><table><thead><tr><th>参数</th><th>完整写法</th><th>中文含义</th><th>典型使用场景</th><th>示例</th></tr></thead><tbody><tr><td><code>-h</code> / <code>-?</code></td><td><code>openresty -h</code></td><td>显示帮助信息并退出</td><td>快速查看可用参数</td><td><code>openresty -h</code></td></tr><tr><td><code>-v</code></td><td><code>openresty -v</code></td><td>显示版本号</td><td>确认运行版本</td><td><code>openresty -v</code></td></tr><tr><td><code>-V</code></td><td><code>openresty -V</code></td><td>显示版本号 + 编译参数</td><td>排查模块、编译选项、依赖</td><td><code>openresty -V</code></td></tr><tr><td><code>-t</code></td><td><code>openresty -t</code></td><td>校验配置文件合法性并退出</td><td>修改配置后验证语法</td><td><code>openresty -t</code></td></tr><tr><td><code>-T</code></td><td><code>openresty -T</code></td><td>校验配置并打印完整配置内容</td><td>排查 include 文件、调试配置加载顺序</td><td><code>openresty -T</code></td></tr><tr><td><code>-q</code></td><td><code>openresty -t -q</code></td><td>测试配置时只输出错误信息</td><td>CI / 自动化脚本</td><td><code>openresty -t -q</code></td></tr><tr><td><code>-s</code></td><td><code>openresty -s reload</code></td><td>向 master 进程发送控制信号</td><td>服务管理（重载、停止等）</td><td><code>openresty -s reload</code></td></tr><tr><td></td><td><code>stop</code></td><td>立即停止服务（强制）</td><td>紧急停服</td><td><code>openresty -s stop</code></td></tr><tr><td></td><td><code>quit</code></td><td>优雅停止服务（处理完请求后退出）</td><td>平滑下线</td><td><code>openresty -s quit</code></td></tr><tr><td></td><td><code>reload</code></td><td>平滑重载配置</td><td>发布配置变更</td><td><code>openresty -s reload</code></td></tr><tr><td></td><td><code>reopen</code></td><td>重新打开日志文件</td><td>日志切割后使用</td><td><code>openresty -s reopen</code></td></tr><tr><td><code>-p</code></td><td><code>openresty -p /path</code></td><td>指定运行前缀目录（prefix）</td><td>多实例部署、定制目录结构</td><td><code>openresty -p /opt/openresty</code></td></tr><tr><td><code>-e</code></td><td><code>openresty -e file</code></td><td>指定错误日志路径</td><td>临时调试错误日志</td><td><code>openresty -e /tmp/error.log</code></td></tr><tr><td><code>-c</code></td><td><code>openresty -c file</code></td><td>指定配置文件路径</td><td>使用非默认配置启动</td><td><code>openresty -c ./nginx.conf</code></td></tr><tr><td><code>-g</code></td><td><code>openresty -g &quot;daemon off;&quot;</code></td><td>设置全局指令（覆盖配置文件）</td><td>容器化 / 临时调试</td><td><code>openresty -g &quot;daemon off;&quot;</code></td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>安装后的目录</p></li></ul><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">/usr/local/opt/openresty</span><br><span class="line"><span class="comment"># 配置文件目录</span></span><br><span class="line">/usr/local/etc/openresty/</span><br></pre></td></tr></table></figure><h3 id="Linux">Linux</h3><ul class="lvl-0"><li class="lvl-2"><p><a href="https://openresty.org/cn/download.html">官网参考</a></p></li><li class="lvl-2"><p>这里以 <code>Amazon Linux 2023(内核 6.1)</code> 系统为例，从<a href="https://openresty.org/cn/linux-packages.html#amazon-linux">这里</a>找到对应的安装方法</p></li></ul><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="built_in">sudo</span> yum install -y yum-utils</span><br><span class="line"><span class="built_in">sudo</span> yum-config-manager --add-repo https://openresty.org/package/amazon/openresty.repo</span><br><span class="line"><span class="built_in">sudo</span> yum install -y openresty</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>安装后的目录</p></li></ul><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">/usr/local/openresty</span><br><span class="line"><span class="comment"># 配置文件目录</span></span><br><span class="line">/usr/local/openresty/nginx/conf</span><br></pre></td></tr></table></figure><h2 id="一个简单的示例">一个简单的示例</h2><ul class="lvl-0"><li class="lvl-2"><p><code>vim test-nginx.conf</code></p></li></ul><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><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">worker_processes  1;</span><br><span class="line">events &#123;</span><br><span class="line">    worker_connections 1024;</span><br><span class="line">&#125;</span><br><span class="line">http &#123;</span><br><span class="line">    server &#123;</span><br><span class="line">        listen 8080;</span><br><span class="line">        location / &#123;</span><br><span class="line">            default_type text/html;</span><br><span class="line">            <span class="comment"># 这是 OpenResty 提供的 Lua 指令: 在 HTTP 请求的 Content 阶段 执行 Lua 代码，用于生成响应内容。</span></span><br><span class="line">            content_by_lua_block &#123;</span><br><span class="line">                -- ngx: OpenResty 提供的全局对象，封装了 Nginx API，可用于：写响应、读请求、操作 header、访问共享内存、控制状态码</span><br><span class="line">                -- ngx.say(...): 向 HTTP 响应体写入内容，自动追加换行符 \n，可多次调用</span><br><span class="line">                -- 等价于：ngx.print(<span class="string">&quot;&lt;p&gt;hello, world&lt;/p&gt;\n&quot;</span>)</span><br><span class="line">                ngx.say(<span class="string">&quot;&lt;p&gt;hello, world&lt;/p&gt;&quot;</span>)</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><ul class="lvl-0"><li class="lvl-2"><p>实际响应效果，浏览器收到：</p></li></ul><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">HTTP/1.1 200 OK</span><br><span class="line">Content-Type: text/html</span><br><span class="line"></span><br><span class="line">&lt;p&gt;hello, world&lt;/p&gt;</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>启动</p></li></ul><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">openresty -p `<span class="built_in">pwd</span>` -c ./test-nginx.conf</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>访问</p></li></ul><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">curl http://127.0.0.1:8080</span><br><span class="line"><span class="comment"># 结果</span></span><br><span class="line">&lt;p&gt;hello, world&lt;/p&gt;</span><br></pre></td></tr></table></figure><h2 id="Nginx-请求处理阶段划分-与-Lua-指令">Nginx 请求处理阶段划分 与 Lua 指令</h2><ul class="lvl-0"><li class="lvl-2"><p>在 OpenResty 中，一个 HTTP 请求大致经历以下核心阶段：</p></li></ul><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">rewrite  →  access  →  content  →  <span class="built_in">log</span></span><br></pre></td></tr></table></figure><table><thead><tr><th>阶段</th><th>主要职责</th><th>典型指令</th></tr></thead><tbody><tr><td>rewrite</td><td>URL 重写、变量计算、跳转</td><td>rewrite、set、rewrite_by_lua*</td></tr><tr><td>access</td><td>访问控制、鉴权、限流</td><td>allow/deny、auth_request、access_by_lua*</td></tr><tr><td>content</td><td>生成响应内容</td><td>proxy_pass、root、fastcgi_pass、content_by_lua*</td></tr><tr><td>log</td><td>日志记录、统计</td><td>access_log、log_by_lua*</td></tr></tbody></table><blockquote><p>带 <code>*_by_lua</code> 的指令是 OpenResty 在对应阶段注入 Lua 执行逻辑。</p></blockquote><ul class="lvl-0"><li class="lvl-2"><p>Lua 指令与 Nginx 阶段关系对照表</p></li></ul><table><thead><tr><th>Nginx 阶段</th><th>Lua 指令</th><th>是否可读请求</th><th>是否可写响应</th><th>是否推荐输出内容</th><th>典型业务</th></tr></thead><tbody><tr><td>rewrite</td><td>rewrite_by_lua*</td><td>✅</td><td>⚠️（不建议）</td><td>❌</td><td>URL 重写、变量</td></tr><tr><td>access</td><td>access_by_lua*</td><td>✅</td><td>⚠️（仅拒绝时）</td><td>❌</td><td>鉴权、限流</td></tr><tr><td>content</td><td>content_by_lua*</td><td>✅</td><td>✅</td><td>✅</td><td>动态服务</td></tr><tr><td>log</td><td>log_by_lua*</td><td>⚠️</td><td>❌</td><td>❌</td><td>日志、统计</td></tr></tbody></table><blockquote><p><code>*_by_lua* 指令</code> 支持三种形式：</p></blockquote><table><thead><tr><th>写法形式</th><th>语法示例</th><th>含义说明</th><th>Lua 代码来源</th><th>是否支持多行</th><th>是否支持复杂逻辑</th><th>热更新友好度</th><th>推荐使用场景</th><th>注意事项</th></tr></thead><tbody><tr><td><code>_by_lua_block &#123; ... &#125;</code></td><td><code>content_by_lua_block &#123; ngx.say(&quot;hello&quot;) &#125; </code></td><td>在 Nginx 配置文件中直接以内嵌代码块方式书写 Lua</td><td>nginx.conf 内嵌</td><td>✅ 支持</td><td>⚠️ 一般</td><td>⚠️ 中等（需 reload）</td><td>简单逻辑、Demo、调试</td><td>配置文件可读性下降</td></tr><tr><td><code>_by_lua_file /path/a.lua</code></td><td><code>access_by_lua_file lua/auth.lua; </code></td><td>从外部 Lua 文件加载并执行</td><td>独立 Lua 脚本文件</td><td>✅ 支持</td><td>✅ 强</td><td>✅ 高（代码可版本管理）</td><td>生产环境、复杂业务</td><td>路径必须正确</td></tr><tr><td><code>_by_lua &quot;inline lua&quot;</code></td><td><code>content_by_lua &quot;ngx.say('ok')&quot;; </code></td><td>将 Lua 代码作为字符串参数传入</td><td>Nginx 指令字符串</td><td>❌ 不友好</td><td>❌ 弱</td><td>❌ 差</td><td>临时测试、单行逻辑</td><td>转义复杂，难维护</td></tr></tbody></table><blockquote><p>工程实践推荐等级</p></blockquote><table><thead><tr><th>使用方式</th><th>推荐等级</th><th>理由</th></tr></thead><tbody><tr><td><code>_by_lua_file</code></td><td>⭐⭐⭐⭐⭐</td><td>可维护、可测试、可版本管理</td></tr><tr><td><code>_by_lua_block</code></td><td>⭐⭐⭐</td><td>适合简单逻辑</td></tr><tr><td><code>_by_lua &quot;...&quot;</code></td><td>⭐</td><td>仅适合临时验证</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>示例</p></li></ul><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><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">server &#123;</span><br><span class="line">    listen 8080;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 方式1.content_by_lua_block ：lua 内嵌代码块</span></span><br><span class="line">    location /hello-lua &#123;</span><br><span class="line">        default_type <span class="string">&#x27;text/plain&#x27;</span>;</span><br><span class="line">        content_by_lua_block &#123;</span><br><span class="line">        ngx.say(<span class="string">&quot;Hello World! Lua &amp; Nginx .&quot;</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"># 方式2.content_by_lua_file ：lua 脚本文件路径</span></span><br><span class="line">    location /hello-lua-file &#123;</span><br><span class="line">        default_type <span class="string">&#x27;text/html&#x27;</span>;</span><br><span class="line">        content_by_lua_file ./lua/hello.lua;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 方式3.access_by_lua 在请求访问阶段处理用于访问控制 ：lua 字符串</span></span><br><span class="line">    location /hello-lua-access &#123;</span><br><span class="line">        default_type <span class="string">&#x27;text/html&#x27;</span>;</span><br><span class="line">        access_by_lua <span class="string">&#x27;</span></span><br><span class="line"><span class="string">        local message = &quot;403 - Hello World! Lua &amp; Nginx  access_by_lua&quot;</span></span><br><span class="line"><span class="string">        ngx.say(message)</span></span><br><span class="line"><span class="string">        &#x27;</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 方式4.content_by_lua 在内容处理阶段接受请求并输出响应：lua 字符串</span></span><br><span class="line">    location /hello-lua-content &#123;</span><br><span class="line">        default_type <span class="string">&#x27;text/html&#x27;</span>;</span><br><span class="line">        content_by_lua <span class="string">&quot;ngx.print(&#x27;Hello World!&#x27;)&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="如何获取客户端请求数据？">如何获取客户端请求数据？</h2><ul class="lvl-0"><li class="lvl-2"><p>OpenResty 把 HTTP 请求映射为 Lua API，主要分为 5 类</p></li></ul><h3 id="✅-1-获取-URL-Query-参数（GET）">✅ 1. 获取 URL / Query 参数（GET）</h3><ul class="lvl-0"><li class="lvl-2"><p>示例请求</p></li></ul><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">GET /api?user=tom&amp;age=18</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>Lua 获取方式</p></li></ul><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> args = ngx.req.get_uri_args()</span><br><span class="line"><span class="comment">-- args 是一个 table，获取不到参数返回 nil</span></span><br><span class="line"><span class="keyword">local</span> user = args[<span class="string">&quot;user&quot;</span>]</span><br><span class="line"><span class="keyword">local</span> age = args[<span class="string">&quot;age&quot;</span>]</span><br></pre></td></tr></table></figure><h3 id="✅-2-获取-POST-表单参数（application-x-www-form-urlencoded）">✅ 2. 获取 POST 表单参数（application/x-www-form-urlencoded）</h3><ul class="lvl-0"><li class="lvl-2"><p>示例请求</p></li></ul><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">POST /api</span><br><span class="line">Content-Type: application/x-www-form-urlencoded</span><br><span class="line"></span><br><span class="line">user=tom&amp;age=18</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>Lua 获取方式</p></li></ul><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">-- 必须先读取 Body</span></span><br><span class="line">ngx.req.read_body()</span><br><span class="line"><span class="keyword">local</span> args = ngx.req.get_post_args()</span><br><span class="line"></span><br><span class="line"><span class="keyword">local</span> user = args[<span class="string">&quot;user&quot;</span>]</span><br><span class="line"><span class="keyword">local</span> age = args[<span class="string">&quot;age&quot;</span>]</span><br></pre></td></tr></table></figure><h3 id="✅-3-获取-JSON-Body（application-json）">✅ 3. 获取 JSON Body（application/json）</h3><ul class="lvl-0"><li class="lvl-2"><p>示例请求</p></li></ul><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">POST /api</span><br><span class="line">Content-Type: application/json</span><br><span class="line"></span><br><span class="line">&#123;<span class="string">&quot;user&quot;</span>:<span class="string">&quot;tom&quot;</span>,<span class="string">&quot;age&quot;</span>:18&#125;</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>Lua 获取方式</p></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 必须先读取 Body</span></span><br><span class="line">ngx.req.read_body()</span><br><span class="line"><span class="keyword">local</span> body = ngx.req.get_body_data()</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 加载 JSON 库</span></span><br><span class="line"><span class="keyword">local</span> cjson = <span class="built_in">require</span> <span class="string">&quot;cjson.safe&quot;</span></span><br><span class="line"><span class="comment">-- 解析为 JSON 对象</span></span><br><span class="line"><span class="keyword">local</span> data, err = cjson.decode(body)</span><br><span class="line"><span class="keyword">if</span> <span class="keyword">not</span> data <span class="keyword">then</span></span><br><span class="line">   ngx.<span class="built_in">log</span>(ngx.WARN, <span class="string">&quot;json decode failed: &quot;</span>, err)</span><br><span class="line">   <span class="keyword">return</span> ngx.<span class="built_in">exit</span>(<span class="number">400</span>)</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">local</span> user = data.user</span><br><span class="line"><span class="keyword">local</span> age = data.age</span><br></pre></td></tr></table></figure><h3 id="✅-4-获取-HTTP-Headers">✅ 4. 获取 HTTP Headers</h3><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">-- 获取所有 Header</span></span><br><span class="line"><span class="keyword">local</span> headers = ngx.req.get_headers()</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> k, v <span class="keyword">in</span> <span class="built_in">pairs</span>(headers) <span class="keyword">do</span></span><br><span class="line">    ngx.say(k, <span class="string">&quot; = &quot;</span>, v)</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 获取单个 Header</span></span><br><span class="line"><span class="keyword">local</span> ua = ngx.var.http_user_agent</span><br><span class="line"><span class="keyword">local</span> token = ngx.var.http_authorization</span><br></pre></td></tr></table></figure><h3 id="✅-5-获取请求方法、URI、路径等">✅ 5. 获取请求方法、URI、路径等</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 比如请求：curl http://127.0.0.1:8080/api\?name\=zhangsan\&amp;age\=20</span></span><br><span class="line"><span class="comment">-- 请求方法，如 GET，POST</span></span><br><span class="line"><span class="keyword">local</span> method = ngx.req.get_method() <span class="comment">-- GET</span></span><br><span class="line"><span class="comment">-- 请求 URI</span></span><br><span class="line"><span class="keyword">local</span> uri = ngx.var.uri <span class="comment">-- /api</span></span><br><span class="line"><span class="comment">-- QueryString</span></span><br><span class="line"><span class="keyword">local</span> args = ngx.var.args <span class="comment">-- name=zhangsan&amp;age=20</span></span><br><span class="line"><span class="comment">-- 域名或IP</span></span><br><span class="line"><span class="keyword">local</span> host = ngx.var.host <span class="comment">-- 127.0.0.1</span></span><br><span class="line"><span class="comment">-- 端口</span></span><br><span class="line"><span class="keyword">local</span> port = ngx.var.server_port <span class="comment">-- 8080</span></span><br><span class="line"><span class="comment">-- 请求协议</span></span><br><span class="line"><span class="keyword">local</span> scheme = ngx.var.scheme <span class="comment">-- http</span></span><br></pre></td></tr></table></figure><h3 id="总结">总结</h3><table><thead><tr><th>数据类型</th><th>Lua API</th></tr></thead><tbody><tr><td>客户端 IP</td><td><code>ngx.var.remote_addr</code></td></tr><tr><td>请求方法</td><td><code>ngx.req.get_method()</code></td></tr><tr><td>完整 URI</td><td><code>ngx.var.request_uri</code></td></tr><tr><td>Path</td><td><code>ngx.var.uri</code></td></tr><tr><td>QueryString</td><td><code>ngx.var.args</code></td></tr><tr><td>GET 参数</td><td><code>ngx.req.get_uri_args()</code></td></tr><tr><td>POST 表单</td><td><code>ngx.req.get_post_args()</code></td></tr><tr><td>Raw Body</td><td><code>ngx.req.get_body_data()</code></td></tr><tr><td>JSON Body</td><td><code>cjson.decode()</code></td></tr><tr><td>Headers</td><td><code>ngx.req.get_headers()</code></td></tr><tr><td>单个 Header</td><td><code>ngx.var.http_xxx</code></td></tr><tr><td>Cookie</td><td><code>ngx.var.http_cookie</code></td></tr></tbody></table><h2 id="Nginx-Lua-Redis-限流完整示例">Nginx + Lua + Redis 限流完整示例</h2><h3 id="限流设计说明">限流设计说明</h3><ul class="lvl-0"><li class="lvl-2"><p>限流规则</p></li></ul><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">维度：客户端 IP</span><br><span class="line">窗口：60 秒</span><br><span class="line">阈值：10 次</span><br><span class="line">算法：固定窗口计数器(0秒-60秒的每个整分钟内)，实现复杂度极低，性能极高，但有边界突刺问题，可以使用滑动窗口算法(第一次访问后的最近 60 秒内)</span><br><span class="line">存储：Redis</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>Redis Key 设计</p></li></ul><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">rate:&#123;client_ip&#125;:&#123;minute_timestamp&#125;</span><br><span class="line"><span class="comment"># TTL：70 秒（防止残留）</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p><code>vim redis-nginx.conf</code></p></li></ul><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><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 指定 Nginx Worker 进程数量，生产环境：等于 CPU 核心数，建议配置为 auto，自动适配</span></span><br><span class="line">worker_processes auto;</span><br><span class="line"><span class="comment"># 指定错误日志路径，所有错误都会写入该日志文件，包括 Lua ngx.log(ngx.ERR, ...)</span></span><br><span class="line">error_log logs/error.log;</span><br><span class="line"><span class="comment"># 定义事件模型参数</span></span><br><span class="line">events &#123;</span><br><span class="line">    <span class="comment"># 单个 Worker 最大并发连接数</span></span><br><span class="line">    worker_connections  1024;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">http &#123;</span><br><span class="line">    <span class="comment"># Lua 模块加载路径</span></span><br><span class="line">    <span class="comment"># 语法规则</span></span><br><span class="line">    <span class="comment">#  ?.lua 表示模块文件名占位符。</span></span><br><span class="line">    <span class="comment">#  ;; 表示 保留默认路径，否则会覆盖系统默认路径。</span></span><br><span class="line">    lua_package_path <span class="string">&quot;/usr/local/openresty/lualib/?.lua;;&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># ----------------------------</span></span><br><span class="line">    <span class="comment"># Redis 原子限流 Lua 脚本</span></span><br><span class="line">    <span class="comment"># ----------------------------</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 创建一块 共享内存区，名称：redis_scripts，大小：1MB（Worker 之间共享内存）</span></span><br><span class="line">    <span class="comment"># 常用于：缓存 Lua 脚本、Token、计数器、配置信息</span></span><br><span class="line">    lua_shared_dict redis_scripts 1m;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># Worker 初始化阶段加载 Lua，在 每个 Worker 启动时执行一次</span></span><br><span class="line">    <span class="comment"># 适合：加载配置、初始化缓存、预加载脚本、启动定时器</span></span><br><span class="line">    init_worker_by_lua_block &#123;</span><br><span class="line">        -- 将 Lua 脚本加载到 Nginx Worker 内存</span><br><span class="line">        <span class="built_in">local</span> script = [[</span><br><span class="line">            -- 对指定 Key 进行自增</span><br><span class="line">            <span class="built_in">local</span> cnt = redis.call(<span class="string">&quot;INCR&quot;</span>, KEYS[1])</span><br><span class="line">            -- 如果是第一次创建 Key</span><br><span class="line">            <span class="keyword">if</span> cnt == 1 <span class="keyword">then</span></span><br><span class="line">                -- 设置 Key 过期时间(秒)</span><br><span class="line">                redis.call(<span class="string">&quot;EXPIRE&quot;</span>, KEYS[1], ARGV[1])</span><br><span class="line">            end</span><br><span class="line">            -- 返回当前计数</span><br><span class="line">            <span class="built_in">return</span> cnt</span><br><span class="line">        ]]</span><br><span class="line">        -- 将 Lua 脚本存入共享内存，避免每次请求拼接 Lua 脚本字符串，提升性能</span><br><span class="line">        <span class="built_in">local</span> dict = ngx.shared.redis_scripts</span><br><span class="line">        -- Key：rate_limit_lua，Value：Lua 脚本</span><br><span class="line">        dict:<span class="built_in">set</span>(<span class="string">&quot;rate_limit_lua&quot;</span>, script)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    server &#123;</span><br><span class="line">        listen 8080;</span><br><span class="line"></span><br><span class="line">        location /api &#123;</span><br><span class="line">            <span class="comment"># 在 content 阶段执行 Lua，完全由 Lua 生成响应内容</span></span><br><span class="line">            content_by_lua_block &#123;</span><br><span class="line">                -- 加载 OpenResty 官方 Redis 客户端</span><br><span class="line">                <span class="built_in">local</span> redis = require <span class="string">&quot;resty.redis&quot;</span></span><br><span class="line">                -- 创建 Redis 对象</span><br><span class="line">                <span class="built_in">local</span> red = redis:new()</span><br><span class="line">                -- Redis 连接信息</span><br><span class="line">                <span class="built_in">local</span> redis_ip = <span class="string">&quot;127.0.0.1&quot;</span></span><br><span class="line">                <span class="built_in">local</span> redis_port = 6379</span><br><span class="line">                <span class="built_in">local</span> redis_timeout = 500</span><br><span class="line">                <span class="built_in">local</span> redis_user = <span class="string">&quot;admin&quot;</span></span><br><span class="line">                <span class="built_in">local</span> redis_pass = <span class="string">&quot;123456&quot;</span></span><br><span class="line"></span><br><span class="line">                -- 关闭redis连接的工具方法，其实是放入连接池</span><br><span class="line">                <span class="built_in">local</span> <span class="keyword">function</span> close_redis(red)</span><br><span class="line">                    <span class="built_in">local</span> pool_max_idle_time = 10000 -- 连接的空闲时间，单位是毫秒</span><br><span class="line">                    <span class="built_in">local</span> pool_size = 100 --连接池大小</span><br><span class="line">                    -- 将连接放回连接池，后续请求可复用</span><br><span class="line">                    <span class="built_in">local</span> ok, err = red:set_keepalive(pool_max_idle_time, pool_size)</span><br><span class="line">                    <span class="keyword">if</span> not ok <span class="keyword">then</span></span><br><span class="line">                        -- 失败时记录错误日志</span><br><span class="line">                        ngx.log(ngx.ERR, <span class="string">&quot;放入redis连接池失败: &quot;</span>, err)</span><br><span class="line">                    end</span><br><span class="line">                end</span><br><span class="line"></span><br><span class="line">                -- 超时（毫秒）</span><br><span class="line">                red:set_timeout(redis_timeout)</span><br><span class="line"></span><br><span class="line">                -- 建立 Redis 连接，若连接池有空闲连接会复用</span><br><span class="line">                <span class="built_in">local</span> ok, err = red:connect(redis_ip, redis_port)</span><br><span class="line">                <span class="keyword">if</span> not ok <span class="keyword">then</span></span><br><span class="line">                    ngx.log(ngx.ERR, <span class="string">&quot;redis connect failed: &quot;</span>, err)</span><br><span class="line">                    -- 连接失败返回 500 错误码</span><br><span class="line">                    <span class="built_in">return</span> ngx.exit(500)</span><br><span class="line">                end</span><br><span class="line"></span><br><span class="line">                -- ACL 认证（仅新连接）</span><br><span class="line">                -- 判断是否是新连接，避免重复认证浪费性能</span><br><span class="line">                <span class="keyword">if</span> red:get_reused_times() == 0 <span class="keyword">then</span></span><br><span class="line">                    <span class="built_in">local</span> ok, err = red:auth(redis_user, redis_pass)</span><br><span class="line">                    <span class="keyword">if</span> not ok <span class="keyword">then</span></span><br><span class="line">                        ngx.log(ngx.ERR, <span class="string">&quot;redis auth failed: &quot;</span>, err)</span><br><span class="line">                        -- 认证失败返回 500 错误码</span><br><span class="line">                        <span class="built_in">return</span> ngx.exit(500)</span><br><span class="line">                    end</span><br><span class="line">                end</span><br><span class="line"></span><br><span class="line">                -- 客户端 IP，这里 remote_addr 是nginx的内置变量</span><br><span class="line">                <span class="built_in">local</span> client_ip = ngx.var.remote_addr or <span class="string">&quot;unknown&quot;</span></span><br><span class="line"></span><br><span class="line">                -- 当前分钟窗口</span><br><span class="line">                <span class="built_in">local</span> now = ngx.time() -- 获取当前时间戳（秒）</span><br><span class="line">                <span class="built_in">local</span> minute = math.floor(now / 60) -- 转换为分钟窗口编号</span><br><span class="line"></span><br><span class="line">                -- Redis Key: 每个 IP 每分钟一个计数器</span><br><span class="line">                <span class="built_in">local</span> key = <span class="string">&quot;rate:&quot;</span> .. client_ip .. <span class="string">&quot;:&quot;</span> .. minute</span><br><span class="line"></span><br><span class="line">                -- 从共享字典读取 Lua 脚本</span><br><span class="line">                <span class="built_in">local</span> dict = ngx.shared.redis_scripts</span><br><span class="line">                <span class="built_in">local</span> script = dict:get(<span class="string">&quot;rate_limit_lua&quot;</span>)</span><br><span class="line"></span><br><span class="line">                -- 执行 Redis Lua（原子）</span><br><span class="line">                <span class="built_in">local</span> ttl = 70</span><br><span class="line">                <span class="built_in">local</span> cnt, err = red:<span class="built_in">eval</span>(script, 1, key, ttl)</span><br><span class="line">                <span class="keyword">if</span> not cnt <span class="keyword">then</span></span><br><span class="line">                    ngx.log(ngx.ERR, <span class="string">&quot;redis eval failed: &quot;</span>, err)</span><br><span class="line">                    <span class="built_in">return</span> ngx.exit(500)</span><br><span class="line">                end</span><br><span class="line"></span><br><span class="line">                -- 限流判断</span><br><span class="line">                <span class="built_in">local</span> <span class="built_in">limit</span> = 10</span><br><span class="line">                <span class="keyword">if</span> cnt &gt; <span class="built_in">limit</span> <span class="keyword">then</span></span><br><span class="line">                    ngx.status = 429</span><br><span class="line">                    ngx.say(<span class="string">&quot;Too Many Requests, limit=&quot;</span>, <span class="built_in">limit</span>)</span><br><span class="line">                    <span class="built_in">return</span> ngx.exit(429)  -- 返回 HTTP 429（Too Many Requests）。</span><br><span class="line">                end</span><br><span class="line"></span><br><span class="line">                -- 放回连接池</span><br><span class="line">                close_redis(red)</span><br><span class="line"></span><br><span class="line">                -- 正常返回</span><br><span class="line">                ngx.say(<span class="string">&quot;OK, request count=&quot;</span>, cnt)</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><ul class="lvl-0"><li class="lvl-2"><p>启动</p></li></ul><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">openresty -p `<span class="built_in">pwd</span>` -c ./redis-nginx.conf</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>访问</p></li></ul><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><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="keyword">for</span> i <span class="keyword">in</span> &#123;1..15&#125;; <span class="keyword">do</span></span><br><span class="line">  curl http://localhost:8080/api</span><br><span class="line"><span class="keyword">done</span></span><br><span class="line"><span class="comment"># 结果</span></span><br><span class="line">OK, request count=1</span><br><span class="line">OK, request count=2</span><br><span class="line">OK, request count=3</span><br><span class="line">OK, request count=4</span><br><span class="line">OK, request count=5</span><br><span class="line">OK, request count=6</span><br><span class="line">OK, request count=7</span><br><span class="line">OK, request count=8</span><br><span class="line">OK, request count=9</span><br><span class="line">OK, request count=10</span><br><span class="line">Too Many Requests, <span class="built_in">limit</span>=10</span><br><span class="line">Too Many Requests, <span class="built_in">limit</span>=10</span><br><span class="line">Too Many Requests, <span class="built_in">limit</span>=10</span><br><span class="line">Too Many Requests, <span class="built_in">limit</span>=10</span><br><span class="line">Too Many Requests, <span class="built_in">limit</span>=10</span><br></pre></td></tr></table></figure><h3 id="生产环境推荐使用-content-by-lua-file">生产环境推荐使用 <code>content_by_lua_file</code></h3><ul class="lvl-0"><li class="lvl-2"><p>修改 <code>redis-nginx.conf</code></p></li></ul><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><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="comment"># 指定 Nginx Worker 进程数量，生产环境：等于 CPU 核心数，建议配置为 auto，自动适配</span></span><br><span class="line">worker_processes auto;</span><br><span class="line"><span class="comment"># 指定错误日志路径，所有错误都会写入该日志文件，包括 Lua ngx.log(ngx.ERR, ...)</span></span><br><span class="line">error_log logs/error.log;</span><br><span class="line"><span class="comment"># 定义事件模型参数</span></span><br><span class="line">events &#123;</span><br><span class="line">    <span class="comment"># 单个 Worker 最大并发连接数</span></span><br><span class="line">    worker_connections  1024;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">http &#123;</span><br><span class="line">    <span class="comment"># Lua 模块加载路径</span></span><br><span class="line">    <span class="comment"># 语法规则</span></span><br><span class="line">    <span class="comment">#  ?.lua 表示模块文件名占位符。</span></span><br><span class="line">    <span class="comment">#  ;; 表示 保留默认路径，否则会覆盖系统默认路径。</span></span><br><span class="line">    lua_package_path <span class="string">&quot;/usr/local/openresty/lualib/?.lua;;&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># ----------------------------</span></span><br><span class="line">    <span class="comment"># Redis 原子限流 Lua 脚本</span></span><br><span class="line">    <span class="comment"># ----------------------------</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 创建一块 共享内存区，名称：redis_scripts，大小：1MB（Worker 之间共享内存）</span></span><br><span class="line">    <span class="comment"># 常用于：缓存 Lua 脚本、Token、计数器、配置信息</span></span><br><span class="line">    lua_shared_dict redis_scripts 1m;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># Worker 初始化阶段加载 Lua，在 每个 Worker 启动时执行一次</span></span><br><span class="line">    <span class="comment"># 适合：加载配置、初始化缓存、预加载脚本、启动定时器</span></span><br><span class="line">    init_worker_by_lua_block &#123;</span><br><span class="line">        -- 将 Lua 脚本加载到 Nginx Worker 内存</span><br><span class="line">        <span class="built_in">local</span> script = [[</span><br><span class="line">            -- 对指定 Key 进行自增</span><br><span class="line">            <span class="built_in">local</span> cnt = redis.call(<span class="string">&quot;INCR&quot;</span>, KEYS[1])</span><br><span class="line">            -- 如果是第一次创建 Key</span><br><span class="line">            <span class="keyword">if</span> cnt == 1 <span class="keyword">then</span></span><br><span class="line">                -- 设置 Key 过期时间(秒)</span><br><span class="line">                redis.call(<span class="string">&quot;EXPIRE&quot;</span>, KEYS[1], ARGV[1])</span><br><span class="line">            end</span><br><span class="line">            -- 返回当前计数</span><br><span class="line">            <span class="built_in">return</span> cnt</span><br><span class="line">        ]]</span><br><span class="line">        -- 将 Lua 脚本存入共享内存，避免每次请求拼接 Lua 脚本字符串，提升性能</span><br><span class="line">        <span class="built_in">local</span> dict = ngx.shared.redis_scripts</span><br><span class="line">        -- Key：rate_limit_lua，Value：Lua 脚本</span><br><span class="line">        dict:<span class="built_in">set</span>(<span class="string">&quot;rate_limit_lua&quot;</span>, script)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    server &#123;</span><br><span class="line">        listen 8080;</span><br><span class="line"></span><br><span class="line">        location /api &#123;</span><br><span class="line">            <span class="comment"># 关键修改点：改为加载 Lua 文件</span></span><br><span class="line">            content_by_lua_file /Users/hanqf/Desktop/openresty/lua/rate_limit.lua;</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><ul class="lvl-0"><li class="lvl-2"><p><code>rate_limit.lua</code></p></li></ul><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 加载 OpenResty 官方 Redis 客户端</span></span><br><span class="line"><span class="keyword">local</span> redis = <span class="built_in">require</span> <span class="string">&quot;resty.redis&quot;</span></span><br><span class="line"><span class="comment">-- 创建 Redis 对象</span></span><br><span class="line"><span class="keyword">local</span> red = redis:new()</span><br><span class="line"><span class="comment">-- Redis 连接信息</span></span><br><span class="line"><span class="keyword">local</span> redis_ip = <span class="string">&quot;127.0.0.1&quot;</span></span><br><span class="line"><span class="keyword">local</span> redis_port = <span class="number">6379</span></span><br><span class="line"><span class="keyword">local</span> redis_timeout = <span class="number">500</span></span><br><span class="line"><span class="keyword">local</span> redis_user = <span class="string">&quot;admin&quot;</span></span><br><span class="line"><span class="keyword">local</span> redis_pass = <span class="string">&quot;123456&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 关闭redis连接的工具方法，其实是放入连接池</span></span><br><span class="line"><span class="keyword">local</span> <span class="function"><span class="keyword">function</span> <span class="title">close_redis</span><span class="params">(red)</span></span></span><br><span class="line">    <span class="keyword">local</span> pool_max_idle_time = <span class="number">10000</span> <span class="comment">-- 连接的空闲时间，单位是毫秒</span></span><br><span class="line">    <span class="keyword">local</span> pool_size = <span class="number">100</span> <span class="comment">--连接池大小</span></span><br><span class="line">    <span class="comment">-- 将连接放回连接池，后续请求可复用</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:set_keepalive(pool_max_idle_time, pool_size)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        <span class="comment">-- 失败时记录错误日志</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;放入redis连接池失败: &quot;</span>, err)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 超时（毫秒）</span></span><br><span class="line">red:set_timeout(redis_timeout)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 建立 Redis 连接，若连接池有空闲连接会复用</span></span><br><span class="line"><span class="keyword">local</span> ok, err = red:connect(redis_ip, redis_port)</span><br><span class="line"><span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">    ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;redis connect failed: &quot;</span>, err)</span><br><span class="line">    <span class="comment">-- 连接失败返回 500 错误码</span></span><br><span class="line">    <span class="keyword">return</span> ngx.<span class="built_in">exit</span>(<span class="number">500</span>)</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- ACL 认证（仅新连接）</span></span><br><span class="line"><span class="comment">-- 判断是否是新连接，避免重复认证浪费性能</span></span><br><span class="line"><span class="keyword">if</span> red:get_reused_times() == <span class="number">0</span> <span class="keyword">then</span></span><br><span class="line">    <span class="keyword">local</span> ok, err = red:auth(redis_user, redis_pass)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> ok <span class="keyword">then</span></span><br><span class="line">        ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;redis auth failed: &quot;</span>, err)</span><br><span class="line">        <span class="comment">-- 认证失败返回 500 错误码</span></span><br><span class="line">        <span class="keyword">return</span> ngx.<span class="built_in">exit</span>(<span class="number">500</span>)</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 客户端 IP</span></span><br><span class="line"><span class="keyword">local</span> client_ip = ngx.var.remote_addr <span class="keyword">or</span> <span class="string">&quot;unknown&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 当前分钟窗口</span></span><br><span class="line"><span class="keyword">local</span> now = ngx.<span class="built_in">time</span>() <span class="comment">-- 获取当前时间戳（秒）</span></span><br><span class="line"><span class="keyword">local</span> minute = <span class="built_in">math</span>.<span class="built_in">floor</span>(now / <span class="number">60</span>) <span class="comment">-- 转换为分钟窗口编号</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- Redis Key: 每个 IP 每分钟一个计数器</span></span><br><span class="line"><span class="keyword">local</span> key = <span class="string">&quot;rate:&quot;</span> .. client_ip .. <span class="string">&quot;:&quot;</span> .. minute</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 从共享字典读取 Lua 脚本</span></span><br><span class="line"><span class="keyword">local</span> dict = ngx.shared.redis_scripts</span><br><span class="line"><span class="keyword">local</span> script = dict:get(<span class="string">&quot;rate_limit_lua&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 执行 Redis Lua（原子）</span></span><br><span class="line"><span class="keyword">local</span> ttl = <span class="number">70</span></span><br><span class="line"><span class="keyword">local</span> cnt, err = red:eval(script, <span class="number">1</span>, key, ttl)</span><br><span class="line"><span class="keyword">if</span> <span class="keyword">not</span> cnt <span class="keyword">then</span></span><br><span class="line">    ngx.<span class="built_in">log</span>(ngx.ERR, <span class="string">&quot;redis eval failed: &quot;</span>, err)</span><br><span class="line">    <span class="keyword">return</span> ngx.<span class="built_in">exit</span>(<span class="number">500</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">local</span> limit = <span class="number">10</span></span><br><span class="line"><span class="keyword">if</span> cnt &gt; limit <span class="keyword">then</span></span><br><span class="line">    ngx.<span class="built_in">status</span> = <span class="number">429</span></span><br><span class="line">    ngx.say(<span class="string">&quot;Too Many Requests, limit=&quot;</span>, limit)</span><br><span class="line">    <span class="keyword">return</span> ngx.<span class="built_in">exit</span>(<span class="number">429</span>)  <span class="comment">-- 返回 HTTP 429（Too Many Requests）。</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">close_redis(red)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 正常返回</span></span><br><span class="line">ngx.say(<span class="string">&quot;OK, request count=&quot;</span>, cnt)</span><br></pre></td></tr></table></figure><h3 id="改用-access-by-lua-file">改用 <code>access_by_lua_file</code></h3><ul class="lvl-0"><li class="lvl-2"><p>前面的配置所有的响应都是由 lua 处理的，但是实际上，用户的请求被限流器放行后应该将请求下发到后端真实的服务，而不是通过 Lua 返回一个状态码或字符串。</p></li><li class="lvl-2"><p>生产级网关设计的标准做法: ✅ 凡是“鉴权/限流/风控/灰度/路由决策”逻辑，都应该放在 access 阶段，而不是 content 阶段。</p></li><li class="lvl-2"><p>修改 <code>redis-nginx.conf</code></p></li></ul><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><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 指定 Nginx Worker 进程数量，生产环境：等于 CPU 核心数，建议配置为 auto，自动适配</span></span><br><span class="line">worker_processes auto;</span><br><span class="line"><span class="comment"># 指定错误日志路径，所有错误都会写入该日志文件，包括 Lua ngx.log(ngx.ERR, ...)</span></span><br><span class="line">error_log logs/error.log;</span><br><span class="line"><span class="comment"># 定义事件模型参数</span></span><br><span class="line">events &#123;</span><br><span class="line">    <span class="comment"># 单个 Worker 最大并发连接数</span></span><br><span class="line">    worker_connections  1024;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">http &#123;</span><br><span class="line">    <span class="comment"># Lua 模块加载路径</span></span><br><span class="line">    <span class="comment"># 语法规则</span></span><br><span class="line">    <span class="comment">#  ?.lua 表示模块文件名占位符。</span></span><br><span class="line">    <span class="comment">#  ;; 表示 保留默认路径，否则会覆盖系统默认路径。</span></span><br><span class="line">    lua_package_path <span class="string">&quot;/usr/local/openresty/lualib/?.lua;;&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># ----------------------------</span></span><br><span class="line">    <span class="comment"># Redis 原子限流 Lua 脚本</span></span><br><span class="line">    <span class="comment"># ----------------------------</span></span><br><span class="line"></span><br><span class="line">    <span class="comment"># 创建一块 共享内存区，名称：redis_scripts，大小：1MB（Worker 之间共享内存）</span></span><br><span class="line">    <span class="comment"># 常用于：缓存 Lua 脚本、Token、计数器、配置信息</span></span><br><span class="line">    lua_shared_dict redis_scripts 1m;</span><br><span class="line"></span><br><span class="line">    <span class="comment"># Worker 初始化阶段加载 Lua，在 每个 Worker 启动时执行一次</span></span><br><span class="line">    <span class="comment"># 适合：加载配置、初始化缓存、预加载脚本、启动定时器</span></span><br><span class="line">    init_worker_by_lua_block &#123;</span><br><span class="line">        -- 将 Lua 脚本加载到 Nginx Worker 内存</span><br><span class="line">        <span class="built_in">local</span> script = [[</span><br><span class="line">            -- 对指定 Key 进行自增</span><br><span class="line">            <span class="built_in">local</span> cnt = redis.call(<span class="string">&quot;INCR&quot;</span>, KEYS[1])</span><br><span class="line">            -- 如果是第一次创建 Key</span><br><span class="line">            <span class="keyword">if</span> cnt == 1 <span class="keyword">then</span></span><br><span class="line">                -- 设置 Key 过期时间(秒)</span><br><span class="line">                redis.call(<span class="string">&quot;EXPIRE&quot;</span>, KEYS[1], ARGV[1])</span><br><span class="line">            end</span><br><span class="line">            -- 返回当前计数</span><br><span class="line">            <span class="built_in">return</span> cnt</span><br><span class="line">        ]]</span><br><span class="line">        -- 将 Lua 脚本存入共享内存，避免每次请求拼接 Lua 脚本字符串，提升性能</span><br><span class="line">        <span class="built_in">local</span> dict = ngx.shared.redis_scripts</span><br><span class="line">        -- Key：rate_limit_lua，Value：Lua 脚本</span><br><span class="line">        dict:<span class="built_in">set</span>(<span class="string">&quot;rate_limit_lua&quot;</span>, script)</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">    upstream backend &#123;</span><br><span class="line">        server 127.0.0.1:9000;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    server &#123;</span><br><span class="line">        listen 8080;</span><br><span class="line"></span><br><span class="line">        location /api &#123;</span><br><span class="line">             <span class="comment"># 限流在 access 阶段执行</span></span><br><span class="line">            access_by_lua_file /Users/hanqf/Desktop/openresty/lua/rate_limit.lua;</span><br><span class="line"></span><br><span class="line">            <span class="comment"># 通过后转发给后端服务</span></span><br><span class="line">            proxy_pass http://backend;</span><br><span class="line"></span><br><span class="line">            proxy_set_header Host <span class="variable">$host</span>;</span><br><span class="line">            proxy_set_header X-Real-IP <span class="variable">$remote_addr</span>;</span><br><span class="line">            proxy_set_header X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</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><ul class="lvl-0"><li class="lvl-2"><p>✅ rate_limit.lua 修改</p></li></ul><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 将最后的</span></span><br><span class="line"><span class="comment">-- ngx.say(&quot;OK, request count=&quot;, cnt)</span></span><br><span class="line"><span class="comment">-- 替换为</span></span><br><span class="line"><span class="keyword">return</span> <span class="comment">-- 放行：什么都不做</span></span><br></pre></td></tr></table></figure><h2 id="后记">后记</h2><h3 id="应该如何编写滑动窗口限流脚本呢？">应该如何编写滑动窗口限流脚本呢？</h3><ul class="lvl-0"><li class="lvl-2"><p>目标：</p></li></ul><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">限制：最近 60 秒内 ≤ 10 次请求</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>核心思想：</p><ul class="lvl-2"><li class="lvl-5">每次请求记录当前时间戳</li><li class="lvl-5">删除窗口外的旧记录</li><li class="lvl-5">统计窗口内请求数量</li><li class="lvl-5">超过阈值拒绝</li></ul></li><li class="lvl-2"><p>Redis 数据结构使用：<code>ZSET</code></p></li></ul><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"># KEY:rate:IP，SCORE:时间戳，MEMBER:任意唯一值，可以依旧使用时间戳，或者 时间戳_随机数，防止重复</span></span><br><span class="line">ZADD rate:192.168.1.10 1620000000 1620000000</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>具体的lua实现方法在 <a href="/2026/01/11/redis7-command-05-script-function/" title="Redis 命令详解：Scripting &#x2F; Functions 命令">Redis 命令详解：Scripting &#x2F; Functions 命令</a> 中有详细介绍。</p></li></ul>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/01/15/redis8-OpenResty-nginx/</id>
    <link href="https://blog.hanqunfeng.com/2026/01/15/redis8-OpenResty-nginx/"/>
    <published>2026-01-15T13:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">本文介绍如何通过 OpenResty 实现 Nginx + Lua 访问 Redis</li>
<li class="lvl-2">本文基于<code>redis-7.4.7</code></li>
<li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li>
<li class="lvl-2">OpenResty官网：<a href="https://openresty.org/">https://openresty.org/</a></li>
<li class="lvl-2"><a href="https://www.runoob.com/lua/lua-tutorial.html">Lua语法参考</a></li>
</ul>]]>
    </summary>
    <title>OpenResty -- Nginx + Lua 访问 Redis</title>
    <updated>2026-01-16T09:28:51.229Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/redis/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/tags/redis/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">本文介绍 Redis8 新增的数据类型 Vector Set</li><li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li></ul><span id="more"></span><h2 id="Vector-Set-命令说明">Vector Set 命令说明</h2><h3 id="一、基础管理类">一、基础管理类</h3><table><thead><tr><th>命令</th><th>作用</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td><strong>VADD</strong></td><td>向向量集合中添加一个向量元素</td><td><code>VADD key VALUES num vector element</code></td><td><code>key</code>：向量集合名<br><code>vector</code>：向量数据（浮点数组）<br><code>element</code>：元素标识</td><td><code>VADD user:embeddings VALUES 3 0.1 0.2 0.3 u1</code></td></tr><tr><td><strong>VREM</strong></td><td>从向量集合中删除一个或多个元素</td><td><code>VREM key element</code></td><td><code>key</code>：向量集合<br><code>element</code>：元素标识</td><td><code>VREM user:embeddings u1</code></td></tr><tr><td><strong>VCARD</strong></td><td>返回向量集合中的元素数量</td><td><code>VCARD key</code></td><td><code>key</code>：向量集合</td><td><code>VCARD user:embeddings</code></td></tr><tr><td><strong>VDIM</strong></td><td>获取向量集合的维度</td><td><code>VDIM key</code></td><td><code>key</code>：向量集合</td><td><code>VDIM user:embeddings</code></td></tr><tr><td><strong>VINFO</strong></td><td>查看向量集合的元信息</td><td><code>VINFO key</code></td><td><code>key</code>：向量集合</td><td><code>VINFO user:embeddings</code></td></tr></tbody></table><h4 id="VADD-命令参数详解">VADD 命令参数详解</h4><ul class="lvl-0"><li class="lvl-2"><p>语法</p></li></ul><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">VADD key</span><br><span class="line">     [REDUCE dim]</span><br><span class="line">     (FP32 | VALUES num) vector</span><br><span class="line">     element</span><br><span class="line">     [CAS]</span><br><span class="line">     [NOQUANT | Q8 | BIN]</span><br><span class="line">     [EF build-exploration-factor]</span><br><span class="line">     [SETATTR attributes]</span><br><span class="line">     [M numlinks]</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>必需参数</p></li></ul><table><thead><tr><th>参数</th><th>参数类型</th><th>作用</th><th>语法 / 格式</th><th>说明</th></tr></thead><tbody><tr><td><code>key</code></td><td>String</td><td>向量集合名称</td><td><code>VADD key ...</code></td><td>Redis 中用于存储向量集合的键名。如果不存在会自动创建集合。</td></tr><tr><td><code>FP32 vector</code></td><td>Binary</td><td>直接传入二进制浮点向量</td><td><code>FP32 &lt;binary&gt;</code></td><td>以 <strong>小端序</strong>编码的 32 位浮点数组，适合客户端直接传二进制向量数据（高性能、低序列化开销）。</td></tr><tr><td><code>VALUES num vector</code></td><td>Numeric List</td><td>以数值方式传入向量</td><td><code>VALUES &lt;num&gt; &lt;v1&gt; &lt;v2&gt; ... &lt;vN&gt;</code></td><td><code>num</code> 为向量维度，后面必须跟随 <code>num</code> 个浮点数。适合在 CLI 或调试场景使用。</td></tr><tr><td><code>element</code></td><td>String</td><td>向量元素 ID</td><td><code>&lt;element&gt;</code></td><td>向量在集合中的唯一标识符，可理解为向量的主键或业务 ID。</td></tr></tbody></table><blockquote><p>重点说明：客户端实现时推荐直接传 FP32 二进制向量，避免浮点字符串解析开销。</p></blockquote><ul class="lvl-0"><li class="lvl-2"><p>可选参数</p></li></ul><table><thead><tr><th>参数</th><th>参数类型</th><th>作用</th><th>语法约束</th><th>说明</th></tr></thead><tbody><tr><td><code>REDUCE dim</code></td><td>Integer</td><td>向量降维</td><td><strong>必须紧跟在 key 后面</strong></td><td>使用随机投影算法将原始向量降维到 <code>dim</code> 维，降低存储与计算成本。<br>如果原始向量维度 &gt; dim → 是“降维”<br>如果原始向量维度 &lt; dim → 是“升维 / 填充映射”</td></tr><tr><td><code>CAS</code></td><td>Flag</td><td>异步构建索引</td><td>可放在 element 后</td><td>采用 Check-And-Set 风格：主线程快速返回，后台异步完成候选集构建，提高写入吞吐。</td></tr><tr><td><code>NOQUANT</code></td><td>Flag</td><td>禁用量化</td><td>仅首次创建集合时生效</td><td>不使用默认 int8 量化，保留原始浮点精度，内存占用更高。</td></tr><tr><td><code>Q8</code></td><td>Flag</td><td>启用 int8 量化（默认）</td><td>仅首次创建集合时生效</td><td>使用有符号 8 位量化，在精度、性能和内存之间取得平衡。</td></tr><tr><td><code>BIN</code></td><td>Flag</td><td>二进制量化</td><td>仅首次创建集合时生效</td><td>内存占用最小、速度最快，但相似度召回率可能下降。</td></tr><tr><td><code>EF build-exploration-factor</code></td><td>Integer</td><td>构图探索因子</td><td>可放在 element 后</td><td>HNSW 构图阶段的搜索宽度参数，默认约 200，值越大构图质量越高，但写入成本增加。</td></tr><tr><td><code>SETATTR attributes</code></td><td>JSON</td><td>设置向量属性</td><td>可放在 element 后</td><td>给元素绑定 JSON 属性，等价于调用 <code>VSETATTR</code>。</td></tr><tr><td><code>M numlinks</code></td><td>Integer</td><td>HNSW 最大连接数</td><td>可放在 element 后</td><td>每个节点允许的最大邻居数，默认 16。值越大索引更稠密，查询质量更高，但内存和写入成本上升。</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>示例：使用 VALUES</p></li></ul><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">VADD my_vectors VALUES 3 0.1 1.2 0.5 image123</span><br><span class="line"><span class="comment"># 解释：</span></span><br><span class="line"><span class="comment">#    将向量 [0.1, 1.2, 0.5] 添加到键 my_vectors 对应的向量结构中。</span></span><br><span class="line"><span class="comment">#    元素名为 image123。</span></span><br><span class="line"><span class="comment">#    如果该键不存在，则创建新向量集。</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例：使用降维和量化选项</p></li></ul><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><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></pre></td><td class="code"><pre><span class="line">VADD my_vectors REDUCE 50 VALUES 5 0.02 0.89 0.77 0.56 0.33 pic_001 Q8 EF 300 M 32</span><br><span class="line"><span class="comment"># 解释：</span></span><br><span class="line"><span class="comment">#    对向量 REDUCE 到 50 维，这里原始数据维度小，实际上是升维</span></span><br><span class="line"><span class="comment">#    向量为 5 维浮点数值。</span></span><br><span class="line"><span class="comment">#    使用默认的带符号 8 位量化（Q8）。</span></span><br><span class="line"><span class="comment">#    设置 HNSW 图探索因子为 300。</span></span><br><span class="line"><span class="comment">#    每节点连接数上限为 32。</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 获取向量维度</span></span><br><span class="line">127.0.0.1:6379&gt; VDIM my_vectors</span><br><span class="line">(<span class="built_in">integer</span>) 50</span><br><span class="line"><span class="comment"># 获取向量信息，输出内容见下表中的说明</span></span><br><span class="line">127.0.0.1:6379&gt; VINFO my_vectors</span><br><span class="line"> 1) quant-type</span><br><span class="line"> 2) int8</span><br><span class="line"> 3) hnsw-m</span><br><span class="line"> 4) (<span class="built_in">integer</span>) 32</span><br><span class="line"> 5) vector-dim</span><br><span class="line"> 6) (<span class="built_in">integer</span>) 50</span><br><span class="line"> 7) projection-input-dim</span><br><span class="line"> 8) (<span class="built_in">integer</span>) 5</span><br><span class="line"> 9) size</span><br><span class="line">10) (<span class="built_in">integer</span>) 1</span><br><span class="line">11) max-level</span><br><span class="line">12) (<span class="built_in">integer</span>) 0</span><br><span class="line">13) attributes-count</span><br><span class="line">14) (<span class="built_in">integer</span>) 0</span><br><span class="line">15) vset-uid</span><br><span class="line">16) (<span class="built_in">integer</span>) 0</span><br><span class="line">17) hnsw-max-node-uid</span><br><span class="line">18) (<span class="built_in">integer</span>) 1</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>VINFO 输出字段说明表</p></li></ul><table><thead><tr><th>字段名</th><th>当前值</th><th>类型</th><th>含义说明</th><th>工程解读 / 使用价值</th></tr></thead><tbody><tr><td><code>quant-type</code></td><td><code>int8</code></td><td>String</td><td>向量量化方式</td><td>当前使用 <strong>Q8（8位有符号整数量化）</strong>，在精度、性能、内存之间平衡。</td></tr><tr><td><code>hnsw-m</code></td><td><code>32</code></td><td>Integer</td><td>HNSW 每节点最大连接数（M 参数）</td><td>图结构较稠密，召回率高，但内存占用和写入成本上升。</td></tr><tr><td><code>vector-dim</code></td><td><code>50</code></td><td>Integer</td><td><strong>索引内部真实使用的向量维度</strong></td><td>REDUCE 后的目标维度，VSIM / VRANGE 查询均基于此维度计算。</td></tr><tr><td><code>projection-input-dim</code></td><td><code>5</code></td><td>Integer</td><td>投影前输入向量的原始维度</td><td>说明你输入的是 <strong>5维向量</strong>，然后被映射到 50 维。</td></tr><tr><td><code>size</code></td><td><code>1</code></td><td>Integer</td><td>当前向量元素数量</td><td>集合中仅有 1 条向量数据（item42）。</td></tr><tr><td><code>max-level</code></td><td><code>0</code></td><td>Integer</td><td>HNSW 当前最大层级</td><td>数据量太小，仅构建了 0 层（尚未形成多层索引结构）。</td></tr><tr><td><code>attributes-count</code></td><td><code>0</code></td><td>Integer</td><td>当前已存储的属性对象数量</td><td>说明 SETATTR 没有成功写入或尚未设置属性。</td></tr><tr><td><code>vset-uid</code></td><td><code>0</code></td><td>Integer</td><td>向量集合内部唯一 ID</td><td>内部调试字段，对业务无直接影响。</td></tr><tr><td><code>hnsw-max-node-uid</code></td><td><code>1</code></td><td>Integer</td><td>HNSW 当前最大节点 ID</td><td>当前仅存在 1 个节点。</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>示例：添加带属性的向量</p></li></ul><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">VADD my_vectors VALUES 4 0.15 0.26 0.47 0.88 item42 SETATTR <span class="string">&quot;&#123;\&quot;type\&quot;:\&quot;product\&quot;,\&quot;price\&quot;:29.99&#125;&quot;</span></span><br><span class="line"><span class="comment"># 解释：</span></span><br><span class="line">   <span class="comment"># 插入 4 维向量并给 item42 设置 JSON 属性 type 和 price。</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 获取向量属性</span></span><br><span class="line">127.0.0.1:6379&gt; VGETATTR my_vectors item42</span><br><span class="line"><span class="string">&quot;&#123;\&quot;type\&quot;:\&quot;product\&quot;,\&quot;price\&quot;:29.99&#125;&quot;</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例：插入多条数据</p></li></ul><blockquote><p>只有<code>维度</code>、<code>quant-type</code> 和 <code>hnsw-m</code>都相同的数据才会被加入同一个 VectorSet<br>VectorSet 在第一次创建时就固定了相关的属性</p></blockquote><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><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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379&gt; VADD my_vectors VALUES 4 0.15 0.26 0.47 0.88 item42 SETATTR <span class="string">&quot;&#123;\&quot;type\&quot;:\&quot;product\&quot;,\&quot;price\&quot;:29.99&#125;&quot;</span></span><br><span class="line">(<span class="built_in">integer</span>) 1</span><br><span class="line">127.0.0.1:6379&gt; VADD my_vectors VALUES 4 0.25 0.20 0.57 0.98 item43</span><br><span class="line">127.0.0.1:6379&gt; VADD my_vectors VALUES 4 0.25 0.20 0.57 0.98 item44 Q8 EF 300 M 16</span><br><span class="line">(<span class="built_in">integer</span>) 1</span><br><span class="line">127.0.0.1:6379&gt; VADD my_vectors VALUES 4 0.25 0.20 0.57 0.98 item44 Q8 EF 300 M 16</span><br><span class="line">(<span class="built_in">integer</span>) 0  <span class="comment"># 添加重复元素标识，返回 0</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 获取向量数量</span></span><br><span class="line">127.0.0.1:6379&gt; VCARD my_vectors</span><br><span class="line">(<span class="built_in">integer</span>) 3</span><br><span class="line"><span class="comment"># 获取向量信息</span></span><br><span class="line">127.0.0.1:6379&gt; VINFO my_vectors</span><br><span class="line"> 1) quant-type</span><br><span class="line"> 2) int8</span><br><span class="line"> 3) hnsw-m</span><br><span class="line"> 4) (<span class="built_in">integer</span>) 16</span><br><span class="line"> 5) vector-dim</span><br><span class="line"> 6) (<span class="built_in">integer</span>) 4</span><br><span class="line"> 7) projection-input-dim</span><br><span class="line"> 8) (<span class="built_in">integer</span>) 0</span><br><span class="line"> 9) size</span><br><span class="line">10) (<span class="built_in">integer</span>) 2</span><br><span class="line">11) max-level</span><br><span class="line">12) (<span class="built_in">integer</span>) 0</span><br><span class="line">13) attributes-count</span><br><span class="line">14) (<span class="built_in">integer</span>) 2</span><br><span class="line">15) vset-uid</span><br><span class="line">16) (<span class="built_in">integer</span>) 2</span><br><span class="line">17) hnsw-max-node-uid</span><br><span class="line">18) (<span class="built_in">integer</span>) 2</span><br></pre></td></tr></table></figure><h3 id="二、向量元素访问与判断">二、向量元素访问与判断</h3><table><thead><tr><th>命令</th><th>作用</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td><strong>VISMEMBER</strong></td><td>判断元素是否存在于向量集合中</td><td><code>VISMEMBER key element</code></td><td><code>key</code>：向量集合<br><code>element</code>：元素标识</td><td><code>VISMEMBER user:embeddings u1</code></td></tr><tr><td><strong>VRANDMEMBER</strong></td><td>随机返回一个或多个向量元素</td><td><code>VRANDMEMBER key [count]</code></td><td><code>count</code>：返回数量，默认为1（可选）</td><td><code>VRANDMEMBER user:embeddings 2</code></td></tr><tr><td><strong>VRANGE</strong></td><td>返回向量集合中指定范围的元素（按内部顺序）</td><td><code>VRANGE key start stop [count]</code></td><td><code>start</code> / <code>stop</code>：说明见下表</td><td><code>VRANGE user:embeddings - + -1</code></td></tr></tbody></table><h4 id="VRANGE">VRANGE</h4><ul class="lvl-0"><li class="lvl-2"><p>参数说明</p></li></ul><table><thead><tr><th>参数</th><th>类型</th><th>是否必须</th><th>描述</th></tr></thead><tbody><tr><td><code>key</code></td><td>key</td><td>是</td><td>向量集合的键名</td></tr><tr><td><code>start</code></td><td>string</td><td>是</td><td>范围起始元素（字典序）<br>可用：<code>[</code> 前缀表示包含，<code>(</code> 前缀表示排除，<code>-</code> 表示最小元素</td></tr><tr><td><code>end</code></td><td>string</td><td>是</td><td>范围结束元素（字典序）<br>可用：<code>[</code> 前缀表示包含，<code>(</code> 前缀表示排除，<code>+</code> 表示最大元素</td></tr><tr><td><code>count</code></td><td>integer</td><td>否</td><td>最多返回元素数量<br>若为负数，则返回范围内所有匹配元素（注意可能阻塞）</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>参数举例说明</p></li></ul><table><thead><tr><th>参数值</th><th>说明</th></tr></thead><tbody><tr><td><code>-</code></td><td>最小元素（等同 open-ended 从头开始）</td></tr><tr><td><code>+</code></td><td>最大元素（等同 open-ended 到尾结束）</td></tr><tr><td><code>[Redis</code></td><td>从字典序 ≥ <code>&quot;Redis&quot;</code> 的元素开始（包含 <code>&quot;Redis&quot;</code>）</td></tr><tr><td><code>(a7</code></td><td>从字典序 &gt; <code>&quot;a7&quot;</code> 的元素开始（不包含 <code>&quot;a7&quot;</code>）</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>示例 1：返回从 “Redis”（包含）开始的前 10 个元素</p></li></ul><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">VRANGE mykey [Redis + 10</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例 2：分段迭代所有元素</p></li></ul><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">VRANGE mykey - + 10</span><br><span class="line"><span class="comment"># 输出：从最小到最大按每次 10 个元素迭代遍历（客户端可用结果接续下一段）。</span></span><br><span class="line"><span class="comment"># 比如输出最后一个元素是 &quot;Redis&quot;，则下一次迭代从 &quot;Redis&quot; 后面开始。</span></span><br><span class="line">VRANGE mykey (Redis + 10</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例 3：返回指定范围所有元素（无数量上限）</p></li></ul><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">VRANGE mykey - + -1</span><br><span class="line"><span class="comment"># 输出：返回整个集合所有元素。注意：若集合很大可能阻塞 Redis。</span></span><br></pre></td></tr></table></figure><h3 id="三、向量属性（Metadata-Attributes）管理">三、向量属性（Metadata / Attributes）管理</h3><ul class="lvl-0"><li class="lvl-2"><p>用于给向量元素绑定结构化元数据（如标签、业务字段）</p></li></ul><table><thead><tr><th>命令</th><th>作用</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td><strong>VSETATTR</strong></td><td>为向量元素设置属性</td><td><code>VSETATTR key element &quot;{ JSON obj }&quot;</code></td><td>JSON obj</td><td>设置元数据：<code>VSETATTR key element &quot;{\&quot;type\&quot;: \&quot;fruit\&quot;, \&quot;color\&quot;: \&quot;red\&quot;}&quot;</code> <br> 清除元数据：<code>VSETATTR key element &quot;&quot;</code></td></tr><tr><td><strong>VGETATTR</strong></td><td>获取向量元素的属性</td><td><code>VGETATTR key element</code></td><td></td><td><code>VGETATTR user:embeddings u1</code></td></tr></tbody></table><h3 id="四、向量相似度与检索类（核心）">四、向量相似度与检索类（核心）</h3><table><thead><tr><th>命令</th><th>作用</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td><strong>VSIM</strong></td><td>基于向量相似度进行近邻搜索（KNN）</td><td>详见下面的说明</td><td></td><td><code>VSIM word_embeddings ELE apple</code></td></tr><tr><td><strong>VLINKS</strong></td><td>查看向量之间的近邻链接关系（图结构）</td><td><code>VLINKS key element [WITHSCORES]</code></td><td><code>element</code>：向量元素 ID</td><td><code>VLINKS user:embeddings u1</code></td></tr></tbody></table><h4 id="VSIM">VSIM</h4><ul class="lvl-0"><li class="lvl-2"><p>VSIM 用于在 向量集合（vector set） 中执行 相似度搜索，返回与指定参考向量或已存在元素向量 最相似 的元素列表。可以进行近似（默认 HNSW 索引）或精确（TRUTH）查询。</p></li><li class="lvl-2"><p>语法</p></li></ul><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">VSIM key (ELE | FP32 | VALUES num) (vector | element)</span><br><span class="line">   [WITHSCORES] [WITHATTRIBS] [COUNT num]</span><br><span class="line">   [EPSILON delta] [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort]</span><br><span class="line">   [TRUTH] [NOTHREAD]</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>参数说明</p></li></ul><table><thead><tr><th>参数 / 选项</th><th>类型</th><th>是否必需</th><th>描述</th></tr></thead><tbody><tr><td><code>key</code></td><td>key</td><td>是</td><td>向量集合的键名</td></tr><tr><td><code>ELE</code></td><td>literal with element name</td><td>是（三者不能同时出现）</td><td>使用集合中已有的元素名称作为查询向量</td></tr><tr><td><code>FP32</code></td><td>literal</td><td>是（三者不能同时出现）</td><td>使用二进制 float32 格式提供查询向量</td></tr><tr><td><code>VALUES num</code></td><td>literal with integer</td><td>是（三者不能同时出现）</td><td>使用后续 <code>num</code> 个字符串 float 值提供查询向量</td></tr><tr><td><code>vector</code> / <code>element</code></td><td>vector values or element name</td><td>是</td><td>查询向量本身（或元素名）</td></tr><tr><td><code>WITHSCORES</code></td><td>flag</td><td>否</td><td>返回每个匹配项的相似度分数</td></tr><tr><td><code>WITHATTRIBS</code></td><td>flag</td><td>否</td><td>返回每个匹配项关联的 JSON 属性（如有）</td></tr><tr><td><code>COUNT num</code></td><td>integer</td><td>否</td><td>限制返回的相似项数量</td></tr><tr><td><code>EPSILON delta</code></td><td>float</td><td>否</td><td>过滤出距离不大于 delta 的结果（score ≥ 1−delta）</td></tr><tr><td><code>EF search-exploration-factor</code></td><td>integer</td><td>否</td><td>调整 HNSW 搜索探索因子（值越高搜索更深、更准确但更慢）。</td></tr><tr><td><code>FILTER expression</code></td><td>string</td><td>否</td><td>对属性进行过滤表达式约束（仅返回满足的元素）</td></tr><tr><td><code>FILTER-EF max-filtering-effort</code></td><td>integer</td><td>否</td><td>限制 FILTER 表达式的评估尝试次数</td></tr><tr><td><code>TRUTH</code></td><td>flag</td><td>否</td><td>强制精确线性扫描（不使用图索引）</td></tr><tr><td><code>NOTHREAD</code></td><td>flag</td><td>否</td><td>在主线程执行搜索而非后台线程</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>参数/选项说明细节</p></li></ul><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><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">输入向量方式</span><br><span class="line">   ELE elementName：以集合中已有元素的向量作为查询参考。</span><br><span class="line">   FP32：提供 little-endian 编码的二进制浮点向量。</span><br><span class="line">   VALUES num …：直接以 <span class="built_in">float</span> 字符串序列作为查询向量，其后跟 num 个浮点值。</span><br><span class="line"></span><br><span class="line">输出控制</span><br><span class="line">   WITHSCORES：输出时每个结果后附带 similarity 分数（1 = 完全相同, 0 = 完全不同）。</span><br><span class="line">   WITHATTRIBS：若集合元素有 JSON 属性，则返回对应属性值。</span><br><span class="line">   COUNT num：指定最多返回匹配数量。</span><br><span class="line"></span><br><span class="line">搜索行为控制</span><br><span class="line">   EPSILON delta：仅返回与查询向量 distance &lt;= delta 的元素。</span><br><span class="line">   EF search-exploration-factor：HNSW 图搜索的探索因子（值越高搜索更深、更准确但更慢）。</span><br><span class="line">   FILTER expression：基于元素属性进行过滤，例如按年份、分类等。</span><br><span class="line">   FILTER-EF max-filtering-effort：限制 FILTER 表达式的评估尝试次数。</span><br><span class="line">   TRUTH：执行精确线性扫描而非近似图索引。</span><br><span class="line">   NOTHREAD：禁用后台线程，在主线程执行搜索（可能阻塞）。</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例 1 — 基于现有元素查询（取最相似前 10 个）</p></li></ul><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">VSIM my_vectors ELE apple WITHSCORES COUNT 10</span><br><span class="line"><span class="comment"># 返回元素 &quot;apple&quot; 最相似的前 10 个元素及其相似度分数。&quot;apple&quot; 必须在集合中存在。</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例 2 — 基于明确定义的向量查询</p></li></ul><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">VSIM my_vectors VALUES 3 0.12 0.34 0.56 WITHSCORES COUNT 5</span><br><span class="line"><span class="comment"># 使用向量 [0.12, 0.34, 0.56] 作为查询参考，返回 最相似 的前 5 个结果及分数。</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例 3 — 精确线性扫描用于基准或严格匹配</p></li></ul><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">VSIM my_vectors ELE targetElement TRUTH WITHSCORES</span><br><span class="line"><span class="comment"># 绕过 HNSW 索引，执行全量扫描以获取精确的相似度结果。</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例 4 — 使用属性过滤</p></li></ul><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"># 假设集合元素有 JSON 属性如 &#123; &quot;price&quot;: 20.99 &#125;</span></span><br><span class="line">VSIM my_vectors VALUES 4 0.15 0.26 0.47 0.88 FILTER <span class="string">&#x27;.price &gt;= 20&#x27;</span> COUNT 10</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例 5 — 使用 EPSILON 控制相似度范围</p></li></ul><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">VSIM my_vectors ELE queryElem EPSILON 0.2 COUNT 50</span><br><span class="line"><span class="comment"># 仅返回相似度 ≥ 0.8（即距离 ≤ 0.2）且最多 50 个的匹配项。</span></span><br></pre></td></tr></table></figure><h3 id="五、向量嵌入（Embedding）相关">五、向量嵌入（Embedding）相关</h3><table><thead><tr><th>命令</th><th>作用</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td><strong>VEMB</strong></td><td>从 向量集合（vector set） 中检索给定元素的向量（embedding）</td><td><code>VEMB key element [RAW]</code></td><td><code>key</code>: 向量集合的键名（即 vector set 名称）。<br><code>element</code>：目标元素名称，其向量将被检索。<br> <code>RAW</code>: 如果指定，返回 原始内部表示（量化信息 + 载体数据），而不是简单的反归一化向量。</td><td><code>VEMB my_vectors item42</code></td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>示例 1 — 获取向量（embedding）</p></li></ul><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">VADD my_vectors VALUES 4 0.15 0.26 0.47 0.88 item42</span><br><span class="line"><span class="comment"># 查询元素的向量</span></span><br><span class="line">VEMB my_vectors item42</span><br><span class="line"><span class="comment">## 输出</span></span><br><span class="line">1) <span class="string">&quot;0.15244093537330627&quot;</span></span><br><span class="line">2) <span class="string">&quot;0.2633070647716522&quot;</span></span><br><span class="line">3) <span class="string">&quot;0.47118112444877625&quot;</span></span><br><span class="line">4) <span class="string">&quot;0.8799999952316284&quot;</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例 2 — 使用 RAW 选项</p></li></ul><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></pre></td><td class="code"><pre><span class="line">VEMB my_vectors item42 RAW</span><br><span class="line"><span class="comment">## 输出</span></span><br><span class="line">1) int8              <span class="comment"># quantization type，字符串：如 fp32、bin、q8</span></span><br><span class="line">2) <span class="string">&quot;\x16&amp;D\x7f&quot;</span>      <span class="comment"># raw data blob ，二进制向量原始数据</span></span><br><span class="line">3) <span class="string">&quot;1.041825294494629&quot;</span>  <span class="comment"># L2 norm，归一化前向量的 L2 范数</span></span><br><span class="line">4) <span class="string">&quot;0.844671368598938&quot;</span>  <span class="comment"># quantization range (仅 q8 时)，量化范围，用于恢复真实值</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/01/14/redis8-datatype-vector/</id>
    <link href="https://blog.hanqunfeng.com/2026/01/14/redis8-datatype-vector/"/>
    <published>2026-01-14T13:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">本文介绍 Redis8 新增的数据类型 Vector Set</li>
<li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li>
</ul>]]>
    </summary>
    <title>Redis8 新增的数据类型 -- Vector Set</title>
    <updated>2026-01-14T10:07:56.048Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/redis/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/tags/redis/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">本文介绍 Redis 扩展模块 – RediSearch 操作 向量检索 的方法(<code>非 Redis8 中新增的向量功能</code>)</li><li class="lvl-2">本文基于<code>redis-7.4.7</code>，<code>springboot-3.5.8</code></li><li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li><li class="lvl-2">Redis 命令文档：<a href="https://redis.io/docs/latest/commands/">https://redis.io/docs/latest/commands/</a></li><li class="lvl-2">RediSearch 的安装参见 <a href="/2025/12/26/redis7-module-RediSearch/" title="Redis 扩展模块 -- RediSearch 的安装方法">Redis 扩展模块 -- RediSearch 的安装方法</a></li></ul><span id="more"></span><h2 id="向量是什么？">向量是什么？</h2><ul class="lvl-0"><li class="lvl-2"><p>在现代人工智能系统中，文本、图片、音频、视频等非结构化数据并不能直接被计算机理解和比较。</p></li><li class="lvl-2"><p>向量（Vector）是将这些复杂信息转换为数学可计算形式的核心载体，也是大模型检索、推荐、搜索、RAG、语义理解等能力的基础。</p></li></ul><h3 id="数学意义上的向量">数学意义上的向量</h3><ul class="lvl-0"><li class="lvl-2"><p>在数学中，向量是一个有序的数值集合，例如：</p></li></ul><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">[0.12, -0.87, 1.45, 0.003, ...]</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>每一个数字称为一个维度（dimension），整体可以看作是在高维空间中的一个点。</p></li><li class="lvl-2"><p>如果一个向量有 1024 个数字，则它位于一个 1024 维空间中。</p></li></ul><h3 id="向量在-AI-中的含义">向量在 AI 中的含义</h3><ul class="lvl-0"><li class="lvl-2"><p>在 AI 领域，向量并不是随意的数字，而是 对某个对象语义特征的数值化表达，例如：</p></li></ul><table><thead><tr><th>对象</th><th>向量表达含义</th></tr></thead><tbody><tr><td>一句话</td><td>语义含义、主题、情感、上下文关系</td></tr><tr><td>一张图片</td><td>纹理、颜色、形状、语义对象</td></tr><tr><td>一段音频</td><td>音色、节奏、频谱特征</td></tr><tr><td>一个用户</td><td>兴趣偏好、行为模式</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>模型通过学习大量数据，将“相似的语义”映射到“空间中距离接近的向量”。</p></li></ul><h3 id="向量为什么可以比较相似度？">向量为什么可以比较相似度？</h3><ul class="lvl-0"><li class="lvl-2"><p>向量之间可以通过距离函数进行比较，常见距离指标：</p></li></ul><table><thead><tr><th>指标</th><th>含义</th><th>适用场景</th></tr></thead><tbody><tr><td>Cosine Similarity</td><td>余弦相似度</td><td>文本语义、Embedding</td></tr><tr><td>Euclidean Distance</td><td>欧式距离</td><td>图像、空间特征</td></tr><tr><td>Dot Product</td><td>点积</td><td>推荐系统</td></tr><tr><td>L2 / L1</td><td>范数距离</td><td>通用计算</td></tr></tbody></table><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">向量A ≈ 向量B  → 表示语义相似</span><br><span class="line">向量A 距离 向量C 很远 → 表示语义差异大</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>向量相似度计算代码示例</p></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.example.langchain4j.embeding;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.example.langchain4j.ModelUtil;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.data.embedding.Embedding;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.model.embedding.EmbeddingModel;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.model.output.Response;</span><br><span class="line"><span class="keyword">import</span> org.apache.lucene.util.VectorUtil;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 向量demo</span></span><br><span class="line"><span class="comment"> * Created by hanqf on 2026/1/13 09:51.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">VectorDemo</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">        <span class="type">EmbeddingModel</span> <span class="variable">embeddingModel</span> <span class="operator">=</span> QwenEmbeddingModel.builder()</span><br><span class="line">                .apiKey(ModelUtil.DASHSCOPE_API_KEY)</span><br><span class="line">                .modelName(<span class="string">&quot;text-embedding-v3&quot;</span>)</span><br><span class="line">                .build();</span><br><span class="line"></span><br><span class="line">        <span class="type">String</span> <span class="variable">question</span> <span class="operator">=</span> <span class="string">&quot;Redis 7 如何开启向量检索功能？&quot;</span>;</span><br><span class="line"></span><br><span class="line">        String[] texts = &#123;</span><br><span class="line">                <span class="string">&quot;Redis 需要安装 RediSearch 模块才能支持向量索引和相似度搜索。&quot;</span>,</span><br><span class="line">                <span class="string">&quot;Redis 7.4 可以通过 MODULE LOAD 加载向量模块。&quot;</span>,</span><br><span class="line">                <span class="string">&quot;MySQL 如何建立索引提升查询性能？&quot;</span>,</span><br><span class="line">                <span class="string">&quot;Spring Boot 如何配置 RedisTemplate 连接集群？&quot;</span></span><br><span class="line">        &#125;;</span><br><span class="line"></span><br><span class="line">        Response&lt;Embedding&gt; embed = embeddingModel.embed(question);</span><br><span class="line">        <span class="type">float</span>[] q_vector = embed.content().vector();</span><br><span class="line">        System.out.println(embed.content().vectorAsList());</span><br><span class="line">        <span class="comment">// 获取向量的维度</span></span><br><span class="line">        System.out.println(embed.content().vector().length);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (String s : texts) &#123;</span><br><span class="line">            Response&lt;Embedding&gt; embedded = embeddingModel.embed(s);</span><br><span class="line">            <span class="type">float</span>[] vector = embedded.content().vector();</span><br><span class="line">            <span class="comment">// 计算余弦相似度, 范围[0,1],越大表示越相似</span></span><br><span class="line">            <span class="keyword">final</span> <span class="type">float</span> <span class="variable">cosine</span> <span class="operator">=</span> VectorUtil.cosine(q_vector, vector);</span><br><span class="line">            System.out.println(cosine);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="向量模型（Embedding-Model）">向量模型（Embedding Model）</h2><ul class="lvl-0"><li class="lvl-2"><p>向量模型非常多，常用的向量模型可以参考<a href="https://docs.langchain4j.dev/category/embedding-models">向量模型</a></p></li><li class="lvl-2"><p>向量模型，也称为 Embedding 模型，是一种：把原始数据映射为固定长度向量的深度学习模型。</p></li><li class="lvl-2"><p>输入可以是：文本、图片、音频、视频、多模态组合</p></li><li class="lvl-2"><p>输出是：<code>float[] embedding</code></p></li></ul><h3 id="向量维度的意义">向量维度的意义</h3><ul class="lvl-0"><li class="lvl-2"><p>常见维度范围：</p></li></ul><table><thead><tr><th>模型类型</th><th>向量维度</th></tr></thead><tbody><tr><td>Mini Embedding</td><td>256 ~ 384</td></tr><tr><td>通用文本模型</td><td>512 ~ 1024</td></tr><tr><td>高精度模型</td><td>1536 ~ 4096</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>维度越高：表达能力越强、存储和计算成本越高、检索延迟越大</p></li></ul><h3 id="向量模型的典型应用">向量模型的典型应用</h3><table><thead><tr><th>场景</th><th>作用</th></tr></thead><tbody><tr><td>语义搜索</td><td>搜索语义相关内容</td></tr><tr><td>RAG</td><td>文档召回</td></tr><tr><td>推荐系统</td><td>用户兴趣匹配</td></tr><tr><td>聚类分析</td><td>自动分类</td></tr><tr><td>去重</td><td>内容相似性检测</td></tr><tr><td>多模态检索</td><td>文搜图 / 图搜文</td></tr></tbody></table><h2 id="向量数据库">向量数据库</h2><h3 id="为什么需要向量数据库">为什么需要向量数据库?</h3><ul class="lvl-0"><li class="lvl-2"><p>普通数据库擅长：等值查询、范围查询、排序、聚合，但对：“在百万向量中找最相似的10个” 效率极低。</p></li><li class="lvl-2"><p>向量数据库专门解决：高维向量的相似度检索问题（ANN Search）。</p></li><li class="lvl-2"><p>向量数据库的能力</p></li></ul><table><thead><tr><th>能力</th><th>说明</th></tr></thead><tbody><tr><td>向量存储</td><td>高维 float 向量</td></tr><tr><td>相似度索引</td><td>HNSW、IVF、PQ</td></tr><tr><td>KNN 查询</td><td>TopK 最近邻</td></tr><tr><td>混合检索</td><td>向量 + 结构化过滤</td></tr><tr><td>持久化</td><td>磁盘存储</td></tr><tr><td>分布式</td><td>横向扩展</td></tr><tr><td>实时写入</td><td>在线更新</td></tr></tbody></table><h3 id="常见向量索引算法">常见向量索引算法</h3><table><thead><tr><th>算法</th><th>全称</th><th>核心原理</th><th>查询性能</th><th>检索精度</th><th>内存占用</th><th>构建成本</th><th>适用场景</th><th>典型系统</th></tr></thead><tbody><tr><td><strong>HNSW</strong></td><td>Hierarchical Navigable Small World</td><td>分层小世界图结构，通过多层图实现快速导航</td><td>极快（毫秒级）</td><td>极高（接近精确搜索）</td><td>较高</td><td>中等</td><td>实时检索、高 QPS、低延迟系统</td><td>Redis, Milvus, Faiss</td></tr><tr><td><strong>IVF</strong></td><td>Inverted File Index</td><td>先聚类分桶，再在桶内进行向量搜索</td><td>快</td><td>中等~高（可调）</td><td>中等~低</td><td>较高（需要训练聚类）</td><td>大规模离线检索、成本敏感场景</td><td>Faiss, Milvus</td></tr><tr><td><strong>PQ</strong></td><td>Product Quantization</td><td>向量分块压缩，用码本近似原始向量</td><td>快</td><td>较低（有量化误差）</td><td>极低</td><td>高（训练码本）</td><td>海量数据、内存受限系统</td><td>Faiss, Milvus</td></tr></tbody></table><h3 id="主流向量数据库">主流向量数据库</h3><ul class="lvl-0"><li class="lvl-2"><p>向量数据库非常多，常用的向量数据库可以参考<a href="https://docs.langchain4j.dev/category/embedding-stores">向量数据库</a></p></li><li class="lvl-2"><p>这里只列出一些主流的向量数据库</p></li></ul><table><thead><tr><th>产品</th><th>特点</th></tr></thead><tbody><tr><td>Milvus</td><td>专业向量数据库</td></tr><tr><td>Qdrant</td><td>Rust 高性能</td></tr><tr><td>Weaviate</td><td>Graph + Vector</td></tr><tr><td>Elasticsearch</td><td>向量扩展</td></tr><tr><td>Redis</td><td>内存级向量</td></tr><tr><td>PGVector</td><td>PostgreSQL 插件</td></tr><tr><td>Faiss</td><td>本地库</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>向量数据库在 RAG 架构中的位置</p></li></ul><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><br><span class="line">   ↓</span><br><span class="line">Embedding 模型 → 向量</span><br><span class="line">   ↓</span><br><span class="line">向量数据库 KNN 查询</span><br><span class="line">   ↓</span><br><span class="line">召回相关文档片段</span><br><span class="line">   ↓</span><br><span class="line">LLM 生成答案</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>向量数据库的能力决定了如下指标：</p></li></ul><table><thead><tr><th>指标</th><th>含义</th></tr></thead><tbody><tr><td>检索精度</td><td>检索结果质量，召回准确率</td></tr><tr><td>检索延迟</td><td>检索耗时</td></tr><tr><td>存储空间</td><td>索引大小</td></tr><tr><td>索引构建时间</td><td>索引构建耗时</td></tr><tr><td>系统吞吐能力</td><td>系统处理能力</td></tr></tbody></table><h2 id="RediSearch-创建向量索引">RediSearch 创建向量索引</h2><ul class="lvl-0"><li class="lvl-2"><p>前提是安装了 RedisJSON 和 RediSearch 模块，具体安装方法参见 <a href="/2025/12/24/redis7-module-RedisJSON/" title="Redis 扩展模块 -- RedisJSON 的安装方法">Redis 扩展模块 -- RedisJSON 的安装方法</a> 和 <a href="/2025/12/26/redis7-module-RediSearch/" title="Redis 扩展模块 -- RediSearch 的安装方法">Redis 扩展模块 -- RediSearch 的安装方法</a></p></li><li class="lvl-2"><p>在 Redis7 中，向量数据存储在 JSON 数据类型中，然后通过 RediSearch 对其创建索引，实现向量的搜索功能。</p></li><li class="lvl-2"><p>示例</p></li></ul><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><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 创建 JSON 数据 示例</span></span><br><span class="line">JSON.SET vector:1 $ <span class="string">&#x27;&#123;&quot;vector&quot;:[0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0], &quot;text&quot;:&quot;This is a test vector&quot;&#125;&#x27;</span></span><br><span class="line"></span><br><span class="line">JSON.SET vector:2 $ <span class="string">&#x27;&#123;&quot;vector&quot;:[0.2,0.1,0.4,0.3,0.6,0.5,0.8,0.7,1.0,0.9], &quot;text&quot;:&quot;Another vector&quot;&#125;&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 创建索引示例</span></span><br><span class="line">FT.CREATE vector_index ON JSON</span><br><span class="line">PREFIX 1 vector:</span><br><span class="line">SCHEMA</span><br><span class="line">  $.text AS text TEXT</span><br><span class="line">  $.vector AS vector VECTOR</span><br><span class="line">    HNSW 10</span><br><span class="line">    TYPE FLOAT32</span><br><span class="line">    DIM 10</span><br><span class="line">    DISTANCE_METRIC COSINE</span><br><span class="line">    M 16</span><br><span class="line">    EF_CONSTRUCTION 200</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>创建索引的参数说明</p></li></ul><table><thead><tr><th>参数</th><th>含义</th></tr></thead><tbody><tr><td>VECTOR</td><td>向量字段</td></tr><tr><td>HNSW</td><td>一种索引算法，可以把查询复杂度降低到近似 O(log N)，牺牲极少精度换取极高性能。</td></tr><tr><td>10</td><td>表示后面紧跟的 10 个参数是用于创建向量索引的参数</td></tr><tr><td>TYPE FLOAT32</td><td>向量数据类型</td></tr><tr><td>DIM 10</td><td>向量维度，一般由向量模型的输出维度决定，如：1024</td></tr><tr><td>COSINE</td><td>相似度度量</td></tr><tr><td>M 16</td><td>每个节点在 HNSW 图中允许建立的最大邻居连接数 <br>每个向量最多和多少个其它向量建立边关系<br>在插入一个新向量时：<br>算法会搜索一批候选邻居,<br>从候选集合中选择最多 M 个最合适的邻居建立双向连接</td></tr><tr><td>EF_CONSTRUCTION 200</td><td>建索引时搜索宽度<br>在构建索引（插入向量）时，用于搜索候选邻居的动态候选队列大小</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p><code>M</code>的大小影响的维度</p></li></ul><table><thead><tr><th>维度</th><th>M 增大</th><th>M 减小</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></tbody></table><ul class="lvl-0"><li class="lvl-2"><p><code>M</code>常见取值经验</p></li></ul><table><thead><tr><th>场景</th><th>推荐 M</th></tr></thead><tbody><tr><td>小规模数据（&lt;100万）</td><td>16 ~ 32</td></tr><tr><td>高召回要求（搜索、推荐）</td><td>32 ~ 64</td></tr><tr><td>内存敏感</td><td>8 ~ 16</td></tr><tr><td>向量维度很高（&gt;1024）</td><td>24 ~ 48</td></tr></tbody></table><blockquote><p>Redis 官方示例一般使用 M = 16，属于平衡型配置。</p></blockquote><ul class="lvl-0"><li class="lvl-2"><p><code>EF_CONSTRUCTION</code> 影响维度</p></li></ul><table><thead><tr><th>维度</th><th>EF_CONSTRUCTION 增大</th><th>EF_CONSTRUCTION 减小</th></tr></thead><tbody><tr><td>索引质量</td><td>↑ 提升</td><td>↓ 下降</td></tr><tr><td>构建时间</td><td>↑ 明显变慢</td><td>↓ 更快</td></tr><tr><td>构建 CPU</td><td>↑ 占用增加</td><td>↓ 减少</td></tr><tr><td>查询性能</td><td>↑ 更稳定</td><td>↓ 容易退化</td></tr><tr><td>内存</td><td>≈ 基本不变</td><td>≈</td></tr></tbody></table><blockquote><p>⚠️ 注意：EF_CONSTRUCTION 只影响建索引阶段，不影响查询阶段的实时性能。</p></blockquote><ul class="lvl-0"><li class="lvl-2"><p><code>EF_CONSTRUCTION</code> 常见取值经验</p></li></ul><blockquote><p>经验公式：<code>EF_CONSTRUCTION ≥ 2 × M</code></p></blockquote><table><thead><tr><th>M</th><th>推荐 EF_CONSTRUCTION</th></tr></thead><tbody><tr><td>16</td><td>100 ~ 200</td></tr><tr><td>32</td><td>200 ~ 400</td></tr><tr><td>48</td><td>300 ~ 600</td></tr></tbody></table><h2 id="搜索向量">搜索向量</h2><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><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 搜索示例: 🔍 查询一个向量，找 Top 2 个最相似向量，并返回 text 和相似度距离 score。</span></span><br><span class="line">FT.SEARCH vector_index</span><br><span class="line"><span class="string">&quot;*=&gt;[KNN 2 @vector <span class="variable">$vec</span> AS score]&quot;</span></span><br><span class="line">PARAMS 2 vec <span class="string">&quot;\xCD\xCC\xCC\x3D\xCD\xCC\x4C\x3E\x9A\x99\x99\x3E\xCD\xCC\xCC\x3E\x00\x00\x00\x3F\x9A\x99\x19\x3F\x33\x33\x33\x3F\xCD\xCC\x4C\x3F\x66\x66\x66\x3F\x00\x00\x80\x3F&quot;</span></span><br><span class="line">SORTBY score</span><br><span class="line">RETURN 2 text score</span><br><span class="line">DIALECT 2</span><br><span class="line"><span class="comment">## 返回结果</span></span><br><span class="line">1) (<span class="built_in">integer</span>) 2</span><br><span class="line">2) <span class="string">&quot;vector:1&quot;</span></span><br><span class="line">3) 1) <span class="string">&quot;score&quot;</span></span><br><span class="line">   2) <span class="string">&quot;0&quot;</span></span><br><span class="line">   3) <span class="string">&quot;text&quot;</span></span><br><span class="line">   4) <span class="string">&quot;This is a test vector&quot;</span></span><br><span class="line">4) <span class="string">&quot;vector:2&quot;</span></span><br><span class="line">5) 1) <span class="string">&quot;score&quot;</span></span><br><span class="line">   2) <span class="string">&quot;0.0129868984222&quot;</span></span><br><span class="line">   3) <span class="string">&quot;text&quot;</span></span><br><span class="line">   4) <span class="string">&quot;Another vector&quot;</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>查询表达式: <code>*=&gt;[KNN 2 @vector $vec AS score]</code></p></li></ul><table><thead><tr><th>参数</th><th>含义</th></tr></thead><tbody><tr><td><code>*=&gt;</code></td><td>这是 RediSearch 向量检索语法，这里表示匹配所有文档。<br>向量搜索通常不需要文本过滤，所以直接使用 *。</td></tr><tr><td><code>KNN</code></td><td>执行最近邻搜索（K-Nearest Neighbors）</td></tr><tr><td><code>2</code></td><td>返回最相近的 <strong>2 条记录</strong></td></tr><tr><td><code>@vector</code></td><td>索引中的向量字段名</td></tr><tr><td><code>$vec</code></td><td>查询向量参数（来自 PARAMS）</td></tr><tr><td><code>AS score</code></td><td>将“距离结果”保存到字段名 score</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>PARAMS 参数绑定</p></li></ul><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">PARAMS &lt;N&gt; &lt;key1&gt; &lt;value1&gt; ... &lt;keyN&gt; &lt;valueN&gt;</span><br><span class="line"><span class="comment"># 本示例中表示：定义一个参数，参数名：vec，参数值：一个 float32 二进制向量</span></span><br><span class="line">PARAMS 2 vec <span class="string">&quot;&lt;binary vector&gt;&quot;</span></span><br><span class="line"><span class="comment"># 注意这里要将 float 数组转换为FLOAT32二进制数据，不能直接传 [0.1,0.2,0.3]</span></span><br></pre></td></tr></table></figure><blockquote><p>float数组转换为FLOAT32二进制数据的方法</p></blockquote><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="keyword">package</span> com.example.langchain4j;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.nio.ByteBuffer;</span><br><span class="line"><span class="keyword">import</span> java.nio.ByteOrder;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">FloatToByte</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">        <span class="type">float</span>[] vector = <span class="keyword">new</span> <span class="title class_">float</span>[]&#123;<span class="number">0.1f</span>, <span class="number">0.2f</span>, <span class="number">0.3f</span>, <span class="number">0.4f</span>, <span class="number">0.5f</span>, <span class="number">0.6f</span>, <span class="number">0.7f</span>, <span class="number">0.8f</span>, <span class="number">0.9f</span>, <span class="number">1.0f</span>&#125;;</span><br><span class="line">        <span class="type">byte</span>[] bytes = toFloat32Bytes(vector);</span><br><span class="line">        System.out.println(toRedisHex(bytes));</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="type">byte</span>[] toFloat32Bytes(<span class="type">float</span>[] vector) &#123;</span><br><span class="line">        <span class="type">ByteBuffer</span> <span class="variable">buffer</span> <span class="operator">=</span> ByteBuffer</span><br><span class="line">                .allocate(vector.length * <span class="number">4</span>)</span><br><span class="line">                .order(ByteOrder.LITTLE_ENDIAN); <span class="comment">// Redis 要求小端序</span></span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (<span class="type">float</span> v : vector) &#123;</span><br><span class="line">            buffer.putFloat(v);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> buffer.array();</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> String <span class="title function_">toRedisHex</span><span class="params">(<span class="type">byte</span>[] bytes)</span> &#123;</span><br><span class="line">        <span class="type">StringBuilder</span> <span class="variable">sb</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringBuilder</span>(bytes.length * <span class="number">4</span>);</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">byte</span> b : bytes) &#123;</span><br><span class="line">            sb.append(<span class="string">&quot;\\x&quot;</span>);</span><br><span class="line">            sb.append(String.format(<span class="string">&quot;%02X&quot;</span>, b &amp; <span class="number">0xFF</span>));</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> sb.toString();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>SORTBY score 排序规则</p></li></ul><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"># 按 score 字段排序（默认升序）。</span></span><br><span class="line">SORTBY score</span><br><span class="line"><span class="comment">## 注意，COSINE 度量在 Redis 中返回的是“距离”，不是“相似度”</span></span><br><span class="line">score = 距离，距离越小 → 越相似 → 排在前面</span><br><span class="line">如果希望返回的相似度得分，则 (1 - score)</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>RETURN 2 text score: 返回字段控制</p></li><li class="lvl-2"><p>DIALECT 2: Dialect 版本，向量搜索语法：<code>=&gt;[KNN ...]</code> 必须使用 Dialect ≥ 2，否则会报语法错误。</p></li></ul><h2 id="使用-Redis-向量搜索的示例-基于-LangChain4j">使用 Redis 向量搜索的示例(基于 <code>LangChain4j</code>)</h2><ul class="lvl-0"><li class="lvl-2"><p>示例代码：<a href="https://github.com/hanqunfeng/springbootchapter/tree/master/springboot3-demo/redis-demo/langchain4j-ai-demo">GitHub</a></p></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.example.langchain4j.embeding;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.example.langchain4j.ModelUtil;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.community.model.dashscope.QwenEmbeddingModel;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.community.store.embedding.redis.RedisEmbeddingStore;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.data.embedding.Embedding;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.data.segment.TextSegment;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.model.embedding.EmbeddingModel;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.model.output.Response;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.store.embedding.EmbeddingMatch;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.store.embedding.EmbeddingSearchRequest;</span><br><span class="line"><span class="keyword">import</span> dev.langchain4j.store.embedding.EmbeddingStore;</span><br><span class="line"><span class="keyword">import</span> redis.clients.jedis.DefaultJedisClientConfig;</span><br><span class="line"><span class="keyword">import</span> redis.clients.jedis.HostAndPort;</span><br><span class="line"><span class="keyword">import</span> redis.clients.jedis.UnifiedJedis;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.IOException;</span><br><span class="line"><span class="keyword">import</span> java.nio.file.Files;</span><br><span class="line"><span class="keyword">import</span> java.nio.file.Paths;</span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisVectorDemo</span> &#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// EmbeddingModel</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">EmbeddingModel</span> <span class="variable">embeddingModel</span> <span class="operator">=</span> QwenEmbeddingModel.builder()</span><br><span class="line">            .apiKey(ModelUtil.DASHSCOPE_API_KEY)</span><br><span class="line">            .modelName(<span class="string">&quot;text-embedding-v3&quot;</span>)</span><br><span class="line">            .build();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Redis连接信息</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">DefaultJedisClientConfig</span> <span class="variable">jedisClientConfig</span> <span class="operator">=</span> DefaultJedisClientConfig.builder()</span><br><span class="line">            .user(<span class="string">&quot;admin&quot;</span>)</span><br><span class="line">            .password(<span class="string">&quot;123456&quot;</span>)</span><br><span class="line">            .build();</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">HostAndPort</span> <span class="variable">hostAndPort</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">HostAndPort</span>(<span class="string">&quot;127.0.0.1&quot;</span>, <span class="number">6379</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// EmbeddingStore 这里是Redis向量数据库</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> EmbeddingStore&lt;TextSegment&gt; embeddingStore = RedisEmbeddingStore.builder()</span><br><span class="line">            .unifiedJedis(<span class="keyword">new</span> <span class="title class_">UnifiedJedis</span>(hostAndPort, jedisClientConfig))</span><br><span class="line">            .dimension(<span class="number">1024</span>) <span class="comment">// 模型返回的维度</span></span><br><span class="line">            .indexName(<span class="string">&quot;redis-vector-index&quot;</span>) <span class="comment">// 索引名称，默认 embedding-index</span></span><br><span class="line">            .prefix(<span class="string">&quot;redis-vector-embedding:&quot;</span>) <span class="comment">// 索引前缀，默认 embedding:</span></span><br><span class="line">            .build();</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_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 读取 rag.txt 并插入到 Redis</span></span><br><span class="line">        insertDocumentsToRedis();</span><br><span class="line"></span><br><span class="line">        <span class="type">String</span> <span class="variable">question</span> <span class="operator">=</span> <span class="string">&quot;Redis 7 如何开启向量检索功能？&quot;</span>;</span><br><span class="line">        search(question);</span><br><span class="line"></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 class="doctag">@param</span> question</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> <span class="keyword">void</span> <span class="title function_">search</span><span class="params">(String question)</span>&#123;</span><br><span class="line">        <span class="type">TextSegment</span> <span class="variable">textSegment</span> <span class="operator">=</span> TextSegment.from(question);</span><br><span class="line">        Response&lt;Embedding&gt; embed = embeddingModel.embed(textSegment);</span><br><span class="line">        <span class="keyword">final</span> <span class="type">Embedding</span> <span class="variable">queryEmbedding</span> <span class="operator">=</span> embed.content();</span><br><span class="line"></span><br><span class="line">        <span class="type">EmbeddingSearchRequest</span> <span class="variable">embeddingSearchRequest</span> <span class="operator">=</span> EmbeddingSearchRequest.builder()</span><br><span class="line">                .queryEmbedding(queryEmbedding)</span><br><span class="line">                .maxResults(<span class="number">3</span>)</span><br><span class="line">                .build();</span><br><span class="line">        List&lt;EmbeddingMatch&lt;TextSegment&gt;&gt; matches = embeddingStore.search(embeddingSearchRequest).matches();</span><br><span class="line">        <span class="keyword">for</span> (EmbeddingMatch&lt;TextSegment&gt; match : matches) &#123;</span><br><span class="line">            System.out.println(match.embedded().text());</span><br><span class="line">            System.out.println(match.score());</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">     * 插入文档到 Redis，初始化数据</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> <span class="keyword">void</span> <span class="title function_">insertDocumentsToRedis</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="type">String</span> <span class="variable">content</span> <span class="operator">=</span> Files.readString(Paths.get(<span class="string">&quot;rag.txt&quot;</span>));</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 简单按段落分割，也可以使用专门的文本分割工具</span></span><br><span class="line">            String[] paragraphs = content.split(<span class="string">&quot;\n\n&quot;</span>);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">for</span> (String paragraph : paragraphs) &#123;</span><br><span class="line">                <span class="keyword">if</span> (!paragraph.trim().isEmpty()) &#123;</span><br><span class="line">                    <span class="type">TextSegment</span> <span class="variable">textSegment</span> <span class="operator">=</span> TextSegment.from(paragraph.trim());</span><br><span class="line">                    Response&lt;Embedding&gt; embeddingResponse = embeddingModel.embed(textSegment);</span><br><span class="line">                    <span class="type">Embedding</span> <span class="variable">embedding</span> <span class="operator">=</span> embeddingResponse.content();</span><br><span class="line"></span><br><span class="line">                    <span class="comment">// 将向量和文本段添加到 Redis</span></span><br><span class="line">                    embeddingStore.add(embedding, textSegment);</span><br><span class="line">                    System.out.println(<span class="string">&quot;已添加段落到向量库: &quot;</span> + textSegment.text().substring(<span class="number">0</span>, Math.min(<span class="number">50</span>, textSegment.text().length())) + <span class="string">&quot;...&quot;</span>);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            System.out.println(<span class="string">&quot;所有文档已成功插入到 Redis 向量库中&quot;</span>);</span><br><span class="line"></span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">            System.err.println(<span class="string">&quot;读取文件失败: &quot;</span> + e.getMessage());</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><ul class="lvl-0"><li class="lvl-2"><p>rag.txt 内容</p></li></ul><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><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">问题：Redis 7 如何开启向量检索功能？</span><br><span class="line">答案：需要安装并加载 RediSearch 模块，然后创建包含 VECTOR 字段的索引即可支持向量相似度搜索。</span><br><span class="line"></span><br><span class="line">问题：Redis 中 STRING 和 HASH 的主要区别是什么？</span><br><span class="line">答案：STRING 适合存储单值或序列化对象，HASH 适合存储结构化字段，支持单字段读写和节省内存。</span><br><span class="line"></span><br><span class="line">问题：Spring Boot 如何配置 RedisTemplate 连接 Redis 集群？</span><br><span class="line">答案：需要配置 cluster.nodes 节点列表，并使用 Lettuce 客户端自动发现和维护集群拓扑。</span><br></pre></td></tr></table></figure><h2 id="后记">后记</h2><ul class="lvl-0"><li class="lvl-2"><p>相同的功能使用 <code>SpringAI</code> 也实现了一版，具体代码参看示例代码：<a href="https://github.com/hanqunfeng/springbootchapter/tree/master/springboot3-demo/redis-demo/springai-redis-demo">GitHub</a></p></li></ul>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/01/13/redis7-module-RediSearch-vector/</id>
    <link href="https://blog.hanqunfeng.com/2026/01/13/redis7-module-RediSearch-vector/"/>
    <published>2026-01-13T13:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">本文介绍 Redis 扩展模块 – RediSearch 操作 向量检索 的方法(<code>非 Redis8 中新增的向量功能</code>)</li>
<li class="lvl-2">本文基于<code>redis-7.4.7</code>，<code>springboot-3.5.8</code></li>
<li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li>
<li class="lvl-2">Redis 命令文档：<a href="https://redis.io/docs/latest/commands/">https://redis.io/docs/latest/commands/</a></li>
<li class="lvl-2">RediSearch 的安装参见 <a href="/2025/12/26/redis7-module-RediSearch/" title="Redis 扩展模块 -- RediSearch 的安装方法">Redis 扩展模块 -- RediSearch 的安装方法</a></li>
</ul>]]>
    </summary>
    <title>RediSearch 开发实战 之 向量检索</title>
    <updated>2026-01-14T07:58:32.098Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/redis/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/tags/redis/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">本文介绍 SpringBoot 集成 Redis 的方法</li><li class="lvl-2">本文基于<code>redis-7.4.7</code>，<code>springboot-3.5.8</code></li><li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li></ul><span id="more"></span><h2 id="引入依赖">引入依赖</h2><ul class="lvl-0"><li class="lvl-2"><p>在<code>pom.xml</code>中引入依赖</p></li></ul><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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="tag">&lt;<span class="name">dependencyManagement</span>&gt;</span></span><br><span class="line">   <span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-dependencies<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.5.8<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">type</span>&gt;</span>pom<span class="tag">&lt;/<span class="name">type</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">scope</span>&gt;</span>import<span class="tag">&lt;/<span class="name">scope</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">   <span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencyManagement</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">dependencies</span>&gt;</span></span><br><span class="line">   <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.springframework.boot<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>spring-boot-starter-data-redis<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">   <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line">   <span class="comment">&lt;!-- redis 连接池 --&gt;</span></span><br><span class="line">   <span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.commons<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>commons-pool2<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">   <span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependencies</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="配置-application-yml">配置 <code>application.yml</code></h2><h3 id="单机模式">单机模式</h3><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><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="attr">spring:</span></span><br><span class="line">  <span class="attr">data:</span></span><br><span class="line">    <span class="attr">redis:</span></span><br><span class="line">      <span class="attr">host:</span> <span class="number">127.0</span><span class="number">.0</span><span class="number">.1</span></span><br><span class="line">      <span class="attr">port:</span> <span class="number">6379</span></span><br><span class="line">      <span class="attr">database:</span> <span class="number">0</span></span><br><span class="line">      <span class="attr">username:</span> <span class="string">admin</span></span><br><span class="line">      <span class="attr">password:</span> <span class="string">redis123</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">5s</span></span><br><span class="line">      <span class="attr">connectTimeout:</span> <span class="string">3s</span></span><br><span class="line">      <span class="attr">clientName:</span> <span class="string">demo-service</span></span><br><span class="line">      <span class="attr">lettuce:</span></span><br><span class="line">        <span class="attr">shutdown-timeout:</span> <span class="string">100ms</span></span><br><span class="line">        <span class="attr">pool:</span></span><br><span class="line">          <span class="attr">max-active:</span> <span class="number">64</span></span><br><span class="line">          <span class="attr">max-idle:</span> <span class="number">32</span></span><br><span class="line">          <span class="attr">min-idle:</span> <span class="number">16</span></span><br><span class="line">          <span class="attr">max-wait:</span> <span class="string">2s</span></span><br><span class="line">          <span class="attr">time-between-eviction-runs:</span> <span class="string">30s</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>基础连接配置项</p></li></ul><table><thead><tr><th>配置项</th><th>示例值</th><th>含义</th><th>默认值</th><th>生产建议</th></tr></thead><tbody><tr><td><code>spring.data.redis.host</code></td><td><code>localhost</code></td><td>Redis 服务地址（IP / 域名）</td><td><code>localhost</code></td><td>生产使用内网 IP / 域名</td></tr><tr><td><code>spring.data.redis.port</code></td><td><code>6379</code></td><td>Redis 服务端口</td><td><code>6379</code></td><td>一般无需修改</td></tr><tr><td><code>spring.data.redis.username</code></td><td><code>admin</code></td><td>Redis ACL 用户名（Redis 6+）</td><td><code>(空)</code></td><td>未启用 ACL 可不配置</td></tr><tr><td><code>spring.data.redis.password</code></td><td><code>123456</code></td><td>Redis 访问密码</td><td><code>(空)</code></td><td>生产必须配置</td></tr><tr><td><code>spring.data.redis.database</code></td><td><code>0</code></td><td>逻辑数据库索引（0–15）</td><td><code>0</code></td><td>Cluster 模式无效</td></tr><tr><td><code>spring.data.redis.timeout</code></td><td><code>3s</code></td><td>Redis 命令执行超时</td><td><code>60s</code>（依版本）</td><td>建议 2–5s</td></tr><tr><td><code>spring.data.redis.connect-timeout</code></td><td><code>3s</code></td><td>TCP 建连超时</td><td>OS 默认</td><td>建议 1–5s</td></tr><tr><td><code>spring.data.redis.client-name</code></td><td><code>my-redis-client</code></td><td>客户端标识，用于运维定位</td><td><code>(空)</code></td><td>强烈建议配置</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>连接池配置（Lettuce Pool）</p></li></ul><table><thead><tr><th>配置项</th><th>示例值</th><th>含义</th><th>默认值</th><th>生产建议</th></tr></thead><tbody><tr><td><code>spring.data.redis.lettuce.pool.max-active</code></td><td><code>8</code></td><td>连接池最大连接数（使用中 + 空闲）</td><td><code>8</code></td><td>CPU × 2~4 或压测评估</td></tr><tr><td><code>spring.data.redis.lettuce.pool.max-wait</code></td><td><code>2s</code></td><td>连接耗尽时等待时间</td><td><code>-1</code>（无限等待）</td><td>必须设置，1–3s</td></tr><tr><td><code>spring.data.redis.lettuce.pool.max-idle</code></td><td><code>8</code></td><td>最大空闲连接数</td><td><code>8</code></td><td>max-active × 30%~50%</td></tr><tr><td><code>spring.data.redis.lettuce.pool.min-idle</code></td><td><code>0</code></td><td>最小空闲连接数</td><td><code>0</code></td><td>≥ max-active × 25%</td></tr><tr><td><code>spring.data.redis.lettuce.pool.time-between-eviction-runs</code></td><td><code>60s</code></td><td>空闲连接检测周期</td><td><code>-1</code>（不启用）</td><td>30–60s</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>Lettuce 客户端运行参数</p></li></ul><table><thead><tr><th>配置项</th><th>示例值</th><th>含义</th><th>默认值</th><th>生产建议</th></tr></thead><tbody><tr><td><code>spring.data.redis.lettuce.shutdown-timeout</code></td><td><code>100ms</code></td><td>应用关闭时等待连接释放时间</td><td><code>100ms</code></td><td>一般无需修改</td></tr></tbody></table><h3 id="sentinel-模式">sentinel 模式</h3><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><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="attr">spring:</span></span><br><span class="line">  <span class="attr">data:</span></span><br><span class="line">    <span class="attr">redis:</span></span><br><span class="line">      <span class="attr">database:</span> <span class="number">0</span></span><br><span class="line">      <span class="attr">username:</span> <span class="string">admin</span></span><br><span class="line">      <span class="attr">password:</span> <span class="string">redis123</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">5s</span></span><br><span class="line">      <span class="attr">connectTimeout:</span> <span class="string">3s</span></span><br><span class="line">      <span class="attr">clientName:</span> <span class="string">demo-service</span></span><br><span class="line">      <span class="attr">sentinel:</span></span><br><span class="line">        <span class="attr">master:</span> <span class="string">mymaster</span></span><br><span class="line">        <span class="attr">nodes:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="number">10.0</span><span class="number">.0</span><span class="number">.10</span><span class="string">:26379</span></span><br><span class="line">          <span class="bullet">-</span> <span class="number">10.0</span><span class="number">.0</span><span class="number">.11</span><span class="string">:26379</span></span><br><span class="line">          <span class="bullet">-</span> <span class="number">10.0</span><span class="number">.0</span><span class="number">.12</span><span class="string">:26379</span></span><br><span class="line">      <span class="attr">lettuce:</span></span><br><span class="line">        <span class="attr">shutdown-timeout:</span> <span class="string">100ms</span></span><br><span class="line">        <span class="attr">pool:</span></span><br><span class="line">          <span class="attr">max-active:</span> <span class="number">64</span></span><br><span class="line">          <span class="attr">max-idle:</span> <span class="number">32</span></span><br><span class="line">          <span class="attr">min-idle:</span> <span class="number">16</span></span><br><span class="line">          <span class="attr">max-wait:</span> <span class="string">2s</span></span><br><span class="line">          <span class="attr">time-between-eviction-runs:</span> <span class="string">30s</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>哨兵模式配置项</p></li></ul><table><thead><tr><th>配置项</th><th>示例值</th><th>含义</th><th>是否必填</th><th>说明</th></tr></thead><tbody><tr><td><code>spring.data.redis.sentinel.master</code></td><td><code>mymaster</code></td><td>Sentinel 监控的 Master 名称</td><td>✅</td><td>必须与 Sentinel <code>monitor</code> 名称完全一致</td></tr><tr><td><code>spring.data.redis.sentinel.nodes</code></td><td><code>10.0.0.10:26379,10.0.0.11:26379,10.0.0.12:26379</code></td><td>Sentinel 节点列表</td><td>✅</td><td>至少配置 2–3 个 Sentinel，提升可用性</td></tr><tr><td><code>spring.data.redis.sentinel.username</code></td><td><code>(空)</code></td><td>Sentinel 的认证用户名</td><td></td><td>如果启用 ACL，则必填</td></tr><tr><td><code>spring.data.redis.sentinel.password</code></td><td><code>(空)</code></td><td>Sentinel 的认证密码</td><td></td><td>如果启用 ACL，则必填</td></tr></tbody></table><h3 id="cluster-模式">cluster 模式</h3><figure class="highlight yml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">spring:</span></span><br><span class="line">  <span class="attr">data:</span></span><br><span class="line">    <span class="attr">redis:</span></span><br><span class="line">      <span class="attr">username:</span> <span class="string">admin</span></span><br><span class="line">      <span class="attr">password:</span> <span class="string">redis123</span></span><br><span class="line">      <span class="attr">timeout:</span> <span class="string">5s</span></span><br><span class="line">      <span class="attr">connectTimeout:</span> <span class="string">3s</span></span><br><span class="line">      <span class="attr">clientName:</span> <span class="string">order-service</span></span><br><span class="line">      <span class="attr">cluster:</span></span><br><span class="line">        <span class="attr">nodes:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.1</span><span class="number">.10</span><span class="string">:6379</span></span><br><span class="line">          <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.1</span><span class="number">.11</span><span class="string">:6379</span></span><br><span class="line">          <span class="bullet">-</span> <span class="number">192.168</span><span class="number">.1</span><span class="number">.12</span><span class="string">:6379</span></span><br><span class="line">      <span class="attr">lettuce:</span></span><br><span class="line">        <span class="attr">shutdown-timeout:</span> <span class="string">100ms</span></span><br><span class="line">        <span class="attr">cluster:</span></span><br><span class="line">          <span class="attr">refresh:</span></span><br><span class="line">            <span class="attr">adaptive:</span> <span class="literal">true</span></span><br><span class="line">            <span class="attr">period:</span> <span class="string">10s</span></span><br><span class="line">        <span class="attr">pool:</span></span><br><span class="line">          <span class="attr">max-active:</span> <span class="number">64</span></span><br><span class="line">          <span class="attr">max-idle:</span> <span class="number">32</span></span><br><span class="line">          <span class="attr">min-idle:</span> <span class="number">16</span></span><br><span class="line">          <span class="attr">max-wait:</span> <span class="string">2s</span></span><br><span class="line">          <span class="attr">time-between-eviction-runs:</span> <span class="string">30s</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>注意</p></li></ul><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">集群模式下，不能配置 `spring.data.redis.database`，因为集群只能使用默认数据库索引 0</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>集群模式配置项</p></li></ul><table><thead><tr><th>配置项</th><th>示例值</th><th>含义</th><th>是否必填</th><th>默认值</th><th>生产建议</th></tr></thead><tbody><tr><td><code>spring.data.redis.cluster.nodes</code></td><td><code>192.168.1.10:6379,...</code></td><td>Redis Cluster 节点地址列表</td><td>✅</td><td><code>(空)</code></td><td>至少配置 3 个节点</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>Cluster 拓扑自动刷新配置，用于解决集群拓扑变化时 Client 无法自动感知</p></li></ul><table><thead><tr><th>配置项</th><th>示例值</th><th>含义</th><th>默认值</th><th>是否推荐</th></tr></thead><tbody><tr><td><code>adaptive</code></td><td><code>true</code></td><td>开启 <strong>事件驱动拓扑刷新</strong></td><td><code>false</code></td><td>✅ 必须</td></tr><tr><td><code>period</code></td><td><code>10s</code></td><td>开启 <strong>定时拓扑刷新周期</strong></td><td>关闭</td><td>✅ 必须</td></tr></tbody></table><h2 id="SpringBoot-配置类">SpringBoot 配置类</h2><h3 id="封装-RedisTemplate">封装 RedisTemplate</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.example.config;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.annotation.JsonInclude;</span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.databind.DeserializationFeature;</span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.databind.ObjectMapper;</span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.databind.json.JsonMapper;</span><br><span class="line"><span class="keyword">import</span> com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.connection.RedisConnectionFactory;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.core.RedisTemplate;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.serializer.StringRedisSerializer;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisConfig</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> ObjectMapper <span class="title function_">redisObjectMapper</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> JsonMapper.builder()</span><br><span class="line">                .addModule(<span class="keyword">new</span> <span class="title class_">JavaTimeModule</span>())</span><br><span class="line">                .serializationInclusion(JsonInclude.Include.NON_NULL)</span><br><span class="line">                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, <span class="literal">false</span>)</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="keyword">public</span> RedisTemplate&lt;String, Object&gt; <span class="title function_">redisTemplate</span><span class="params">(</span></span><br><span class="line"><span class="params">            RedisConnectionFactory connectionFactory,</span></span><br><span class="line"><span class="params">            ObjectMapper objectMapper)</span> &#123;</span><br><span class="line"></span><br><span class="line">        RedisTemplate&lt;String, Object&gt; template = <span class="keyword">new</span> <span class="title class_">RedisTemplate</span>&lt;&gt;();</span><br><span class="line">        template.setConnectionFactory(connectionFactory);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// --- Key 序列化 ---</span></span><br><span class="line">        <span class="type">StringRedisSerializer</span> <span class="variable">stringSerializer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringRedisSerializer</span>();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// --- Value 序列化 ---</span></span><br><span class="line">        <span class="comment">// 使用 Jackson JSON，避免 JDK 序列化性能与安全问题</span></span><br><span class="line">        <span class="type">GenericJackson2JsonRedisSerializer</span> <span class="variable">jsonSerializer</span> <span class="operator">=</span></span><br><span class="line">                <span class="keyword">new</span> <span class="title class_">GenericJackson2JsonRedisSerializer</span>(objectMapper);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Key</span></span><br><span class="line">        template.setKeySerializer(stringSerializer);</span><br><span class="line">        template.setHashKeySerializer(stringSerializer);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Value</span></span><br><span class="line">        template.setValueSerializer(jsonSerializer);</span><br><span class="line">        template.setHashValueSerializer(jsonSerializer);</span><br><span class="line"></span><br><span class="line">        template.afterPropertiesSet();</span><br><span class="line">        <span class="keyword">return</span> template;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="开启注解式缓存">开启注解式缓存</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> com.example.config;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> lombok.Getter;</span><br><span class="line"><span class="keyword">import</span> lombok.Setter;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.autoconfigure.AutoConfigureAfter;</span><br><span class="line"><span class="keyword">import</span> org.springframework.boot.context.properties.ConfigurationProperties;</span><br><span class="line"><span class="keyword">import</span> org.springframework.cache.CacheManager;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Bean;</span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Configuration;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.cache.RedisCacheConfiguration;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.cache.RedisCacheManager;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.cache.RedisCacheWriter;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.core.RedisTemplate;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.serializer.RedisSerializationContext;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.time.Duration;</span><br><span class="line"><span class="keyword">import</span> java.util.HashMap;</span><br><span class="line"><span class="keyword">import</span> java.util.Map;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@AutoConfigureAfter(value = RedisConfig.class)</span></span><br><span class="line"><span class="comment">//注入redis分组配置属性：ttlmap</span></span><br><span class="line"><span class="meta">@ConfigurationProperties(prefix = &quot;caching&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisCachingConfig</span> &#123;</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">@Getter</span></span><br><span class="line">    <span class="meta">@Setter</span></span><br><span class="line">    <span class="keyword">private</span> Map&lt;String, Long&gt; ttlmap;</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> CacheManager <span class="title function_">cacheManager</span><span class="params">(RedisTemplate&lt;String, Object&gt; redisTemplate)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> RedisCacheManager</span><br><span class="line">                .builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisTemplate.getConnectionFactory()))</span><br><span class="line">                <span class="comment">//缺省配置</span></span><br><span class="line">                .cacheDefaults(redisCacheConfiguration(redisTemplate, <span class="number">3600L</span>))</span><br><span class="line">                <span class="comment">//分组配置，不需要分组配置可以去掉，不同的组配置不同的缓存过期时间，可以防止&quot;缓存雪崩&quot;</span></span><br><span class="line">                .withInitialCacheConfigurations(initialRedisCacheConfiguration(redisTemplate))</span><br><span class="line">                .build();</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> RedisCacheConfiguration <span class="title function_">redisCacheConfiguration</span><span class="params">(RedisTemplate&lt;String, Object&gt; redisTemplate, Long ttl)</span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> RedisCacheConfiguration.defaultCacheConfig()</span><br><span class="line">                .entryTtl(Duration.ofSeconds(ttl)) <span class="comment">//设置过期，单位秒</span></span><br><span class="line">                <span class="comment">//.disableCachingNullValues() //不允许存储null值，默认可以存储null，缓存null可以防止&quot;缓存穿透&quot;</span></span><br><span class="line">                <span class="comment">//.disableKeyPrefix()  //设置key前面不带前缀，最好不要去掉前缀，否则执行删除缓存时会清空全部缓存</span></span><br><span class="line">                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getStringSerializer()))</span><br><span class="line">                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisTemplate.getValueSerializer()));</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> Map&lt;String, RedisCacheConfiguration&gt; <span class="title function_">initialRedisCacheConfiguration</span><span class="params">(RedisTemplate&lt;String, Object&gt; redisTemplate)</span> &#123;</span><br><span class="line">        Map&lt;String, RedisCacheConfiguration&gt; redisCacheConfigurationMap = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">        <span class="keyword">for</span> (Map.Entry&lt;String, Long&gt; entry : ttlmap.entrySet()) &#123;</span><br><span class="line">            redisCacheConfigurationMap.put(entry.getKey(), redisCacheConfiguration(redisTemplate, entry.getValue()));</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> redisCacheConfigurationMap;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>ttlmap 配置项</p></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="attr">caching:</span></span><br><span class="line">  <span class="attr">ttlmap:</span></span><br><span class="line">    <span class="attr">commonCache:</span> <span class="number">3600</span></span><br><span class="line">    <span class="attr">loginCache:</span> <span class="number">7200</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>启动类上要加 @EnableCaching</p></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@EnableCaching</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DemoApplication</span> &#123;</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_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">        SpringApplication.run(DemoApplication.class, args);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>缓存注解使用示例</p></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="comment">// 缓存分组</span></span><br><span class="line"><span class="meta">@CacheConfig(cacheNames = &quot;commonCache&quot;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SystemUserServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">ISystemUserService</span> &#123;</span><br><span class="line">   <span class="meta">@Autowired</span></span><br><span class="line">   SystemUserJpaRepository systemUserJpaRepository;</span><br><span class="line"></span><br><span class="line">   <span class="comment">//向组内添加缓存</span></span><br><span class="line">   <span class="meta">@Cacheable(key = &quot;&#x27;SystemUserServiceImpl.findAll&#x27;&quot;)</span></span><br><span class="line">   <span class="keyword">public</span> List&lt;SystemUser&gt; <span class="title function_">findAll</span><span class="params">()</span> &#123;</span><br><span class="line">      List&lt;SystemUser&gt; systemUserList = systemUserJpaRepository.findAll();</span><br><span class="line">      <span class="keyword">return</span> systemUserList;</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="comment">//向组内添加缓存</span></span><br><span class="line">   <span class="meta">@Cacheable(key = &quot;&#x27;SystemUserServiceImpl.findById_&#x27;+ #userId&quot;)</span></span><br><span class="line">   <span class="keyword">public</span> SystemUser <span class="title function_">findById</span><span class="params">(String userId)</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> systemUserJpaRepository.findById(userId);</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="comment">// 删除组内指定缓存</span></span><br><span class="line">   <span class="meta">@CacheEvict(key = &quot;&#x27;SystemUserServiceImpl.findById_&#x27;+ #userId&quot;)</span></span><br><span class="line">   <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">deleteById</span><span class="params">(String userId)</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> systemUserJpaRepository.deleteById(userId);</span><br><span class="line">   &#125;</span><br><span class="line">   <span class="comment">// 删除本组全部缓存</span></span><br><span class="line">   <span class="meta">@CacheEvict(allEntries = true, beforeInvocation = true)</span></span><br><span class="line">   <span class="keyword">public</span> SystemUser <span class="title function_">add</span><span class="params">(SystemUser user)</span> &#123;</span><br><span class="line">      systemUserJpaRepository.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>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/01/12/redis-springboot/</id>
    <link href="https://blog.hanqunfeng.com/2026/01/12/redis-springboot/"/>
    <published>2026-01-12T14:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">本文介绍 SpringBoot 集成 Redis 的方法</li>
<li class="lvl-2">本文基于<code>redis-7.4.7</code>，<code>springboot-3.5.8</code></li>
<li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li>
</ul>]]>
    </summary>
    <title>SpringBoot 集成 Redis</title>
    <updated>2026-01-12T08:39:11.238Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/redis/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/tags/redis/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">本文基于<code>redis-7.4.7</code></li><li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li></ul><span id="more"></span><h2 id="Cluster-Management-简介">Cluster Management 简介</h2><ul class="lvl-0"><li class="lvl-2">Redis Cluster Commands 是 Redis 分布式集群的“控制面命令集”，用于管理节点、分片、迁移、故障转移和路由策略，而不是用于业务数据读写。</li></ul><h2 id="Cluster-Management-命令详解">Cluster Management 命令详解</h2><ul class="lvl-0"><li class="lvl-2"><p>通用参数说明</p></li></ul><table><thead><tr><th>字段</th><th>说明</th></tr></thead><tbody><tr><td><strong>slot</strong></td><td>Hash Slot 编号，范围：<code>0 – 16383</code></td></tr><tr><td><strong>nodeId</strong></td><td>Redis Cluster 节点唯一 ID（<code>CLUSTER NODES</code> 可查询）</td></tr></tbody></table><h3 id="一、路由与访问模式控制">一、路由与访问模式控制</h3><table><thead><tr><th>命令</th><th>语法</th><th>说明</th><th>示例</th></tr></thead><tbody><tr><td>READONLY</td><td><code>READONLY</code></td><td>允许客户端从副本读</td><td><code>READONLY</code></td></tr><tr><td>READWRITE</td><td><code>READWRITE</code></td><td>恢复只向主节点写</td><td><code>READWRITE</code></td></tr><tr><td>ASKING</td><td><code>ASKING</code></td><td>迁移过程中允许访问目标节点</td><td><code>ASKING</code></td></tr></tbody></table><h3 id="二、Slot-计算与查询">二、Slot 计算与查询</h3><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>用途</th></tr></thead><tbody><tr><td>CLUSTER KEYSLOT</td><td><code>CLUSTER KEYSLOT key</code></td><td>key</td><td>计算 key 所属 slot</td></tr><tr><td>CLUSTER COUNTKEYSINSLOT</td><td><code>CLUSTER COUNTKEYSINSLOT slot</code></td><td>slot: 槽位</td><td>统计 slot 中 key 数量</td></tr><tr><td>CLUSTER GETKEYSINSLOT</td><td><code>CLUSTER GETKEYSINSLOT slot count</code></td><td>slot, count: 返回数量</td><td>获取 slot 中的 key</td></tr></tbody></table><h3 id="三、Slot-分配与迁移（扩容核心）">三、Slot 分配与迁移（扩容核心）</h3><table><thead><tr><th>命令</th><th>语法</th><th>说明</th></tr></thead><tbody><tr><td>CLUSTER ADDSLOTS</td><td><code>CLUSTER ADDSLOTS slot [slot ...]</code></td><td>给节点分配 slot</td></tr><tr><td>CLUSTER ADDSLOTSRANGE</td><td><code>CLUSTER ADDSLOTSRANGE start end ...</code></td><td>批量分配 slot</td></tr><tr><td>CLUSTER DELSLOTS</td><td><code>CLUSTER DELSLOTS slot [slot ...]</code></td><td>删除 slot</td></tr><tr><td>CLUSTER DELSLOTSRANGE</td><td><code>CLUSTER DELSLOTSRANGE start end ...</code></td><td>批量删除 slot</td></tr><tr><td>CLUSTER FLUSHSLOTS</td><td><code>CLUSTER FLUSHSLOTS</code></td><td>清空节点 slot</td></tr><tr><td>CLUSTER SETSLOT</td><td><code>CLUSTER SETSLOT slot MIGRATING|IMPORTING|NODE nodeId|STABLE</code></td><td>设置 slot 状态</td></tr><tr><td>CLUSTER MIGRATION</td><td><code>CLUSTER MIGRATION</code></td><td>查询迁移状态</td></tr><tr><td>CLUSTER SLOT-STATS</td><td><code>CLUSTER SLOT-STATS</code></td><td>slot 统计</td></tr><tr><td>CLUSTER SLOTS</td><td><code>CLUSTER SLOTS</code></td><td>查看 slot 分布<br>已过时，推荐使用 <code>CLUSTER SHARDS</code></td></tr><tr><td>CLUSTER SHARDS</td><td><code>CLUSTER SHARDS</code></td><td>按 shard 展示</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>实际应用: 集群扩容、数据再平衡、故障恢复、热迁移</p></li><li class="lvl-2"><p>通常由 redis-cli 或自动化工具封装执行。</p></li></ul><h4 id="CLUSTER-SETSLOT">CLUSTER SETSLOT</h4><ul class="lvl-0"><li class="lvl-2"><p>参数说明表</p></li></ul><table><thead><tr><th>子命令</th><th>完整语法</th><th>参数含义</th><th>Slot 状态语义</th><th>客户端行为影响</th><th>典型使用阶段</th><th>示例</th></tr></thead><tbody><tr><td><strong>MIGRATING</strong></td><td><code>CLUSTER SETSLOT &lt;slot&gt; MIGRATING &lt;target-node-id&gt;</code></td><td>target-node-id：目标节点 ID（数据迁往的节点）</td><td>当前节点正在把该 slot 的数据迁出</td><td>- 若 key 仍在本节点 → 正常处理<br>- 若 key 已迁走 → 返回 <strong>ASK 重定向</strong></td><td>槽迁移开始阶段（源节点）</td><td><code>CLUSTER SETSLOT 100 MIGRATING e5f6g7...</code></td></tr><tr><td><strong>IMPORTING</strong></td><td><code>CLUSTER SETSLOT &lt;slot&gt; IMPORTING &lt;source-node-id&gt;</code></td><td>source-node-id：源节点 ID（数据来源）</td><td>当前节点准备接收该 slot 的数据</td><td>- 客户端必须先执行 <code>ASKING</code> 才允许访问<br>- 否则返回 MOVED</td><td>槽迁移开始阶段（目标节点）</td><td><code>CLUSTER SETSLOT 100 IMPORTING a1b2c3...</code></td></tr><tr><td><strong>NODE</strong></td><td><code>CLUSTER SETSLOT &lt;slot&gt; NODE &lt;node-id&gt;</code></td><td>node-id：该 slot 的最终归属节点 ID</td><td>明确该 slot 正式归属某节点</td><td>- 集群路由立即更新<br>- 不再返回 ASK</td><td>槽迁移完成阶段（所有节点）</td><td><code>CLUSTER SETSLOT 100 NODE e5f6g7...</code></td></tr><tr><td><strong>STABLE</strong></td><td><code>CLUSTER SETSLOT &lt;slot&gt; STABLE</code></td><td>无附加参数</td><td>清除 IMPORTING / MIGRATING 标记，恢复稳定状态</td><td>- 恢复正常路由<br>- 不改变 slot 所属节点</td><td>异常恢复 / 状态清理</td><td><code>CLUSTER SETSLOT 100 STABLE</code></td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>一个完整 Slot 迁移示例</p></li></ul><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><br><span class="line">    Slot = 100</span><br><span class="line">    源节点 A = a1b2c3...</span><br><span class="line">    目标节点 B = e5f6g7...</span><br></pre></td></tr></table></figure><p>✅ Step 1：源节点标记迁出</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"># 在节点 A 上执行</span></span><br><span class="line">CLUSTER SETSLOT 100 MIGRATING e5f6g7...</span><br><span class="line"><span class="comment"># 标记状态: Slot 100 正在从 A 迁往 B。</span></span><br></pre></td></tr></table></figure><p>✅ Step 2：目标节点标记导入</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"># 在节点 B 上执行</span></span><br><span class="line">CLUSTER SETSLOT 100 IMPORTING a1b2c3...</span><br><span class="line"><span class="comment"># 标记状态：Slot 100 正在从 A 导入到 B</span></span><br></pre></td></tr></table></figure><p>✅ Step 3：迁移数据（CLUSTER GETKEYSINSLOT + MIGRATE），反复获取 key并迁移</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 在节点 A 上执行，找到 Slot 100 的 key，反复执行，直到 返回 0 个 key</span></span><br><span class="line">CLUSTER GETKEYSINSLOT 100 1000</span><br><span class="line"></span><br><span class="line"><span class="comment"># 迁移数据，分批执行</span></span><br><span class="line">MIGRATE &lt;B-IP&gt; &lt;B-PORT&gt; <span class="string">&quot;&quot;</span> 0 5000 KEYS key1 key2 ... key1000</span><br><span class="line"><span class="comment"># 将数据从 A 节点迁移到 B 节点，可以多次执行</span></span><br></pre></td></tr></table></figure><p>✅ Step 4：设置最终归属</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"># 在所有节点上执行</span></span><br><span class="line">CLUSTER SETSLOT 100 NODE e5f6g7...</span><br><span class="line"><span class="comment"># 标记状态：Slot 100 正式归属 B。</span></span><br></pre></td></tr></table></figure><p>✅ Step 5（可选）：异常清理</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"># 🔍 在哪个节点看到 slot 仍然处于 MIGRATING / IMPORTING，就在哪个节点执行 STABLE。可以通过命令 CLUSTER NODES 查看</span></span><br><span class="line"><span class="comment"># 正常情况下不需要执行，只要运行了 CLUSTER SETSLOT 100 NODE e5f6g7... 就会自动清除这些状态</span></span><br><span class="line"><span class="comment"># 只有在 迁移异常或中断 时才需要。</span></span><br><span class="line">CLUSTER SETSLOT 100 STABLE</span><br><span class="line"><span class="comment"># 用途：清除 MIGRATING / IMPORTING 状态</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>单独 MIGRATE 与 Cluster Slot MIGRATE 对比</p></li></ul><table><thead><tr><th>维度</th><th>MIGRATE 命令</th><th>Redis Cluster Slot 迁移</th></tr></thead><tbody><tr><td>迁移对象</td><td>单个或多个 <strong>Key</strong></td><td>一个或多个 <strong>Slot（包含成千上万 key）</strong></td></tr><tr><td>是否改变 slot 归属</td><td>❌ 不改变</td><td>✅ 会改变</td></tr><tr><td>客户端感知</td><td>客户端无感，但可能访问到旧节点失败</td><td>客户端自动重定向（MOVED / ASK）</td></tr><tr><td>自动路由支持</td><td>❌</td><td>✅</td></tr><tr><td>原子性粒度</td><td>单次 MIGRATE 是原子</td><td>Slot 迁移是分阶段的</td></tr><tr><td>支持在线迁移</td><td>⚠️ 可以，但业务需自行控制</td><td>✅ 天生支持在线迁移</td></tr><tr><td>失败恢复能力</td><td>❌ 需要人工兜底</td><td>✅ Cluster 协议自动修复</td></tr><tr><td>运维复杂度</td><td>低</td><td>高</td></tr><tr><td>自动化程度</td><td>低（需要脚本）</td><td>高（redis-cli --cluster、运维平台）</td></tr><tr><td>典型用途</td><td>数据搬运、修复、临时迁移</td><td>扩容、缩容、负载均衡</td></tr></tbody></table><h3 id="四、节点管理与拓扑">四、节点管理与拓扑</h3><table><thead><tr><th>命令</th><th>语法</th><th>说明</th></tr></thead><tbody><tr><td>CLUSTER MEET</td><td><code>CLUSTER MEET ip port</code></td><td>将指定的节点加入当前集群</td></tr><tr><td>CLUSTER FORGET</td><td><code>CLUSTER FORGET nodeId</code></td><td>从集群移除节点</td></tr><tr><td>CLUSTER NODES</td><td><code>CLUSTER NODES</code></td><td>查看节点列表</td></tr><tr><td>CLUSTER LINKS</td><td><code>CLUSTER LINKS</code></td><td>节点通信链路</td></tr><tr><td>CLUSTER MYID</td><td><code>CLUSTER MYID</code></td><td>当前节点 ID</td></tr><tr><td>CLUSTER MYSHARDID</td><td><code>CLUSTER MYSHARDID</code></td><td>当前 shard ID</td></tr><tr><td>CLUSTER REPLICAS</td><td><code>CLUSTER REPLICAS nodeId</code></td><td>查看指定节点的副本</td></tr><tr><td>CLUSTER SLAVES</td><td><code>CLUSTER SLAVES nodeId</code></td><td>旧命令（等价 replicas）</td></tr><tr><td>CLUSTER REPLICATE</td><td><code>CLUSTER REPLICATE nodeId</code></td><td>将指定的节点设置为当前节点的副本</td></tr></tbody></table><h3 id="五、故障转移与高可用">五、故障转移与高可用</h3><table><thead><tr><th>命令</th><th>语法</th><th>说明</th></tr></thead><tbody><tr><td>CLUSTER FAILOVER</td><td><code>CLUSTER FAILOVER [FORCE|TAKEOVER]</code></td><td>手动触发主从切换</td></tr><tr><td>CLUSTER COUNT-FAILURE-REPORTS</td><td><code>CLUSTER COUNT-FAILURE-REPORTS nodeId</code></td><td>故障投票统计</td></tr></tbody></table><h3 id="六、集群配置与内部控制">六、集群配置与内部控制</h3><table><thead><tr><th>命令</th><th>语法</th><th>说明</th></tr></thead><tbody><tr><td>CLUSTER RESET</td><td><code>CLUSTER RESET [HARD|SOFT]</code></td><td>重置节点</td></tr><tr><td>CLUSTER SAVECONFIG</td><td><code>CLUSTER SAVECONFIG</code></td><td>保存配置</td></tr><tr><td>CLUSTER SET-CONFIG-EPOCH</td><td><code>CLUSTER SET-CONFIG-EPOCH epoch</code></td><td>设置配置版本</td></tr><tr><td>CLUSTER BUMPEPOCH</td><td><code>CLUSTER BUMPEPOCH</code></td><td>自增 epoch</td></tr><tr><td>CLUSTER INFO</td><td><code>CLUSTER INFO</code></td><td>集群状态</td></tr><tr><td>CLUSTER COUNT-FAILURE-REPORTS</td><td><code>CLUSTER COUNT-FAILURE-REPORTS nodeId</code></td><td>故障统计</td></tr></tbody></table>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/01/12/redis7-command-06-cluster-management/</id>
    <link href="https://blog.hanqunfeng.com/2026/01/12/redis7-command-06-cluster-management/"/>
    <published>2026-01-12T12:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">本文基于<code>redis-7.4.7</code></li>
<li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li>
</ul>]]>
    </summary>
    <title>Redis 命令详解：Cluster Management 命令</title>
    <updated>2026-01-12T03:47:25.260Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/redis/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/tags/redis/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">本文基于<code>redis-7.4.7</code></li><li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li><li class="lvl-2"><a href="https://www.runoob.com/lua/lua-tutorial.html">Lua语法参考</a></li></ul><span id="more"></span><h2 id="Scripting-Functions-简介">Scripting / Functions 简介</h2><ul class="lvl-0"><li class="lvl-2">Redis Scripting / Functions 是 Redis 提供的一套“服务器端可编程执行机制”，允许客户端把逻辑发送到 Redis 内部执行，从而：</li></ul><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><br><span class="line">减少网络往返（RTT）</span><br><span class="line">保证并发一致性</span><br><span class="line">支持逻辑复用与版本化管理</span><br></pre></td></tr></table></figure><h2 id="Scripting-Functions-命令详解">Scripting / Functions 命令详解</h2><h3 id="一、Lua-脚本执行与管理">一、Lua 脚本执行与管理</h3><h4 id="1-1-Sctipt-脚本执行-EVAL">1.1 Sctipt 脚本执行(EVAL)</h4><ul class="lvl-0"><li class="lvl-2">用于直接在 Redis 中执行 Lua 脚本。</li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th><th>说明</th></tr></thead><tbody><tr><td>EVAL</td><td><code>EVAL script numkeys key [key ...] arg [arg ...]</code></td><td>script：Lua 脚本<br>numkeys：key 数量</td><td><code>EVAL &quot;return redis.call('GET', KEYS[1])&quot; 1 k1</code></td><td>直接执行脚本</td></tr><tr><td>EVAL_RO</td><td><code>EVAL_RO script numkeys key [key ...] arg [arg ...]</code></td><td>只读执行</td><td><code>EVAL_RO &quot;return redis.call('GET', KEYS[1])&quot; 1 k1</code></td><td>副本安全</td></tr><tr><td>EVALSHA</td><td><code>EVALSHA sha1 numkeys ...</code></td><td>sha1：脚本摘要</td><td><code>EVALSHA abc123 1 k1</code></td><td>缓存执行</td></tr><tr><td>EVALSHA_RO</td><td><code>EVALSHA_RO sha1 numkeys ...</code></td><td>只读缓存执行</td><td>同上</td><td>副本安全</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>核心特性</p></li></ul><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><br><span class="line">可以访问 KEYS / ARGV</span><br><span class="line">会阻塞事件循环（脚本过长有风险）</span><br><span class="line">支持脚本缓存</span><br></pre></td></tr></table></figure><h4 id="1-2-脚本运行管理（SCRIPT）">1.2 脚本运行管理（SCRIPT）</h4><ul class="lvl-0"><li class="lvl-2"><p>用于管理 Lua 脚本缓存和执行状态。</p></li><li class="lvl-2"><p>将脚本加载到缓存中，避免多次执行相同脚本时都要重新发送脚本到 Redis 服务器。</p></li><li class="lvl-2"><p>不会持久化，redis重启后失效</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th><th>说明</th></tr></thead><tbody><tr><td>SCRIPT LOAD</td><td><code>SCRIPT LOAD script</code></td><td>加载脚本并返回 SHA1</td><td><code>SCRIPT LOAD &quot;return 1&quot;</code></td><td>预热，不会持久化，redis重启后失效</td></tr><tr><td>SCRIPT EXISTS</td><td><code>SCRIPT EXISTS sha1 [sha1 ...]</code></td><td>判断是否已缓存</td><td><code>SCRIPT EXISTS abc123</code></td><td>校验</td></tr><tr><td>SCRIPT FLUSH</td><td><code>SCRIPT FLUSH [ASYNC]</code></td><td>清空脚本缓存</td><td><code>SCRIPT FLUSH</code></td><td>运维</td></tr><tr><td>SCRIPT KILL</td><td><code>SCRIPT KILL</code></td><td>终止正在执行脚本</td><td><code>SCRIPT KILL</code></td><td>紧急</td></tr><tr><td>SCRIPT DEBUG</td><td><code>SCRIPT DEBUG YES|SYNC|NO</code></td><td>脚本调试模式</td><td><code>SCRIPT DEBUG YES</code></td><td>调试</td></tr></tbody></table><h3 id="二、Redis-Functions（函数库与调用）">二、Redis Functions（函数库与调用）</h3><ul class="lvl-0"><li class="lvl-2"><p>Redis 7 引入，用于替代大规模 Lua 脚本管理。</p></li></ul><h4 id="2-1-函数调用">2.1 函数调用</h4><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th><th>说明</th></tr></thead><tbody><tr><td>FCALL</td><td><code>FCALL function numkeys key [key ...] arg [arg ...]</code></td><td>function：函数名</td><td><code>FCALL stock.decr 1 stock:1 5</code></td><td>调用函数</td></tr><tr><td>FCALL_RO</td><td><code>FCALL_RO function numkeys ...</code></td><td>只读调用</td><td><code>FCALL_RO metrics.get 1 k1</code></td><td>副本安全</td></tr></tbody></table><h4 id="2-2-函数库管理">2.2 函数库管理</h4><ul class="lvl-0"><li class="lvl-2"><p><code>FUNCTION LOAD</code> 加载的脚本会被持久化保存，重启redis依旧有效。</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th><th>说明</th></tr></thead><tbody><tr><td>FUNCTION LOAD</td><td><code>FUNCTION LOAD [REPLACE] payload</code></td><td>payload：函数源码</td><td><code>FUNCTION LOAD &lt;code&gt;</code></td><td>加载函数,会持久化</td></tr><tr><td>FUNCTION LIST</td><td><code>FUNCTION LIST [LIBRARY lib]</code></td><td>查看函数库</td><td><code>FUNCTION LIST</code></td><td>查询</td></tr><tr><td>FUNCTION DELETE</td><td><code>FUNCTION DELETE library</code></td><td>删除函数库</td><td><code>FUNCTION DELETE mylib</code></td><td>清理</td></tr><tr><td>FUNCTION DUMP</td><td><code>FUNCTION DUMP</code></td><td>导出函数库</td><td><code>FUNCTION DUMP</code></td><td>备份</td></tr><tr><td>FUNCTION RESTORE</td><td><code>FUNCTION RESTORE dump [REPLACE]</code></td><td>恢复函数库</td><td><code>FUNCTION RESTORE &lt;dump&gt;</code></td><td>恢复</td></tr><tr><td>FUNCTION FLUSH</td><td><code>FUNCTION FLUSH [ASYNC]</code></td><td>清空所有函数</td><td><code>FUNCTION FLUSH</code></td><td>高风险</td></tr><tr><td>FUNCTION KILL</td><td><code>FUNCTION KILL</code></td><td>终止正在运行的函数</td><td><code>FUNCTION KILL</code></td><td>紧急停止</td></tr><tr><td>FUNCTION STATS</td><td><code>FUNCTION STATS</code></td><td>运行统计</td><td><code>FUNCTION STATS</code></td><td>监控</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>Redis Functions 的优势</p></li></ul><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">持久化（随 RDB / AOF 保存）</span><br><span class="line">支持版本化与部署</span><br><span class="line">支持多函数库</span><br><span class="line">更适合平台化治理</span><br></pre></td></tr></table></figure><h3 id="Function-源码格式">Function 源码格式</h3><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">#!lua name=ratelimit <span class="comment">-- 这是 Redis Function 专用头声明</span></span><br><span class="line">                     <span class="comment">-- #!lua: 声明这是一个 Lua Function Library，目前只支持 Lua</span></span><br><span class="line">                     <span class="comment">-- name: library，函数库名称，全局唯一</span></span><br><span class="line"></span><br><span class="line">redis.register_function(<span class="string">&#x27;allow&#x27;</span>, <span class="function"><span class="keyword">function</span><span class="params">(keys, args)</span></span> <span class="comment">-- 函数注册 API，</span></span><br><span class="line">                                                      <span class="comment">-- allow : 函数名称</span></span><br><span class="line">                                                      <span class="comment">-- keys: KEY 数组</span></span><br><span class="line">                                                      <span class="comment">-- args: ARGV 数组</span></span><br><span class="line">    xxx                 <span class="comment">-- 函数逻辑</span></span><br><span class="line">    <span class="keyword">return</span> xxx          <span class="comment">-- 函数返回值，只支持 数组/整数/字符串/表格&#123;a,b&#125;作为返回值</span></span><br><span class="line"><span class="keyword">end</span>)                    <span class="comment">-- 函数结束</span></span><br></pre></td></tr></table></figure><h2 id="示例">示例</h2><ul class="lvl-0"><li class="lvl-2"><ul class="lvl-2"><li class="lvl-4">场景：分布式滑动窗口限流器（企业级）</li></ul></li></ul><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><br><span class="line">    每个用户：每分钟最多 5 次请求</span><br><span class="line">    支持：高并发、原子统计、自动过期、可部署升级</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>数据模型设计</p></li></ul><table><thead><tr><th>Key</th><th>类型</th><th>示例</th></tr></thead><tbody><tr><td>rate:{userId}</td><td>ZSET</td><td>score=timestamp</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>初始化数据</p></li></ul><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"># 假设 userId = 1001</span></span><br><span class="line">ZADD rate:1001 1704999990000 1704999990000</span><br><span class="line">ZADD rate:1001 1704999995000 1704999995000</span><br><span class="line">ZADD rate:1001 1704999998000 1704999998000</span><br><span class="line">ZADD rate:1001 1705000001000 1705000001000</span><br><span class="line">ZADD rate:1001 1705000002000 1705000002000</span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置 key 自动过期，window = 60000 ms</span></span><br><span class="line">PEXPIRE rate:1001 60000</span><br></pre></td></tr></table></figure><h3 id="Script-使用示例">Script 使用示例</h3><ul class="lvl-0"><li class="lvl-2"><p>脚本</p></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> rateKey = KEYS[<span class="number">1</span>]</span><br><span class="line"><span class="keyword">local</span> maxReq  = <span class="built_in">tonumber</span>(ARGV[<span class="number">1</span>])</span><br><span class="line"><span class="keyword">local</span> window  = <span class="built_in">tonumber</span>(ARGV[<span class="number">2</span>])</span><br><span class="line"><span class="keyword">local</span> now     = <span class="built_in">tonumber</span>(ARGV[<span class="number">3</span>])</span><br><span class="line"></span><br><span class="line"><span class="keyword">local</span> minTime = now - window</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 1. 清理过期请求</span></span><br><span class="line">redis.call(<span class="string">&quot;ZREMRANGEBYSCORE&quot;</span>, rateKey, <span class="number">0</span>, minTime)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 2. 当前请求数</span></span><br><span class="line"><span class="keyword">local</span> count = <span class="built_in">tonumber</span>(redis.call(<span class="string">&quot;ZCARD&quot;</span>, rateKey))</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> count &gt;= maxReq <span class="keyword">then</span></span><br><span class="line">    <span class="keyword">return</span> count</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 3. 记录请求</span></span><br><span class="line">redis.call(<span class="string">&quot;ZADD&quot;</span>, rateKey, now, now)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 4. 设置自动过期</span></span><br><span class="line">redis.call(<span class="string">&quot;PEXPIRE&quot;</span>, rateKey, window)</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> count+<span class="number">1</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>调用脚本</p></li></ul><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">127.0.0.1:6379&gt; EVAL <span class="string">&quot;local rateKey = KEYS[1] local maxReq  = tonumber(ARGV[1]) local window  = tonumber(ARGV[2]) local now     = tonumber(ARGV[3]) local minTime = now - window redis.call(\&quot;ZREMRANGEBYSCORE\&quot;, rateKey, 0, minTime) local count = tonumber(redis.call(\&quot;ZCARD\&quot;, rateKey)) if count &gt;= maxReq then return count end redis.call(\&quot;ZADD\&quot;, rateKey, now, now) redis.call(\&quot;PEXPIRE\&quot;, rateKey, window) return count+1&quot;</span> 1 rate:1001 5 60000 1705000003000</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>预热后调用</p></li></ul><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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379&gt; SCRIPT LOAD  <span class="string">&quot;local rateKey = KEYS[1] local maxReq  = tonumber(ARGV[1]) local window  = tonumber(ARGV[2]) local now     = tonumber(ARGV[3]) local minTime = now - window redis.call(\&quot;ZREMRANGEBYSCORE\&quot;, rateKey, 0, minTime) local count = tonumber(redis.call(\&quot;ZCARD\&quot;, rateKey)) if count &gt;= maxReq then return count end redis.call(\&quot;ZADD\&quot;, rateKey, now, now) redis.call(\&quot;PEXPIRE\&quot;, rateKey, window) return count+1&quot;</span></span><br><span class="line"><span class="comment">## 脚本 SHA</span></span><br><span class="line"><span class="string">&quot;69ca329ae4744a8b509f9daefea0ddf6415defae&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">## 调用</span></span><br><span class="line">EVALSHA <span class="string">&quot;69ca329ae4744a8b509f9daefea0ddf6415defae&quot;</span> 1 rate:1001 5 60000 1705000003000</span><br></pre></td></tr></table></figure><h4 id="SpringBoot-调用-Script">SpringBoot 调用 Script</h4><ul class="lvl-0"><li class="lvl-2"><p>直接运行脚本</p></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">lua</span> <span class="operator">=</span> <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">                脚本见上文</span></span><br><span class="line"><span class="string">            &quot;&quot;&quot;</span>;</span><br><span class="line"><span class="type">Long</span> <span class="variable">count</span> <span class="operator">=</span> redisTemplate.execute((RedisCallback&lt;Long&gt;) connection -&gt;</span><br><span class="line">        connection.scriptingCommands().eval(</span><br><span class="line">                lua.getBytes(),</span><br><span class="line">                ReturnType.INTEGER,</span><br><span class="line">                <span class="number">1</span>,</span><br><span class="line">                <span class="string">&quot;rate:1001&quot;</span>.getBytes(),</span><br><span class="line">                <span class="string">&quot;5&quot;</span>.getBytes(),</span><br><span class="line">                <span class="string">&quot;60000&quot;</span>.getBytes(),</span><br><span class="line">                <span class="string">&quot;1705000003000&quot;</span>.getBytes()</span><br><span class="line">        )</span><br><span class="line">);</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>预热脚本</p></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">lua</span> <span class="operator">=</span> <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">                脚本见上文</span></span><br><span class="line"><span class="string">            &quot;&quot;&quot;</span>;</span><br><span class="line"><span class="comment">// 预热脚本</span></span><br><span class="line"><span class="keyword">final</span> <span class="type">String</span> <span class="variable">sha</span> <span class="operator">=</span> redisTemplate.execute((RedisCallback&lt;String&gt;) connection -&gt;</span><br><span class="line">        connection.scriptingCommands().scriptLoad(lua.getBytes())</span><br><span class="line">);</span><br><span class="line"><span class="comment">// 执行脚本</span></span><br><span class="line"><span class="keyword">final</span> <span class="type">Long</span> <span class="variable">count</span> <span class="operator">=</span> redisTemplate.execute((RedisCallback&lt;Long&gt;) connection -&gt;</span><br><span class="line">        connection.scriptingCommands().evalSha(</span><br><span class="line">                sha.getBytes(),</span><br><span class="line">                ReturnType.INTEGER,</span><br><span class="line">                <span class="number">1</span>,</span><br><span class="line">                <span class="string">&quot;rate:1001&quot;</span>.getBytes(),</span><br><span class="line">                <span class="string">&quot;5&quot;</span>.getBytes(),</span><br><span class="line">                <span class="string">&quot;60000&quot;</span>.getBytes(),</span><br><span class="line">                <span class="string">&quot;1705000003000&quot;</span>.getBytes()</span><br><span class="line">        )</span><br><span class="line">);</span><br></pre></td></tr></table></figure><h3 id="Function-使用示例">Function 使用示例</h3><ul class="lvl-0"><li class="lvl-2"><p>Function 源码</p></li></ul><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">#!lua name=ratelimit</span><br><span class="line"></span><br><span class="line">redis.register_function(<span class="string">&#x27;allow&#x27;</span>, <span class="function"><span class="keyword">function</span><span class="params">(keys, args)</span></span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">local</span> rateKey = keys[<span class="number">1</span>]</span><br><span class="line">    <span class="keyword">local</span> maxReq  = <span class="built_in">tonumber</span>(args[<span class="number">1</span>])</span><br><span class="line">    <span class="keyword">local</span> window  = <span class="built_in">tonumber</span>(args[<span class="number">2</span>])</span><br><span class="line">    <span class="keyword">local</span> now     = <span class="built_in">tonumber</span>(args[<span class="number">3</span>])</span><br><span class="line"></span><br><span class="line">    <span class="keyword">local</span> minTime = now - window</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 1. 清理过期请求</span></span><br><span class="line">    redis.call(<span class="string">&quot;ZREMRANGEBYSCORE&quot;</span>, rateKey, <span class="number">0</span>, minTime)</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 2. 当前请求数</span></span><br><span class="line">    <span class="keyword">local</span> count = <span class="built_in">tonumber</span>(redis.call(<span class="string">&quot;ZCARD&quot;</span>, rateKey))</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> count &gt;= maxReq <span class="keyword">then</span></span><br><span class="line">        <span class="keyword">return</span> <span class="string">&#x27;notOk&#x27;</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. 记录请求</span></span><br><span class="line">    redis.call(<span class="string">&quot;ZADD&quot;</span>, rateKey, now, now)</span><br><span class="line"></span><br><span class="line">    <span class="comment">-- 4. 设置自动过期</span></span><br><span class="line">    redis.call(<span class="string">&quot;PEXPIRE&quot;</span>, rateKey, window)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="string">&#x27;isOk&#x27;</span></span><br><span class="line"><span class="keyword">end</span>)</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>加载 Function</p></li></ul><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><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 加载 Function</span></span><br><span class="line">127.0.0.1:6379&gt; FUNCTION LOAD <span class="string">&quot;#!lua name=ratelimit \n redis.register_function(&#x27;allow&#x27;, function(keys, args) local rateKey = keys[1] local maxReq  = tonumber(args[1]) local window  = tonumber(args[2]) local now     = tonumber(args[3]) local minTime = now - window redis.call(\&quot;ZREMRANGEBYSCORE\&quot;, rateKey, 0, minTime) local count = tonumber(redis.call(\&quot;ZCARD\&quot;, rateKey)) if count &gt;= maxReq then return count end redis.call(\&quot;ZADD\&quot;, rateKey, now, now) redis.call(\&quot;PEXPIRE\&quot;, rateKey, window) return count+1 end)&quot;</span></span><br><span class="line"><span class="comment">## 输出 library_name</span></span><br><span class="line"><span class="string">&quot;ratelimit&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 列出 Function</span></span><br><span class="line">127.0.0.1:6379&gt; FUNCTION LIST</span><br><span class="line">1) 1) <span class="string">&quot;library_name&quot;</span></span><br><span class="line">   2) <span class="string">&quot;ratelimit&quot;</span></span><br><span class="line">   3) <span class="string">&quot;engine&quot;</span></span><br><span class="line">   4) <span class="string">&quot;LUA&quot;</span></span><br><span class="line">   5) <span class="string">&quot;functions&quot;</span></span><br><span class="line">   6) 1) 1) <span class="string">&quot;name&quot;</span></span><br><span class="line">         2) <span class="string">&quot;allow&quot;</span></span><br><span class="line">         3) <span class="string">&quot;description&quot;</span></span><br><span class="line">         4) (nil)</span><br><span class="line">         5) <span class="string">&quot;flags&quot;</span></span><br><span class="line">         6) (empty array)</span><br><span class="line"></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>调用 Function</p></li></ul><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"># 调用 Function: FCALL function_name num_keys key [key ...] arg [arg ...]</span></span><br><span class="line">FCALL allow 1 rate:1001 5 60000 1705000000000</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>删除 Function</p></li></ul><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">FUNCTION DELETE ratelimit</span><br><span class="line"><span class="comment">## 输出</span></span><br><span class="line"><span class="string">&quot;OK&quot;</span></span><br></pre></td></tr></table></figure><h4 id="SpringBoot-调用-Function">SpringBoot 调用 Function</h4><ul class="lvl-0"><li class="lvl-2"><p>目前 SpringBoot 仅支持 Lua 脚本，不支持 Function，需要自己封装代码</p></li><li class="lvl-2"><p>Function 的返回值仅支持返回字符串，其它类型会抛异常，这是因为<code>Lettuce + RedisTemplate</code> 对 FCALL 的返回类型推断存在缺陷，但对 byte[]（字符串）是稳定可用的。</p></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">lua</span> <span class="operator">=</span> <span class="string">&quot;&quot;&quot;</span></span><br><span class="line"><span class="string">                脚本见上文</span></span><br><span class="line"><span class="string">            &quot;&quot;&quot;</span>;</span><br><span class="line"><span class="type">String</span> <span class="variable">lib_name</span> <span class="operator">=</span> <span class="string">&quot;ratelimit&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 删除 Function</span></span><br><span class="line">redisTemplate.execute((RedisCallback&lt;Object&gt;) connection -&gt;</span><br><span class="line">        connection.execute(<span class="string">&quot;FUNCTION&quot;</span>, <span class="string">&quot;DELETE&quot;</span>.getBytes(), lib_name.getBytes())</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 注册 Function</span></span><br><span class="line">redisTemplate.execute((RedisCallback&lt;Object&gt;) connection -&gt;</span><br><span class="line">        connection.execute(<span class="string">&quot;FUNCTION&quot;</span>, <span class="string">&quot;LOAD&quot;</span>.getBytes(), lua.getBytes())</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 调用 Function</span></span><br><span class="line"><span class="type">Object</span> <span class="variable">result</span> <span class="operator">=</span> redisTemplate.execute((RedisCallback&lt;Object&gt;) connection -&gt;</span><br><span class="line">        connection.execute(<span class="string">&quot;FCALL&quot;</span>,</span><br><span class="line">                <span class="string">&quot;allow&quot;</span>.getBytes(),</span><br><span class="line">                <span class="string">&quot;1&quot;</span>.getBytes(),</span><br><span class="line">                <span class="string">&quot;rate:1001&quot;</span>.getBytes(),</span><br><span class="line">                <span class="string">&quot;5&quot;</span>.getBytes(),</span><br><span class="line">                <span class="string">&quot;60000&quot;</span>.getBytes(),</span><br><span class="line">                <span class="string">&quot;1705000000000&quot;</span>.getBytes())</span><br><span class="line">);</span><br><span class="line"><span class="comment">// 处理返回结果，RedisTemplate 对于 unknown 的 命令 统一返回 byte[]</span></span><br><span class="line"><span class="keyword">if</span> (result <span class="keyword">instanceof</span> <span class="type">byte</span>[] bytes) &#123;</span><br><span class="line">    System.out.println(<span class="keyword">new</span> <span class="title class_">String</span>(bytes, StandardCharsets.UTF_8));</span><br><span class="line">&#125;<span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">IllegalStateException</span>(<span class="string">&quot;Unexpected FCALL return type: &quot;</span> + result.getClass());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/01/11/redis7-command-05-script-function/</id>
    <link href="https://blog.hanqunfeng.com/2026/01/11/redis7-command-05-script-function/"/>
    <published>2026-01-11T15:40:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">本文基于<code>redis-7.4.7</code></li>
<li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li>
<li class="lvl-2"><a href="https://www.runoob.com/lua/lua-tutorial.html">Lua语法参考</a></li>
</ul>]]>
    </summary>
    <title>Redis 命令详解：Scripting / Functions 命令</title>
    <updated>2026-01-16T09:28:51.229Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/redis/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/tags/redis/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">本文基于<code>redis-7.4.7</code></li><li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li></ul><span id="more"></span><h2 id="Connection-Management-简介">Connection Management 简介</h2><ul class="lvl-0"><li class="lvl-2">Connection Management（连接管理） 是 Redis 用于管理客户端与服务器之间连接生命周期、连接状态、协议协商、身份认证、连接行为控制以及连接可观测性的一整套机制与命令集合。</li><li class="lvl-2">在 Redis 内部，每一个客户端连接都会被抽象为一个 client 结构体，包含：</li></ul><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">Socket 连接信息（IP、端口、FD）</span><br><span class="line">协议版本（RESP2 / RESP3）</span><br><span class="line">用户身份（ACL User）</span><br><span class="line">连接名称（Client Name）</span><br><span class="line">阻塞状态（Blocked / Unblocked）</span><br><span class="line">输入输出缓冲区</span><br><span class="line">命令统计信息</span><br><span class="line">Tracking / Caching 状态</span><br><span class="line">最近活动时间</span><br></pre></td></tr></table></figure><h2 id="Connection-Management-命令详解">Connection Management 命令详解</h2><h3 id="一、连接建立-协议协商-基础通信">一、连接建立 / 协议协商 / 基础通信</h3><ul class="lvl-0"><li class="lvl-2"><p>用于客户端与 Redis 建立连接、确认协议版本、心跳检测。</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th><th>典型用途</th></tr></thead><tbody><tr><td>HELLO</td><td><code>HELLO [protover] [AUTH user pass] [SETNAME name]</code></td><td>protover：协议版本（2/3）<br>AUTH：连接时认证<br>SETNAME：设置连接名</td><td><code>HELLO 3 AUTH default pwd SETNAME app-1</code></td><td>协议协商、一次性完成认证</td></tr><tr><td>AUTH</td><td><code>AUTH [username] password</code></td><td>用户名可选（ACL 模式）</td><td><code>AUTH app secret</code></td><td>登录认证</td></tr><tr><td>PING</td><td><code>PING [message]</code></td><td>message：可选回显内容</td><td><code>PING</code></td><td>心跳检测</td></tr><tr><td>ECHO</td><td><code>ECHO message</code></td><td>message：任意字符串</td><td><code>ECHO hello</code></td><td>连通性测试</td></tr><tr><td>QUIT</td><td><code>QUIT</code></td><td>无</td><td><code>QUIT</code></td><td>关闭连接</td></tr></tbody></table><h3 id="二、客户端身份与连接信息">二、客户端身份与连接信息</h3><ul class="lvl-0"><li class="lvl-2"><p>用于识别和查询客户端连接状态。</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th><th>典型用途</th></tr></thead><tbody><tr><td>CLIENT ID</td><td><code>CLIENT ID</code></td><td>返回当前连接 ID</td><td><code>CLIENT ID</code></td><td>连接唯一标识</td></tr><tr><td>CLIENT GETNAME</td><td><code>CLIENT GETNAME</code></td><td>获取连接名称</td><td><code>CLIENT GETNAME</code></td><td>连接识别</td></tr><tr><td>CLIENT SETNAME</td><td><code>CLIENT SETNAME name</code></td><td>设置连接名称</td><td><code>CLIENT SETNAME order-service</code></td><td>连接可观测性</td></tr><tr><td>CLIENT INFO</td><td><code>CLIENT INFO</code></td><td>返回当前客户端详细信息</td><td><code>CLIENT INFO</code></td><td>调试连接状态</td></tr><tr><td>CLIENT LIST</td><td><code>CLIENT LIST [TYPE type] [ID id]</code></td><td>列出所有客户端</td><td><code>CLIENT LIST TYPE normal</code></td><td>运维诊断</td></tr><tr><td>CLIENT GETREDIR</td><td><code>CLIENT GETREDIR</code></td><td>返回客户端重定向状态</td><td><code>CLIENT GETREDIR</code></td><td>集群调试</td></tr></tbody></table><h3 id="三、客户端连接控制-生命周期管理">三、客户端连接控制 / 生命周期管理</h3><ul class="lvl-0"><li class="lvl-2"><p>用于管理其他客户端连接。</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th><th>典型用途</th></tr></thead><tbody><tr><td>CLIENT KILL</td><td><code>CLIENT KILL &lt;filter&gt;</code></td><td>按条件关闭连接</td><td><code>CLIENT KILL TYPE normal</code></td><td>清理异常连接</td></tr><tr><td>CLIENT UNBLOCK</td><td><code>CLIENT UNBLOCK client-id [TIMEOUT|ERROR]</code></td><td>解除阻塞客户端</td><td><code>CLIENT UNBLOCK 1234</code></td><td>解死锁</td></tr><tr><td>CLIENT PAUSE</td><td><code>CLIENT PAUSE timeout [WRITE|ALL]</code></td><td>暂停客户端命令处理</td><td><code>CLIENT PAUSE 1000 WRITE</code></td><td>流量削峰</td></tr><tr><td>CLIENT UNPAUSE</td><td><code>CLIENT UNPAUSE</code></td><td>恢复处理</td><td><code>CLIENT UNPAUSE</code></td><td>恢复服务</td></tr><tr><td>CLIENT REPLY</td><td><code>CLIENT REPLY ON|OFF|SKIP</code></td><td>控制是否返回响应</td><td><code>CLIENT REPLY OFF</code></td><td>管道优化</td></tr></tbody></table><h3 id="四、客户端行为控制（缓存、访问、跟踪）">四、客户端行为控制（缓存、访问、跟踪）</h3><ul class="lvl-0"><li class="lvl-2"><p>用于控制客户端与 Redis 的交互行为。</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th><th>典型用途</th></tr></thead><tbody><tr><td>CLIENT CACHING</td><td><code>CLIENT CACHING YES|NO</code></td><td>开启/关闭客户端缓存</td><td><code>CLIENT CACHING YES</code></td><td>客户端缓存优化</td></tr><tr><td>CLIENT TRACKING</td><td><code>CLIENT TRACKING ON [options]</code></td><td>开启 key 失效跟踪</td><td><code>CLIENT TRACKING ON</code></td><td>客户端缓存一致性</td></tr><tr><td>CLIENT TRACKINGINFO</td><td><code>CLIENT TRACKINGINFO</code></td><td>查询跟踪状态</td><td><code>CLIENT TRACKINGINFO</code></td><td>调试</td></tr><tr><td>CLIENT NO-TOUCH</td><td><code>CLIENT NO-TOUCH ON|OFF</code></td><td>禁止更新 LRU/LFU</td><td><code>CLIENT NO-TOUCH ON</code></td><td>热度统计控制</td></tr><tr><td>CLIENT NO-EVICT</td><td><code>CLIENT NO-EVICT ON|OFF</code></td><td>禁止触发淘汰</td><td><code>CLIENT NO-EVICT ON</code></td><td>防止误淘汰</td></tr><tr><td>CLIENT SETINFO</td><td><code>CLIENT SETINFO LIB-NAME name</code></td><td>设置客户端库信息</td><td><code>CLIENT SETINFO LIB-NAME my-sdk</code></td><td>可观测性</td></tr></tbody></table><h3 id="五、协议行为与响应控制">五、协议行为与响应控制</h3><ul class="lvl-0"><li class="lvl-2"><p>用于调试和高级客户端行为控制。</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th><th>典型用途</th></tr></thead><tbody><tr><td>CLIENT REPLY</td><td><code>CLIENT REPLY ON|OFF|SKIP</code></td><td>控制是否接收响应</td><td><code>CLIENT REPLY SKIP</code></td><td>Pipeline 优化</td></tr><tr><td>CLIENT GETREDIR</td><td><code>CLIENT GETREDIR</code></td><td>获取重定向信息</td><td><code>CLIENT GETREDIR</code></td><td>集群调试</td></tr></tbody></table>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/01/11/redis7-command-04-connection-management/</id>
    <link href="https://blog.hanqunfeng.com/2026/01/11/redis7-command-04-connection-management/"/>
    <published>2026-01-11T15:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">本文基于<code>redis-7.4.7</code></li>
<li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li>
</ul>]]>
    </summary>
    <title>Redis 命令详解：Connection Management 命令</title>
    <updated>2026-01-11T05:42:18.496Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/redis/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/tags/redis/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">本文基于<code>redis-7.4.7</code></li><li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li></ul><span id="more"></span><h2 id="Server-Management-简介">Server Management 简介</h2><ul class="lvl-0"><li class="lvl-2">Server Management Commands 是用于管理 Redis 服务器实例本身运行状态、资源、配置、安全、复制、持久化、模块和诊断能力的一组系统级命令。</li><li class="lvl-2">不建议在业务代码中调用。</li><li class="lvl-2">不操作业务数据内容，而是操作：</li></ul><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></pre></td><td class="code"><pre><span class="line">Redis 服务进程状态</span><br><span class="line">内存 / CPU / IO</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></pre></td></tr></table></figure><h2 id="Server-Management-命令详解">Server Management 命令详解</h2><h3 id="一、ACL-权限与安全管理">一、ACL 权限与安全管理</h3><ul class="lvl-0"><li class="lvl-2"><p>关于 ACL 权限的具体说明，请查看 <a href="/2025/12/07/redis7-acl/" title="Redis 7 + ACL 简介">Redis 7 + ACL 简介</a></p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>ACL CAT</td><td><code>ACL CAT [category]</code></td><td>查看命令分类或某分类下命令</td><td><code>ACL CAT admin</code></td></tr><tr><td>ACL USERS</td><td><code>ACL USERS</code></td><td>列出所有用户</td><td><code>ACL USERS</code></td></tr><tr><td>ACL WHOAMI</td><td><code>ACL WHOAMI</code></td><td>显示当前连接用户</td><td><code>ACL WHOAMI</code></td></tr><tr><td>ACL GETUSER</td><td><code>ACL GETUSER username</code></td><td>查询用户权限</td><td><code>ACL GETUSER app</code></td></tr><tr><td>ACL SETUSER</td><td><code>ACL SETUSER username [rule ...]</code></td><td>创建/修改用户规则</td><td><code>ACL SETUSER app on &gt;pwd ~* +get</code></td></tr><tr><td>ACL DELUSER</td><td><code>ACL DELUSER username [username ...]</code></td><td>删除用户</td><td><code>ACL DELUSER test</code></td></tr><tr><td>ACL LIST</td><td><code>ACL LIST</code></td><td>列出 ACL 配置规则</td><td><code>ACL LIST</code></td></tr><tr><td>ACL LOAD</td><td><code>ACL LOAD</code></td><td>从配置文件加载 ACL</td><td><code>ACL LOAD</code></td></tr><tr><td>ACL SAVE</td><td><code>ACL SAVE</code></td><td>将 ACL 写入磁盘</td><td><code>ACL SAVE</code></td></tr><tr><td>ACL LOG</td><td><code>ACL LOG [count|RESET]</code></td><td>查看权限拒绝日志</td><td><code>ACL LOG 10</code></td></tr><tr><td>ACL DRYRUN</td><td><code>ACL DRYRUN username command [args...]</code></td><td>模拟权限校验</td><td><code>ACL DRYRUN app GET k1</code></td></tr><tr><td>ACL GENPASS</td><td><code>ACL GENPASS [bits]</code></td><td>生成随机密码</td><td><code>ACL GENPASS 128</code></td></tr></tbody></table><h3 id="二、持久化与后台任务">二、持久化与后台任务</h3><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>SAVE</td><td><code>SAVE</code></td><td>同步生成 RDB 快照（阻塞）</td><td><code>SAVE</code></td></tr><tr><td>BGSAVE</td><td><code>BGSAVE</code></td><td>后台生成 RDB</td><td><code>BGSAVE</code></td></tr><tr><td>BGREWRITEAOF</td><td><code>BGREWRITEAOF</code></td><td>重写 AOF 文件</td><td><code>BGREWRITEAOF</code></td></tr><tr><td>LASTSAVE</td><td><code>LASTSAVE</code></td><td>最近一次 RDB 保存时间</td><td><code>LASTSAVE</code></td></tr><tr><td>SHUTDOWN</td><td><code>SHUTDOWN [NOSAVE|SAVE]</code></td><td>关闭 Redis 实例</td><td><code>SHUTDOWN SAVE</code></td></tr></tbody></table><h3 id="三、命令元信息与能力发现">三、命令元信息与能力发现</h3><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>COMMAND</td><td><code>COMMAND</code></td><td>返回所有命令信息</td><td><code>COMMAND</code></td></tr><tr><td>COMMAND COUNT</td><td><code>COMMAND COUNT</code></td><td>返回命令总数</td><td><code>COMMAND COUNT</code></td></tr><tr><td>COMMAND LIST</td><td><code>COMMAND LIST</code></td><td>返回命令列表</td><td><code>COMMAND LIST</code></td></tr><tr><td>COMMAND INFO</td><td><code>COMMAND INFO cmd [cmd ...]</code></td><td>查询命令元数据</td><td><code>COMMAND INFO GET SET</code></td></tr><tr><td>COMMAND DOCS</td><td><code>COMMAND DOCS [cmd ...]</code></td><td>返回命令文档</td><td><code>COMMAND DOCS GET</code></td></tr><tr><td>COMMAND GETKEYS</td><td><code>COMMAND GETKEYS cmd args...</code></td><td>解析命令中的 key</td><td><code>COMMAND GETKEYS MSET a 1 b 2</code></td></tr><tr><td>COMMAND GETKEYSANDFLAGS</td><td><code>COMMAND GETKEYSANDFLAGS cmd args...</code></td><td>返回 key 与访问标志</td><td><code>COMMAND GETKEYSANDFLAGS SET k v</code></td></tr></tbody></table><h3 id="四、配置与运行状态">四、配置与运行状态</h3><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>CONFIG GET</td><td><code>CONFIG GET pattern</code></td><td>查询配置</td><td><code>CONFIG GET maxmemory*</code></td></tr><tr><td>CONFIG SET</td><td><code>CONFIG SET key value</code></td><td>修改配置</td><td><code>CONFIG SET timeout 300</code></td></tr><tr><td>CONFIG RESETSTAT</td><td><code>CONFIG RESETSTAT</code></td><td>重置统计信息</td><td><code>CONFIG RESETSTAT</code></td></tr><tr><td>CONFIG REWRITE</td><td><code>CONFIG REWRITE</code></td><td>重写配置文件</td><td><code>CONFIG REWRITE</code></td></tr><tr><td>INFO</td><td><code>INFO [section]</code></td><td>查看运行状态</td><td><code>INFO memory</code></td></tr><tr><td>DBSIZE</td><td><code>DBSIZE</code></td><td>当前 DB key 数量</td><td><code>DBSIZE</code></td></tr><tr><td>TIME</td><td><code>TIME</code></td><td>返回服务器时间</td><td><code>TIME</code></td></tr><tr><td>LOLWUT</td><td><code>LOLWUT</code></td><td>调试彩蛋命令</td><td><code>LOLWUT</code></td></tr></tbody></table><h3 id="五、数据库管理">五、数据库管理</h3><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>FLUSHDB</td><td><code>FLUSHDB [ASYNC]</code></td><td>清空当前 DB</td><td><code>FLUSHDB ASYNC</code></td></tr><tr><td>FLUSHALL</td><td><code>FLUSHALL [ASYNC]</code></td><td>清空所有 DB</td><td><code>FLUSHALL</code></td></tr><tr><td>SWAPDB</td><td><code>SWAPDB index1 index2</code></td><td>交换两个 DB</td><td><code>SWAPDB 0 1</code></td></tr></tbody></table><h3 id="六、复制、主从、高可用">六、复制、主从、高可用</h3><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>REPLICAOF</td><td><code>REPLICAOF host port</code></td><td>设置为从节点</td><td><code>REPLICAOF 10.0.0.1 6379</code></td></tr><tr><td>SLAVEOF</td><td><code>SLAVEOF host port</code></td><td>REPLICAOF (旧别名)</td><td><code>SLAVEOF NO ONE</code></td></tr><tr><td>SYNC</td><td><code>SYNC</code></td><td>全量复制（旧协议）</td><td><code>SYNC</code></td></tr><tr><td>PSYNC</td><td><code>PSYNC replid offset</code></td><td>增量复制</td><td><code>PSYNC ? -1</code></td></tr><tr><td>REPLCONF</td><td><code>REPLCONF option value</code></td><td>复制参数协商</td><td><code>REPLCONF capa eof</code></td></tr><tr><td>ROLE</td><td><code>ROLE</code></td><td>查询节点角色</td><td><code>ROLE</code></td></tr><tr><td>FAILOVER</td><td><code>FAILOVER [TO host port]</code></td><td>触发主从切换</td><td><code>FAILOVER</code></td></tr><tr><td>RESTORE-ASKING</td><td><code>RESTORE-ASKING</code></td><td>集群迁移辅助</td><td><code>RESTORE-ASKING</code></td></tr></tbody></table><h3 id="七、延迟与慢查询诊断">七、延迟与慢查询诊断</h3><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>LATENCY DOCTOR</td><td><code>LATENCY DOCTOR</code></td><td>自动诊断延迟问题</td><td><code>LATENCY DOCTOR</code></td></tr><tr><td>LATENCY GRAPH</td><td><code>LATENCY GRAPH event</code></td><td>延迟图</td><td><code>LATENCY GRAPH command</code></td></tr><tr><td>LATENCY HISTOGRAM</td><td><code>LATENCY HISTOGRAM event</code></td><td>延迟分布</td><td><code>LATENCY HISTOGRAM command</code></td></tr><tr><td>LATENCY HISTORY</td><td><code>LATENCY HISTORY event</code></td><td>历史记录</td><td><code>LATENCY HISTORY command</code></td></tr><tr><td>LATENCY LATEST</td><td><code>LATENCY LATEST</code></td><td>最近延迟事件</td><td><code>LATENCY LATEST</code></td></tr><tr><td>LATENCY RESET</td><td><code>LATENCY RESET [event]</code></td><td>重置统计</td><td><code>LATENCY RESET</code></td></tr><tr><td>SLOWLOG GET</td><td><code>SLOWLOG GET [n]</code></td><td>获取慢日志</td><td><code>SLOWLOG GET 10</code></td></tr><tr><td>SLOWLOG LEN</td><td><code>SLOWLOG LEN</code></td><td>慢日志条数</td><td><code>SLOWLOG LEN</code></td></tr><tr><td>SLOWLOG RESET</td><td><code>SLOWLOG RESET</code></td><td>清空慢日志</td><td><code>SLOWLOG RESET</code></td></tr><tr><td>MONITOR</td><td><code>MONITOR</code></td><td>实时监听所有命令</td><td><code>MONITOR</code></td></tr></tbody></table><h3 id="八、内存诊断与优化">八、内存诊断与优化</h3><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>MEMORY USAGE</td><td><code>MEMORY USAGE key [SAMPLES n]</code></td><td>key 占用内存</td><td><code>MEMORY USAGE k1</code></td></tr><tr><td>MEMORY STATS</td><td><code>MEMORY STATS</code></td><td>内存统计</td><td><code>MEMORY STATS</code></td></tr><tr><td>MEMORY DOCTOR</td><td><code>MEMORY DOCTOR</code></td><td>内存问题诊断</td><td><code>MEMORY DOCTOR</code></td></tr><tr><td>MEMORY PURGE</td><td><code>MEMORY PURGE</code></td><td>释放碎片</td><td><code>MEMORY PURGE</code></td></tr><tr><td>MEMORY MALLOC-STATS</td><td><code>MEMORY MALLOC-STATS</code></td><td>分配器统计</td><td><code>MEMORY MALLOC-STATS</code></td></tr></tbody></table><h3 id="九、模块管理（Redis-Modules）">九、模块管理（Redis Modules）</h3><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>MODULE LIST</td><td><code>MODULE LIST</code></td><td>查看已加载模块</td><td><code>MODULE LIST</code></td></tr><tr><td>MODULE LOAD</td><td><code>MODULE LOAD path [args...]</code></td><td>加载模块</td><td><code>MODULE LOAD /opt/redisearch.so</code></td></tr><tr><td>MODULE LOADEX</td><td><code>MODULE LOADEX path [CONFIG ...]</code></td><td>扩展加载参数</td><td><code>MODULE LOADEX mod.so CONFIG a 1</code></td></tr><tr><td>MODULE UNLOAD</td><td><code>MODULE UNLOAD name</code></td><td>卸载模块</td><td><code>MODULE UNLOAD redisearch</code></td></tr></tbody></table>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/01/11/redis7-command-03-server-management/</id>
    <link href="https://blog.hanqunfeng.com/2026/01/11/redis7-command-03-server-management/"/>
    <published>2026-01-11T14:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">本文基于<code>redis-7.4.7</code></li>
<li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li>
</ul>]]>
    </summary>
    <title>Redis 命令详解：Server Management 命令</title>
    <updated>2026-01-11T05:34:33.747Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/redis/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/tags/redis/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">本文基于<code>redis-7.4.7</code></li><li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li></ul><span id="more"></span><h2 id="通用命令-简介">通用命令 简介</h2><ul class="lvl-0"><li class="lvl-2">不依赖于具体数据类型，对所有 Key 或 Redis 对象都通用的一组基础管理命令。</li><li class="lvl-2">这些命令作用在 Key 层面、对象元数据层面、存储管理层面、复制与持久化层面，而不是具体的数据结构内容。</li></ul><table><thead><tr><th>特征</th><th>说明</th></tr></thead><tbody><tr><td>与数据类型无关</td><td>不关心 value 是 String、Hash、List、JSON、Bitmap 等</td></tr><tr><td>作用对象是 Key 或对象元数据</td><td>如 TTL、类型、编码、引用计数、是否存在</td></tr><tr><td>管理属性为主</td><td>生命周期、复制、迁移、删除、遍历</td></tr><tr><td>可用于所有 Redis 模块数据</td><td>RedisJSON、RediSearch、TimeSeries 等同样适用</td></tr><tr><td>运维和系统级使用频繁</td><td>监控、迁移、数据治理、容量管理</td></tr></tbody></table><h2 id="通用命令-命令详解">通用命令 命令详解</h2><h3 id="一、Key-生命周期-过期时间管理">一、Key 生命周期 / 过期时间管理</h3><ul class="lvl-0"><li class="lvl-2"><p>用于设置、查询、取消 Key 的过期时间。</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>EXPIRE</td><td><code>EXPIRE key seconds [NX|XX|GT|LT]</code></td><td>seconds：过期秒数<br>NX：仅当 key 没有过期时间时设置<br>XX：仅当 key 已有过期时间时设置<br>GT：仅当新过期时间大于旧值<br>LT：仅当新过期时间小于旧值</td><td><code>EXPIRE user:1 60</code></td></tr><tr><td>PEXPIRE</td><td><code>PEXPIRE key milliseconds [NX|XX|GT|LT]</code></td><td>milliseconds：过期毫秒数</td><td><code>PEXPIRE user:1 1500</code></td></tr><tr><td>EXPIREAT</td><td><code>EXPIREAT key unix_timestamp [NX|XX|GT|LT]</code></td><td>unix_timestamp：秒级时间戳</td><td><code>EXPIREAT user:1 1737000000</code></td></tr><tr><td>PEXPIREAT</td><td><code>PEXPIREAT key unix_milliseconds_timestamp [NX|XX|GT|LT]</code></td><td>毫秒级 Unix 时间戳</td><td><code>PEXPIREAT user:1 1737000000123</code></td></tr><tr><td>TTL</td><td><code>TTL key</code></td><td>返回剩余过期时间（秒）<br>-1：永久 key<br>-2：不存在</td><td><code>TTL user:1</code></td></tr><tr><td>PTTL</td><td><code>PTTL key</code></td><td>返回剩余过期时间（毫秒）</td><td><code>PTTL user:1</code></td></tr><tr><td>EXPIRETIME</td><td><code>EXPIRETIME key</code></td><td>返回过期时间（秒级时间戳）</td><td><code>EXPIRETIME user:1</code></td></tr><tr><td>PEXPIRETIME</td><td><code>PEXPIRETIME key</code></td><td>返回过期时间（毫秒级时间戳）</td><td><code>PEXPIRETIME user:1</code></td></tr><tr><td>PERSIST</td><td><code>PERSIST key</code></td><td>移除过期时间，使 key 永久存在</td><td><code>PERSIST user:1</code></td></tr><tr><td>TOUCH</td><td><code>TOUCH key [key ...]</code></td><td>更新 key 最近访问时间，返回成功更新数量</td><td><code>TOUCH k1 k2</code></td></tr></tbody></table><h3 id="二、Key-查询与遍历">二、Key 查询与遍历</h3><ul class="lvl-0"><li class="lvl-2"><p>用于查询 key 是否存在、类型、批量扫描。</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>EXISTS</td><td><code>EXISTS key [key ...]</code></td><td>返回存在的 key 数量</td><td><code>EXISTS a b c</code></td></tr><tr><td>TYPE</td><td><code>TYPE key</code></td><td>返回 key 的数据类型</td><td><code>TYPE user:1</code></td></tr><tr><td>KEYS</td><td><code>KEYS pattern</code></td><td>按模式匹配所有 key（生产慎用）</td><td><code>KEYS user:*</code></td></tr><tr><td>SCAN</td><td><code>SCAN cursor [MATCH pattern] [COUNT count]</code></td><td>cursor：游标<br>MATCH：匹配模式<br>COUNT：期望返回数量</td><td><code>SCAN 0 MATCH user:* COUNT 100</code></td></tr><tr><td>RANDOMKEY</td><td><code>RANDOMKEY</code></td><td>随机返回一个 key</td><td><code>RANDOMKEY</code></td></tr></tbody></table><h3 id="三、Key-修改-删除-复制-移动">三、Key 修改 / 删除 / 复制 / 移动</h3><ul class="lvl-0"><li class="lvl-2"><p>用于 key 的生命周期与位置管理。</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>DEL</td><td><code>DEL key [key ...]</code></td><td>同步删除 key</td><td><code>DEL k1 k2</code></td></tr><tr><td>UNLINK</td><td><code>UNLINK key [key ...]</code></td><td>异步删除 key（大 key 推荐）</td><td><code>UNLINK bigkey</code></td></tr><tr><td>COPY</td><td><code>COPY source destination [DB db] [REPLACE]</code></td><td>DB：目标数据库<br>REPLACE：覆盖目标</td><td><code>COPY k1 k2 REPLACE</code></td></tr><tr><td>MOVE</td><td><code>MOVE key db</code></td><td>db：目标数据库编号</td><td><code>MOVE user:1 1</code></td></tr><tr><td>RENAME</td><td><code>RENAME key newkey</code></td><td>强制覆盖目标 key</td><td><code>RENAME a b</code></td></tr><tr><td>RENAMENX</td><td><code>RENAMENX key newkey</code></td><td>目标不存在时才重命名</td><td><code>RENAMENX a b</code></td></tr><tr><td>MIGRATE</td><td><code>MIGRATE host port &lt;key | &quot;&quot;&gt; destination-db timeout [COPY] [REPLACE] [AUTH password | AUTH2 username password] [KEYS key [key ...]]</code></td><td>用于跨实例迁移 key</td><td><code>MIGRATE 127.0.0.1 6379 k1 0 5000</code></td></tr></tbody></table><h4 id="MIGRATE-—-命令总览">MIGRATE — 命令总览</h4><ul class="lvl-0"><li class="lvl-2"><p>将一个或多个键 原子性地 从当前 Redis 实例迁移到另一个 Redis 实例的指定数据库。迁移成功后：</p></li></ul><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><br><span class="line">默认源实例上该键将被删除（除非使用 COPY）</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>该命令保证一致性，在迁移期间两个实例都会被阻塞一段时间。</p></li><li class="lvl-2"><p>参数说明表</p></li></ul><table><thead><tr><th>参数</th><th>类型</th><th>含义</th><th>是否必需</th></tr></thead><tbody><tr><td><strong>host</strong></td><td>string</td><td>目标 Redis 实例主机名或 IP</td><td>是</td></tr><tr><td><strong>port</strong></td><td>integer</td><td>目标 Redis 实例端口</td><td>是</td></tr><tr><td><strong>&lt;key | “”&gt;</strong></td><td>string</td><td>单个要迁移的键名，或空字符串用于批量模式</td><td>是</td></tr><tr><td><strong>destination-db</strong></td><td>integer</td><td>目标实例上要写入的数据库索引（0–15）</td><td>是</td></tr><tr><td><strong>timeout</strong></td><td>integer</td><td>最大允许的空闲 I/O 超时时间（毫秒）</td><td>是</td></tr><tr><td><strong>COPY</strong></td><td>keyword</td><td>迁移后不删除源实例上的键</td><td>否</td></tr><tr><td><strong>REPLACE</strong></td><td>keyword</td><td>如果目标实例已存在同名键则覆盖</td><td>否</td></tr><tr><td><strong>AUTH password</strong></td><td>credential</td><td>用于目标实例密码认证（旧 ACL 模式）</td><td>否</td></tr><tr><td><strong>AUTH2 username password</strong></td><td>credential</td><td>用于目标实例 ACL 用户名+密码认证</td><td>否</td></tr><tr><td><strong>KEYS key [key …]</strong></td><td>list</td><td>如果前面的 key 是空字符串，则使用这一组键名批量迁移</td><td>否</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>示例</p></li></ul><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></pre></td><td class="code"><pre><span class="line"><span class="comment"># 示例 1：迁移单个 key</span></span><br><span class="line">MIGRATE 192.168.0.100 6379 user:123 0 5000</span><br><span class="line"></span><br><span class="line"><span class="comment"># 示例 2：复制 key 而不删除源</span></span><br><span class="line">MIGRATE 10.0.0.2 6379 cache:session 1 3000 COPY</span><br><span class="line"></span><br><span class="line"><span class="comment"># 示例 3：覆盖目标已有 key</span></span><br><span class="line">MIGRATE 10.0.0.5 6379 app:data 2 2000 REPLACE</span><br><span class="line"></span><br><span class="line"><span class="comment"># 示例 4：迁移多个 key</span></span><br><span class="line">MIGRATE 10.0.0.10 6379 <span class="string">&quot;&quot;</span> 0 8000 KEYS orders:1 orders:2 orders:3</span><br><span class="line"></span><br><span class="line"><span class="comment"># 示例 5：带目标认证</span></span><br><span class="line">MIGRATE 10.0.0.20 6379 user:token 1 5000 AUTH s3cr3t REPLACE</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>典型使用场景</p></li></ul><table><thead><tr><th>场景</th><th>应用举例</th></tr></thead><tbody><tr><td>单节点间数据迁移</td><td>从 dev Redis 到 prod Redis</td></tr><tr><td>大库数据切分</td><td>数据重分片</td></tr><tr><td>集群迁移前准备</td><td>手动分配 slot 关联 key</td></tr><tr><td>灾备数据同步</td><td>复制业务数据库的部分 key</td></tr></tbody></table><h3 id="四、序列化-备份-恢复">四、序列化 / 备份 / 恢复</h3><ul class="lvl-0"><li class="lvl-2"><p>用于数据导出和导入。</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>DUMP</td><td><code>DUMP key</code></td><td>返回序列化后的二进制值</td><td><code>DUMP user:1</code></td></tr><tr><td>RESTORE</td><td><code>RESTORE key ttl serialized-value [REPLACE]</code></td><td>ttl：毫秒<br>serialized-value：DUMP 输出</td><td><code>RESTORE user:2 0 &quot;&lt;dump&gt;&quot; REPLACE</code></td></tr></tbody></table><h3 id="五、排序相关">五、排序相关</h3><ul class="lvl-0"><li class="lvl-2"><p>对集合数据进行排序。</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>SORT</td><td><code>SORT key [BY pattern] [LIMIT offset count] [GET pattern ...] [ASC|DESC] [ALPHA] [STORE dest]</code></td><td>支持排序、分页、映射、存储</td><td><code>SORT mylist ASC STORE sorted:list</code></td></tr><tr><td>SORT_RO</td><td><code>SORT_RO key ...</code></td><td>只读排序（不会修改数据）</td><td><code>SORT_RO mylist DESC</code></td></tr></tbody></table><h3 id="六、对象内部信息（调试-运维）">六、对象内部信息（调试 / 运维）</h3><ul class="lvl-0"><li class="lvl-2"><p>用于诊断 Redis 内部对象状态。</p></li></ul><table><thead><tr><th>命令</th><th>语法</th><th>参数说明</th><th>示例</th></tr></thead><tbody><tr><td>OBJECT ENCODING</td><td><code>OBJECT ENCODING key</code></td><td>返回内部编码方式</td><td><code>OBJECT ENCODING user:1</code></td></tr><tr><td>OBJECT FREQ</td><td><code>OBJECT FREQ key</code></td><td>返回 LFU 访问频率</td><td><code>OBJECT FREQ user:1</code></td></tr><tr><td>OBJECT IDLETIME</td><td><code>OBJECT IDLETIME key</code></td><td>返回空闲时间（秒）</td><td><code>OBJECT IDLETIME user:1</code></td></tr><tr><td>OBJECT REFCOUNT</td><td><code>OBJECT REFCOUNT key</code></td><td>返回引用计数</td><td><code>OBJECT REFCOUNT user:1</code></td></tr></tbody></table><h3 id="七、复制一致性-持久化确认">七、复制一致性 / 持久化确认</h3><ul class="lvl-0"><li class="lvl-2"><p>主要用于 写入可靠性保证、强一致性场景。</p></li></ul><h4 id="7-1-WAIT-——-等待副本确认">7.1 WAIT —— 等待副本确认</h4><table><thead><tr><th>项目</th><th>内容</th></tr></thead><tbody><tr><td>命令</td><td>WAIT</td></tr><tr><td>语法</td><td><code>WAIT numreplicas timeout</code></td></tr><tr><td>参数说明</td><td>numreplicas：需要确认写入的副本数量<br>timeout：最大等待时间（毫秒）</td></tr><tr><td>返回值</td><td>实际确认的副本数量</td></tr><tr><td>作用</td><td>阻塞当前客户端，直到之前的写命令被至少 numreplicas 个副本确认</td></tr><tr><td>典型场景</td><td>强一致写、主从同步确认、金融类写入</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>示例</p></li></ul><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">SET order:1 PAID</span><br><span class="line">WAIT 2 5000</span><br><span class="line"><span class="comment"># 含义：</span></span><br><span class="line"><span class="comment">#     等待至少 2 个副本确认该写入。</span></span><br><span class="line"><span class="comment">#     最多等待 5 秒。</span></span><br><span class="line"><span class="comment">#     如果 5 秒内只确认 1 个副本，则返回 1。</span></span><br><span class="line"><span class="comment">#     返回0，则表示没有副本确认该写入。</span></span><br></pre></td></tr></table></figure><h4 id="7-2-WAITAOF-——-等待-AOF-和副本持久化确认">7.2 WAITAOF —— 等待 AOF 和副本持久化确认</h4><ul class="lvl-0"><li class="lvl-2"><p>Redis 7+ 新增，用于保证 本地 AOF fsync + 副本同步 的写入可靠性。</p></li></ul><table><thead><tr><th>项目</th><th>内容</th></tr></thead><tbody><tr><td>命令</td><td>WAITAOF</td></tr><tr><td>语法</td><td><code>WAITAOF numlocal numreplicas timeout</code></td></tr><tr><td>参数说明</td><td>numlocal：本地 AOF fsync 确认数量（0/1）<br>numreplicas：副本确认数量<br>timeout：最大等待时间（毫秒）</td></tr><tr><td>返回值</td><td>数组：[local_acks, replica_acks]</td></tr><tr><td>作用</td><td>阻塞直到写入被本地 AOF 持久化和/或副本确认</td></tr><tr><td>典型场景</td><td>金融交易、强持久化一致性要求</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>参数语义说明</p></li></ul><table><thead><tr><th>参数</th><th>含义</th></tr></thead><tbody><tr><td>numlocal = 1</td><td>等待本地 AOF fsync 完成</td></tr><tr><td>numlocal = 0</td><td>不要求本地 fsync</td></tr><tr><td>numreplicas</td><td>等待副本确认数量</td></tr><tr><td>timeout</td><td>超时时间（毫秒）</td></tr></tbody></table><ul class="lvl-0"><li class="lvl-2"><p>示例 1：要求本地 AOF 持久化</p></li></ul><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></pre></td><td class="code"><pre><span class="line">SET pay:1001 SUCCESS</span><br><span class="line">WAITAOF 1 0 3000</span><br><span class="line"><span class="comment"># 含义：</span></span><br><span class="line"><span class="comment">#     必须确认 写入已经 fsync 到本地 AOF 文件。</span></span><br><span class="line"><span class="comment">#     不关心副本同步。</span></span><br><span class="line"><span class="comment">#     超时 3 秒。</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例 2：同时要求 AOF + 副本确认</p></li></ul><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></pre></td><td class="code"><pre><span class="line">SET pay:1002 SUCCESS</span><br><span class="line">WAITAOF 1 2 5000</span><br><span class="line"><span class="comment"># 含义：</span></span><br><span class="line"><span class="comment">#     必须确认本地 AOF 已 fsync。</span></span><br><span class="line"><span class="comment">#     至少 2 个副本确认。</span></span><br><span class="line"><span class="comment">#     最大等待 5 秒。</span></span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例返回值</p></li></ul><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">1) (<span class="built_in">integer</span>) 1   <span class="comment"># 本地 fsync 确认数量</span></span><br><span class="line">2) (<span class="built_in">integer</span>) 2   <span class="comment"># 副本确认数量</span></span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/01/11/redis7-command-02-generic/</id>
    <link href="https://blog.hanqunfeng.com/2026/01/11/redis7-command-02-generic/"/>
    <published>2026-01-11T13:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">本文基于<code>redis-7.4.7</code></li>
<li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li>
</ul>]]>
    </summary>
    <title>Redis 命令详解：通用命令</title>
    <updated>2026-01-12T03:38:07.363Z</updated>
  </entry>
  <entry>
    <author>
      <name>飘逸峰</name>
    </author>
    <category term="技术" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/categories/%E6%8A%80%E6%9C%AF/redis/"/>
    <category term="redis" scheme="https://blog.hanqunfeng.com/tags/redis/"/>
    <content>
      <![CDATA[<h2 id="摘要">摘要</h2><ul class="lvl-0"><li class="lvl-2">本文基于<code>redis-7.4.7</code></li><li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li></ul><span id="more"></span><h2 id="Redis-Transaction-简介">Redis Transaction 简介</h2><ul class="lvl-0"><li class="lvl-2">最关键的核心点是：Redis 的事务模型和关系型数据库（如 MySQL）的事务模型有本质区别。</li><li class="lvl-2">关系型数据库事务：强调 ACID（原子性、一致性、隔离性、持久性），特别是在执行过程中，命令会看到一致的数据库状态（通过锁或MVCC），并且可以回滚。</li><li class="lvl-2">Redis 事务：主要目的是将多个命令打包，按顺序、连续、排他地执行。它更接近于一个批量执行和乐观锁的机制。它不提供回滚功能（只有命令入队时的语法检查，没有执行时的错误回滚）。</li></ul><h2 id="事务-Transaction-命令详解">事务 Transaction 命令详解</h2><ul class="lvl-0"><li class="lvl-2"><p>Redis 事务命令总览表</p></li></ul><table><thead><tr><th>命令</th><th>作用</th><th>所处阶段</th><th>返回值</th><th>典型使用场景</th></tr></thead><tbody><tr><td><strong>MULTI</strong></td><td>开启一个事务，之后的命令进入队列</td><td>事务开始</td><td><code>OK</code></td><td>批量原子执行多个命令</td></tr><tr><td><strong>EXEC</strong></td><td>执行事务队列中的所有命令</td><td>事务提交</td><td>数组（每个命令的执行结果）或 <code>nil</code></td><td>提交事务</td></tr><tr><td><strong>DISCARD</strong></td><td>放弃事务队列中的所有命令</td><td>事务取消</td><td><code>OK</code></td><td>回滚未执行的事务</td></tr><tr><td><strong>WATCH key [key …]</strong></td><td>监控一个或多个 key 是否被修改（乐观锁）</td><td>事务前</td><td><code>OK</code></td><td>并发控制、防止覆盖更新</td></tr><tr><td><strong>UNWATCH</strong></td><td>取消对所有 key 的监控</td><td>事务前 / 中</td><td><code>OK</code></td><td>主动释放监控</td></tr></tbody></table><h3 id="MULTI-EXEC-——-基本事务示例">MULTI / EXEC —— 基本事务示例</h3><ul class="lvl-0"><li class="lvl-2"><p>可以在 MULTI 和 EXEC 之间使用的命令</p></li></ul><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></pre></td><td class="code"><pre><span class="line">几乎所有 “写操作” 和 “只读操作” 命令都可以放入 MULTI...EXEC 块中。例如：</span><br><span class="line">    字符串操作：SET, GET, INCR, MSET</span><br><span class="line">    哈希操作：HSET, HGET, HINCRBY</span><br><span class="line">    列表操作：LPUSH, RPOP, LRANGE</span><br><span class="line">    集合操作：SADD, SREM, SMEMBERS</span><br><span class="line">    有序集合操作：ZADD, ZRANGE, ZREM</span><br><span class="line">    流操作（Redis 5.0+）：XADD, XREADGROUP</span><br><span class="line">    地理空间、位图等：所有相关操作命令。</span><br><span class="line">    管理类命令：DEL, EXPIRE, PERSIST 等。</span><br><span class="line">一个重要的特例：WATCH 命令</span><br><span class="line">    WATCH 命令用于实现乐观锁，它必须在 MULTI 命令之前执行，用来监视一个或多个键。</span><br><span class="line">阻塞命令在事务中是被禁止的</span><br><span class="line">    如 BLPOP， BRPOP， BRPOPLPUSH， BZPOPMIN， BZPOPMAX， XREAD， XREADGROUP（带阻塞选项），因为事务要求所有命令被一次性入队，而阻塞命令需要等待数据到达，这会阻塞整个服务器，导致死锁。Redis 会直接返回错误。</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>示例：原子递增两个计数器</p></li></ul><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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379&gt; MULTI</span><br><span class="line">OK</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; INCR counter:a</span><br><span class="line">QUEUED</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; INCR counter:b</span><br><span class="line">QUEUED</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; EXEC</span><br><span class="line">1) (<span class="built_in">integer</span>) 1</span><br><span class="line">2) (<span class="built_in">integer</span>) 1</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>说明</p><ul class="lvl-2"><li class="lvl-6">MULTI 后，所有命令只会进入队列，返回 QUEUED，不会立即执行。</li><li class="lvl-6">EXEC 时才真正执行。</li><li class="lvl-6">Redis 保证：<ul class="lvl-4"><li class="lvl-10">命令顺序执行</li><li class="lvl-10">执行期间不会被其他客户端插入命令</li></ul></li><li class="lvl-6">但不支持回滚（某条命令失败，不会自动撤销已执行的命令）。</li></ul></li></ul><h3 id="DISCARD-——-放弃事务">DISCARD —— 放弃事务</h3><ul class="lvl-0"><li class="lvl-2"><p>示例：取消事务</p></li></ul><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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379&gt; MULTI</span><br><span class="line">OK</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; SET k1 v1</span><br><span class="line">QUEUED</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; SET k2 v2</span><br><span class="line">QUEUED</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; DISCARD</span><br><span class="line">OK</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; GET k1</span><br><span class="line">(nil)</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>说明</p><ul class="lvl-2"><li class="lvl-6">DISCARD 会：<ul class="lvl-4"><li class="lvl-10">清空事务队列</li><li class="lvl-10">退出事务状态</li></ul></li><li class="lvl-6">队列中的命令 不会被执行。</li></ul></li></ul><h3 id="WATCH-EXEC-——-乐观锁示例">WATCH / EXEC —— 乐观锁示例</h3><ul class="lvl-0"><li class="lvl-2"><p>乐观锁事务模板</p></li></ul><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></pre></td><td class="code"><pre><span class="line">WATCH key</span><br><span class="line">读取并校验数据</span><br><span class="line">MULTI</span><br><span class="line">  写操作...</span><br><span class="line">EXEC</span><br><span class="line">如果返回 nil → 重试</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>场景：实现一个安全的余额扣减逻辑，如果余额在事务执行前被别人修改，则放弃本次事务。</p></li><li class="lvl-2"><p>客户端 A</p></li></ul><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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379&gt; SET balance 100</span><br><span class="line">OK</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; WATCH balance</span><br><span class="line">OK</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; GET balance</span><br><span class="line"><span class="string">&quot;100&quot;</span></span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; MULTI</span><br><span class="line">OK</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; DECRBY balance 30</span><br><span class="line">QUEUED</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>客户端 B（并发修改）</p></li></ul><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">127.0.0.1:6379&gt; INCRBY balance 50</span><br><span class="line">(<span class="built_in">integer</span>) 150</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>客户端 A 提交事务</p></li></ul><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">127.0.0.1:6379&gt; EXEC</span><br><span class="line">(nil)</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>说明</p><ul class="lvl-2"><li class="lvl-6">因为 balance 在 WATCH 之后被客户端 B 修改过：<ul class="lvl-4"><li class="lvl-10">Redis 判定监控失败</li><li class="lvl-10">EXEC 返回 nil</li><li class="lvl-10">事务不会执行</li></ul></li><li class="lvl-6">这是典型的 乐观锁（CAS）机制。</li></ul></li></ul><h3 id="UNWATCH-——-取消监控">UNWATCH —— 取消监控</h3><ul class="lvl-0"><li class="lvl-2"><p>示例：取消 WATCH 监控</p></li></ul><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">127.0.0.1:6379&gt; WATCH k1 k2</span><br><span class="line">OK</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379&gt; UNWATCH</span><br><span class="line">OK</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>说明</p><ul class="lvl-2"><li class="lvl-6">取消所有被监控的 key。</li><li class="lvl-6">常见用途：<ul class="lvl-4"><li class="lvl-10">逻辑判断失败，不再继续事务</li><li class="lvl-10">主动释放监控，避免误触发事务失败</li></ul></li><li class="lvl-6">如果执行了 EXEC 或 DISCARD，Redis 会自动 UNWATCH。</li></ul></li></ul><h2 id="关键行为总结（工程视角）">关键行为总结（工程视角）</h2><table><thead><tr><th>维度</th><th>行为</th></tr></thead><tbody><tr><td>原子性</td><td><code>EXEC</code> 内命令顺序执行，不会被其他客户端插入</td></tr><tr><td>隔离性</td><td>不是数据库级事务隔离，仅保证执行期串行</td></tr><tr><td>回滚能力</td><td>❌ 不支持回滚</td></tr><tr><td>并发控制</td><td>通过 <code>WATCH</code> 实现乐观锁</td></tr><tr><td>失败行为</td><td>WATCH 冲突 → <code>EXEC</code> 返回 <code>nil</code></td></tr><tr><td>性能</td><td>队列入内存，执行非常快</td></tr></tbody></table><h2 id="SpringBoot-中使用-Redis-事务">SpringBoot 中使用 Redis 事务</h2><ul class="lvl-0"><li class="lvl-2"><p>实际项目中Redis事务很少使用，因为<code>WATCH + MULTI</code> 的性能不如 <code>Lua</code> 或 <code>INCR</code> 等原子指令。</p></li><li class="lvl-2"><p>同一事务代码必须在同一线程内完成，推荐使用 <code>SessionCallback</code></p></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">key</span> <span class="operator">=</span> <span class="string">&quot;stock:1001&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 初始化库存</span></span><br><span class="line">redisTemplate.opsForValue().set(key, <span class="number">10</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 执行事务操作</span></span><br><span class="line">List&lt;Object&gt; result = redisTemplate.execute(<span class="keyword">new</span> <span class="title class_">SessionCallback</span>&lt;&gt;() &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> List&lt;Object&gt; <span class="title function_">execute</span><span class="params">(RedisOperations operations)</span> <span class="keyword">throws</span> DataAccessException &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 监视key的变化</span></span><br><span class="line">            operations.watch(key);</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 安全获取当前库存值</span></span><br><span class="line">            <span class="type">Object</span> <span class="variable">stock</span> <span class="operator">=</span> operations.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> (stock == <span class="literal">null</span> || (Integer) stock &lt;= <span class="number">0</span>) &#123;</span><br><span class="line">                operations.unwatch(); <span class="comment">// 取消监视</span></span><br><span class="line">                System.out.println(<span class="string">&quot;库存不足，无法扣减&quot;</span>);</span><br><span class="line">                <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 开始事务</span></span><br><span class="line">            operations.multi();</span><br><span class="line">            operations.opsForValue().decrement(key);</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 执行事务并返回结果</span></span><br><span class="line">            List&lt;Object&gt; execResult = operations.exec();</span><br><span class="line">            System.out.println(<span class="string">&quot;事务执行成功，扣减库存后结果: &quot;</span> + execResult);</span><br><span class="line">            <span class="keyword">return</span> execResult;</span><br><span class="line"></span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="comment">// 发生异常时取消监视</span></span><br><span class="line">            operations.unwatch();</span><br><span class="line">            System.err.println(<span class="string">&quot;事务执行异常: &quot;</span> + e.getMessage());</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">DataAccessException</span>(<span class="string">&quot;Redis事务执行失败&quot;</span>, e) &#123;</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="keyword">if</span> (result != <span class="literal">null</span>) &#123;</span><br><span class="line">    System.out.println(<span class="string">&quot;最终事务结果: &quot;</span> + result);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    System.out.println(<span class="string">&quot;事务未执行或执行失败&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul class="lvl-0"><li class="lvl-2"><p>验证事务是否生效，可以在 redis 终端执行 <code>MONITOR</code> 命令查询</p></li></ul><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></pre></td><td class="code"><pre><span class="line">127.0.0.1:6379&gt; MONITOR</span><br><span class="line">OK</span><br><span class="line">1768054048.497866 [0 127.0.0.1:53634] <span class="string">&quot;HELLO&quot;</span> <span class="string">&quot;3&quot;</span> <span class="string">&quot;AUTH&quot;</span> <span class="string">&quot;(redacted)&quot;</span> <span class="string">&quot;(redacted)&quot;</span></span><br><span class="line">1768054048.510741 [0 127.0.0.1:53634] <span class="string">&quot;CLIENT&quot;</span> <span class="string">&quot;SETINFO&quot;</span> <span class="string">&quot;lib-name&quot;</span> <span class="string">&quot;Lettuce&quot;</span></span><br><span class="line">1768054048.510759 [0 127.0.0.1:53634] <span class="string">&quot;CLIENT&quot;</span> <span class="string">&quot;SETINFO&quot;</span> <span class="string">&quot;lib-ver&quot;</span> <span class="string">&quot;6.6.0.RELEASE/643bd47&quot;</span></span><br><span class="line">1768054048.538458 [0 127.0.0.1:53634] <span class="string">&quot;SET&quot;</span> <span class="string">&quot;stock:1001&quot;</span> <span class="string">&quot;10&quot;</span></span><br><span class="line">1768054048.557699 [0 127.0.0.1:53635] <span class="string">&quot;HELLO&quot;</span> <span class="string">&quot;3&quot;</span> <span class="string">&quot;AUTH&quot;</span> <span class="string">&quot;(redacted)&quot;</span> <span class="string">&quot;(redacted)&quot;</span></span><br><span class="line">1768054048.559907 [0 127.0.0.1:53635] <span class="string">&quot;CLIENT&quot;</span> <span class="string">&quot;SETINFO&quot;</span> <span class="string">&quot;lib-name&quot;</span> <span class="string">&quot;Lettuce&quot;</span></span><br><span class="line">1768054048.559918 [0 127.0.0.1:53635] <span class="string">&quot;CLIENT&quot;</span> <span class="string">&quot;SETINFO&quot;</span> <span class="string">&quot;lib-ver&quot;</span> <span class="string">&quot;6.6.0.RELEASE/643bd47&quot;</span></span><br><span class="line">1768054048.562642 [0 127.0.0.1:53635] <span class="string">&quot;WATCH&quot;</span> <span class="string">&quot;stock:1001&quot;</span></span><br><span class="line">1768054048.566201 [0 127.0.0.1:53634] <span class="string">&quot;GET&quot;</span> <span class="string">&quot;stock:1001&quot;</span></span><br><span class="line">1768054048.583727 [0 127.0.0.1:53635] <span class="string">&quot;MULTI&quot;</span></span><br><span class="line">1768054048.645412 [0 127.0.0.1:53635] <span class="string">&quot;DECR&quot;</span> <span class="string">&quot;stock:1001&quot;</span></span><br><span class="line">1768054048.645421 [0 127.0.0.1:53635] <span class="string">&quot;EXEC&quot;</span></span><br></pre></td></tr></table></figure><blockquote><p>重点看：<code>WATCH</code> 以及 <code>MULTI</code> 和 <code>EXEC</code>之间的输出，要在一个连接中才能生效</p></blockquote>]]>
    </content>
    <id>https://blog.hanqunfeng.com/2026/01/10/redis7-command-01-transaction/</id>
    <link href="https://blog.hanqunfeng.com/2026/01/10/redis7-command-01-transaction/"/>
    <published>2026-01-10T13:30:05.000Z</published>
    <summary>
      <![CDATA[<h2 id="摘要">摘要</h2>
<ul class="lvl-0">
<li class="lvl-2">本文基于<code>redis-7.4.7</code></li>
<li class="lvl-2">Redis官网：<a href="https://redis.io/">https://redis.io/</a></li>
</ul>]]>
    </summary>
    <title>Redis 命令详解：事务 Transaction</title>
    <updated>2026-01-11T04:35:02.873Z</updated>
  </entry>
</feed>
