<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://www.s9mc.cn/</id>
  <title type="text">南柯一梦</title>
  <subtitle type="text">一梦十二载，代码与方块</subtitle>
  <updated>2026-09-28T00:00:00.000Z</updated>
  <author><name>南柯一梦</name></author>
  <link rel="alternate" href="https://www.s9mc.cn/"/>
  <link rel="self" href="https://www.s9mc.cn/atom.xml"/>
  <generator uri="https://astro.build">Astro v6.16.8</generator>
    <entry>
      <id>https://www.s9mc.cn/posts/java-virtual-threads/</id>
      <title type="text">Java 虚拟线程实战：用同步写法扛住十万并发</title>
      <published>2026-09-28T00:00:00.000Z</published>
      <updated>2026-09-28T00:00:00.000Z</updated>
      <author><name>南柯一梦</name></author>
      <link rel="alternate" href="https://www.s9mc.cn/posts/java-virtual-threads/"/>
      <summary type="text">虚拟线程让「一请求一线程」重新变得可行。本文从原理、用法到踩坑，讲清楚它在后端服务里该怎么用。</summary>
      <content type="html"><![CDATA[<section><h2>为什么需要虚拟线程<a href="#为什么需要虚拟线程"><span>#</span></a></h2><p>传统的 Java 线程是对操作系统线程的一层薄封装，一个线程大约占用 1MB 栈空间，创建和切换成本都不低。于是我们才有了线程池、回调、<code>CompletableFuture</code>、响应式编程这一整套「为了省线程而牺牲可读性」的方案。</p><p>虚拟线程（Virtual Thread，JDK 21 正式）换了个思路：线程由 JVM 调度，成本极低，可以轻松创建几十万个。阻塞时，JVM 会把它从载体线程（carrier thread）上卸下来，让出 CPU。</p><p>结论：<strong>可以继续用最直观的同步代码，同时拿到接近异步的吞吐。</strong></p></section>
<section><h2>最小示例<a href="#最小示例"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>try</span><span> (</span><span>var</span><span> executor </span><span><span>=</span><span> </span></span><span>Executors</span><span>.</span><span>newVirtualThreadPerTaskExecutor</span><span><span>()</span><span>) {</span></span></div></div><div><div><div>2</div></div><div><span>    </span><span>List</span><span>&lt;</span><span>Future</span><span>&lt;</span><span>String</span><span><span>&gt;&gt;</span><span> futures </span></span><span><span>=</span><span> </span></span><span>new</span><span> </span><span>ArrayList</span><span><span>&lt;&gt;</span><span>()</span><span>;</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>for</span><span> (</span><span>int</span><span> i </span><span><span>=</span><span> </span></span><span>0</span><span><span>;</span><span> i </span><span>&lt;</span><span> </span></span><span>100_000</span><span><span>;</span><span> i</span><span>++</span><span>) {</span></span></div></div><div><div><div>4</div></div><div><span>        </span><span>int</span><span> id </span><span><span>=</span><span> i</span><span>;</span></span></div></div><div><div><div>5</div></div><div><span>        </span><span>futures</span><span>.</span><span>add</span><span>(</span><span>executor</span><span>.</span><span>submit</span><span>(() </span><span>-&gt;</span><span> {</span></div></div><div><div><div>6</div></div><div><span>            </span><span>Thread</span><span>.</span><span>sleep</span><span>(</span><span>Duration</span><span>.</span><span>ofSeconds</span><span>(</span><span>1</span><span>));</span></div></div><div><div><div>7</div></div><div><span>            </span><span>return</span><span> </span><span>"task-"</span><span><span> </span><span>+</span><span> id;</span></span></div></div><div><div><div>8</div></div><div><span><span>        </span></span><span>}));</span></div></div><div><div><div>9</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>10</div></div><div><span>    </span><span>for</span><span> (</span><span>var</span><span> f </span><span>:</span><span> futures) {</span></div></div><div><div><div>11</div></div><div><span>        </span><span>f</span><span>.</span><span>get</span><span>();</span></div></div><div><div><div>12</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>13</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>十万个任务各睡一秒，总耗时依然接近一秒。换成固定大小的平台线程池，你会等很久。</p></section>
<section><h2>在 Spring Boot 中启用<a href="#在-spring-boot-中启用"><span>#</span></a></h2><p>Spring Boot 3.2+ 只需要一行配置：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>spring</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>threads</span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>virtual</span><span>:</span></div></div><div><div><div>4</div></div><div><span>      </span><span>enabled</span><span>: </span><span>true</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>Tomcat 的请求处理线程、<code>@Async</code> 的执行器都会切换成虚拟线程。</p></section>
<section><h2>常见误区<a href="#常见误区"><span>#</span></a></h2><ol>
<li><strong>不要池化虚拟线程。</strong> 它们本来就是廉价的，用完即弃。需要限流时用 <code>Semaphore</code>，而不是缩小线程池。</li>
<li><strong>CPU 密集型任务没有收益。</strong> 虚拟线程解决的是「等待」问题，不是「计算」问题。</li>
<li><strong>小心 <code>synchronized</code> 造成的 pinning。</strong> 在 JDK 21 中，虚拟线程在 <code>synchronized</code> 块里阻塞会钉住载体线程。能换成 <code>ReentrantLock</code> 就换。可以用 <code>-Djdk.tracePinnedThreads=full</code> 排查。</li>
<li><strong>ThreadLocal 要克制。</strong> 线程数量暴涨后，每个线程一份 ThreadLocal 的内存开销会被放大。</li>
</ol></section>
<section><h2>小结<a href="#小结"><span>#</span></a></h2><p>虚拟线程不是银弹，但对大多数 IO 密集的业务后端来说，它能让代码回到最朴素的样子。升级 JDK 21 之后，先把它开在非核心服务上压测一遍，数据会告诉你答案。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://www.s9mc.cn/posts/paper-server-tuning/</id>
      <title type="text">开了十二年服，我总结的 Paper 服务器调优清单</title>
      <published>2026-09-20T00:00:00.000Z</published>
      <updated>2026-09-20T00:00:00.000Z</updated>
      <author><name>南柯一梦</name></author>
      <link rel="alternate" href="https://www.s9mc.cn/posts/paper-server-tuning/"/>
      <summary type="text">从 JVM 参数到区块加载，从实体清理到定时任务，一份来自十二年运维经验的 Minecraft 服务器调优清单。</summary>
      <content type="html"><![CDATA[<p>服务器卡，十有八九不是机器不行，而是配置没有调。下面是我这些年踩坑后沉淀出的清单。</p>
<section><h2>1. JVM 参数<a href="#1-jvm-参数"><span>#</span></a></h2><p>内存别给满，留出系统和堆外空间。常用的 G1 参数如下（以 8GB 堆为例）：</p><div><figure><figcaption><span></span><span>Terminal window</span></figcaption><pre><code><div><div><div>1</div></div><div><span>java</span><span> </span><span>-Xms8G</span><span> </span><span>-Xmx8G</span><span> </span><span>\</span></div></div><div><div><div>2</div></div><div><span>  </span><span>-XX:+UseG1GC</span><span> </span><span>-XX:+ParallelRefProcEnabled</span><span> </span><span>\</span></div></div><div><div><div>3</div></div><div><span>  </span><span>-XX:MaxGCPauseMillis=200</span><span> </span><span>\</span></div></div><div><div><div>4</div></div><div><span>  </span><span>-XX:+UnlockExperimentalVMOptions</span><span> </span><span>\</span></div></div><div><div><div>5</div></div><div><span>  </span><span>-XX:+DisableExplicitGC</span><span> </span><span>-XX:+AlwaysPreTouch</span><span> </span><span>\</span></div></div><div><div><div>6</div></div><div><span>  </span><span>-XX:G1NewSizePercent=30</span><span> </span><span>-XX:G1MaxNewSizePercent=40</span><span> </span><span>\</span></div></div><div><div><div>7</div></div><div><span>  </span><span>-XX:G1HeapRegionSize=8M</span><span> </span><span>-XX:G1ReservePercent=20</span><span> </span><span>\</span></div></div><div><div><div>8</div></div><div><span>  </span><span>-XX:G1HeapWastePercent=5</span><span> </span><span>-XX:G1MixedGCCountTarget=4</span><span> </span><span>\</span></div></div><div><div><div>9</div></div><div><span>  </span><span>-XX:InitiatingHeapOccupancyPercent=15</span><span> </span><span>\</span></div></div><div><div><div>10</div></div><div><span>  </span><span>-jar</span><span> </span><span>paper.jar</span><span> </span><span>--nogui</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p><code>-Xms</code> 与 <code>-Xmx</code> 设置成一样，避免堆扩缩带来的抖动。</p></section>
<section><h2>2. 视距与模拟距离<a href="#2-视距与模拟距离"><span>#</span></a></h2><p>这是收益最大的一项。在 <code>server.properties</code> 与 <code>paper-world-defaults.yml</code> 里：</p><ul>
<li><code>view-distance</code>：保持 8 左右。</li>
<li><code>simulation-distance</code>：降到 4~6，实体和红石只在近处运行。</li>
</ul><p>玩家几乎感觉不到，但 CPU 压力会明显下降。</p></section>
<section><h2>3. 实体管理<a href="#3-实体管理"><span>#</span></a></h2><ul>
<li>限制每个区块的怪物上限与刷怪频率。</li>
<li>定期清理掉落物，但要给玩家提前提示。</li>
<li>大型农场（村民交易所、刷怪塔）要单独评估，村民的寻路 AI 是出了名的重。</li>
</ul></section>
<section><h2>4. 区块与世界<a href="#4-区块与世界"><span>#</span></a></h2><ul>
<li>使用 <code>/chunky</code> 提前预生成地图，避免玩家探索时实时生成造成卡顿。</li>
<li>设置世界边界，别让地图无限膨胀。</li>
<li>定期备份，并且<strong>定期验证备份可恢复</strong>。没验证过的备份只是一种心理安慰。</li>
</ul></section>
<section><h2>5. 用数据说话<a href="#5-用数据说话"><span>#</span></a></h2><p>不要凭感觉调参数。Paper 自带的 <code>/spark profiler</code> 是我的首选：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>/spark profiler start</span></div></div><div><div><div>2</div></div><div><span># 等待 5~10 分钟，覆盖高峰期</span></div></div><div><div><div>3</div></div><div><span>/spark profiler stop</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>看火焰图，找到占用 tick 时间最多的插件或实体，再对症下药。</p></section>
<section><h2>6. 插件管理<a href="#6-插件管理"><span>#</span></a></h2><ul>
<li>插件够用就好，每个插件都是潜在的性能与安全风险。</li>
<li>更新前先在测试服验证。</li>
<li>看到不再维护的插件，尽早找替代品。</li>
</ul></section>
<section><h2>小结<a href="#小结"><span>#</span></a></h2><p>调优的本质是：测量、定位、改一处、再测量。十二年下来最大的体会是，<strong>稳定比花哨更重要</strong>。玩家记得的是流畅的体验，而不是你装了多少插件。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://www.s9mc.cn/posts/ai-pair-programming/</id>
      <title type="text">和 AI 结对编程：我的日常工作流</title>
      <published>2026-09-12T00:00:00.000Z</published>
      <updated>2026-09-12T00:00:00.000Z</updated>
      <author><name>南柯一梦</name></author>
      <link rel="alternate" href="https://www.s9mc.cn/posts/ai-pair-programming/"/>
      <summary type="text">不是让 AI 替你写代码，而是让它成为你的搭档。分享我在日常开发中使用 AI 工具的几条原则与技巧。</summary>
      <content type="html"><![CDATA[<p>过去一年，AI 工具已经深度嵌进了我的日常开发。这里分享几条我自己总结的原则。</p>
<section><h2>原则一：你来决策，AI 来执行<a href="#原则一你来决策ai-来执行"><span>#</span></a></h2><p>把 AI 当作一位速度很快、但缺乏上下文的搭档。需求怎么拆、边界怎么定、取舍怎么做，这些由人来决定；查文档、写样板代码、补测试、重构，这些交给 AI。</p></section>
<section><h2>原则二：上下文决定质量<a href="#原则二上下文决定质量"><span>#</span></a></h2><p>同样一句「帮我加个接口」，给了项目约定和现有代码，结果会好很多。我一般会提供：</p><ul>
<li>相关文件与调用关系；</li>
<li>项目的编码规范（例如统一返回结构、异常处理方式）；</li>
<li>明确的验收标准。</li>
</ul></section>
<section><h2>原则三：小步提交，逐步验证<a href="#原则三小步提交逐步验证"><span>#</span></a></h2><p>不要一次让 AI 改几十个文件。把任务拆小，每一步都跑测试、看 diff。出现问题时，回滚的成本也很低。</p></section>
<section><h2>我常用的场景<a href="#我常用的场景"><span>#</span></a></h2>

<table><thead><tr><th>场景</th><th>做法</th></tr></thead><tbody><tr><td>阅读陌生代码</td><td>让 AI 梳理调用链，我再对照源码确认</td></tr><tr><td>写单元测试</td><td>先给被测方法，让 AI 列出边界用例，再逐个实现</td></tr><tr><td>排查线上问题</td><td>把日志与堆栈贴给 AI，请它给出几个可验证的假设</td></tr><tr><td>重构</td><td>先让 AI 说明改动计划，我确认后再动手</td></tr><tr><td>写文档与提交信息</td><td>直接交给 AI，我只做审阅</td></tr></tbody></table></section>
<section><h2>一个提醒<a href="#一个提醒"><span>#</span></a></h2><p>AI 生成的代码，<strong>你需要对它负责</strong>。不理解的代码不要合入，涉及安全、权限、资金的逻辑，必须人工逐行审阅。</p></section>
<section><h2>小结<a href="#小结"><span>#</span></a></h2><p>好的使用方式是「放大自己」而不是「替代自己」。把重复劳动交出去，把精力留给真正需要判断力的地方，这才是 AI 工具最大的价值。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://www.s9mc.cn/posts/mysql-index-explain/</id>
      <title type="text">MySQL 索引与执行计划：从 EXPLAIN 看懂慢查询</title>
      <published>2026-09-05T00:00:00.000Z</published>
      <updated>2026-09-05T00:00:00.000Z</updated>
      <author><name>南柯一梦</name></author>
      <link rel="alternate" href="https://www.s9mc.cn/posts/mysql-index-explain/"/>
      <summary type="text">学会读 EXPLAIN，理解最左前缀、覆盖索引与索引失效场景，让慢查询无处遁形。</summary>
      <content type="html"><![CDATA[<p>慢查询是后端工程师绕不开的话题。排查的第一步，永远是 <code>EXPLAIN</code>。</p>
<section><h2>读懂 EXPLAIN<a href="#读懂-explain"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>EXPLAIN </span><span>SELECT</span><span> id, </span><span>name</span><span> </span><span>FROM</span><span> user </span><span>WHERE</span><span><span> age </span><span>=</span><span> </span></span><span>25</span><span> </span><span>AND</span><span><span> city </span><span>=</span><span> </span></span><span>'Shanghai'</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>重点关注几列：</p><ul>
<li><strong>type</strong>：访问方式。从好到差大致是 <code>const</code> &gt; <code>ref</code> &gt; <code>range</code> &gt; <code>index</code> &gt; <code>ALL</code>。出现 <code>ALL</code> 意味着全表扫描。</li>
<li><strong>key</strong>：实际使用的索引。</li>
<li><strong>rows</strong>：预估扫描行数，越小越好。</li>
<li><strong>Extra</strong>：<code>Using index</code> 表示覆盖索引；<code>Using filesort</code>、<code>Using temporary</code> 需要警惕。</li>
</ul></section>
<section><h2>最左前缀原则<a href="#最左前缀原则"><span>#</span></a></h2><p>联合索引 <code>(city, age, name)</code> 可以支撑：</p><ul>
<li><code>WHERE city = ?</code></li>
<li><code>WHERE city = ? AND age = ?</code></li>
<li><code>WHERE city = ? AND age = ? AND name = ?</code></li>
</ul><p>但无法有效支撑只写 <code>age</code> 或 <code>name</code> 的查询。设计联合索引时，把区分度高、使用频率高的列放在前面。</p></section>
<section><h2>覆盖索引<a href="#覆盖索引"><span>#</span></a></h2><p>如果查询的列都在索引里，MySQL 不需要回表：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>-- 索引 (city, age, name)</span></div></div><div><div><div>2</div></div><div><span>SELECT</span><span> city, age, </span><span>name</span><span> </span><span>FROM</span><span> user </span><span>WHERE</span><span><span> city </span><span>=</span><span> </span></span><span>'Shanghai'</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>回表是随机 IO，能避免就避免。</p></section>
<section><h2>常见的索引失效场景<a href="#常见的索引失效场景"><span>#</span></a></h2><ol>
<li>对索引列使用函数或运算：<code>WHERE DATE(create_time) = '2026-01-01'</code>。</li>
<li>隐式类型转换：字符串列用数字去比较。</li>
<li>以通配符开头的模糊查询：<code>LIKE '%abc'</code>。</li>
<li><code>OR</code> 连接的条件中，有一边没有索引。</li>
<li>联合索引没有遵循最左前缀。</li>
</ol></section>
<section><h2>分页深翻页<a href="#分页深翻页"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>-- 慢：需要扫描并丢弃前 100 万行</span></div></div><div><div><div>2</div></div><div><span>SELECT</span><span> * </span><span>FROM</span><span> orders </span><span>ORDER BY</span><span> id </span><span>LIMIT</span><span> </span><span>1000000</span><span>, </span><span>20</span><span>;</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>-- 快：利用上一页的最后一个 id</span></div></div><div><div><div>5</div></div><div><span>SELECT</span><span> * </span><span>FROM</span><span> orders </span><span>WHERE</span><span><span> id </span><span>&gt;</span><span> </span></span><span>1000000</span><span> </span><span>ORDER BY</span><span> id </span><span>LIMIT</span><span> </span><span>20</span><span>;</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>小结<a href="#小结"><span>#</span></a></h2><p>优化的流程很朴素：先定位慢 SQL，再看执行计划，然后调整索引或改写查询，最后用真实数据验证。<strong>不要为了加索引而加索引</strong>，每个索引都会拖慢写入、占用空间。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://www.s9mc.cn/posts/spring-boot-graceful-shutdown/</id>
      <title type="text">Spring Boot 优雅停机：发布时不再丢请求</title>
      <published>2026-08-28T00:00:00.000Z</published>
      <updated>2026-08-28T00:00:00.000Z</updated>
      <author><name>南柯一梦</name></author>
      <link rel="alternate" href="https://www.s9mc.cn/posts/spring-boot-graceful-shutdown/"/>
      <summary type="text">讲清楚 Spring Boot 的优雅停机机制，以及在 Kubernetes 与负载均衡下如何配合，做到发布零报错。</summary>
      <content type="html"><![CDATA[<p>每次发布都会收到几条 502 报警？很可能是应用没有「优雅」地停下来。</p>
<section><h2>什么是优雅停机<a href="#什么是优雅停机"><span>#</span></a></h2><p>收到停止信号后：</p><ol>
<li>不再接收<strong>新</strong>请求；</li>
<li>等待<strong>进行中</strong>的请求处理完成（带超时）；</li>
<li>关闭线程池、连接池等资源；</li>
<li>进程退出。</li>
</ol></section>
<section><h2>开启配置<a href="#开启配置"><span>#</span></a></h2><p>Spring Boot 2.3+ 内置支持：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>server</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>shutdown</span><span>: </span><span>graceful</span></div></div><div><div><div>3</div></div><div>
</div></div><div><div><div>4</div></div><div><span>spring</span><span>:</span></div></div><div><div><div>5</div></div><div><span>  </span><span>lifecycle</span><span>:</span></div></div><div><div><div>6</div></div><div><span>    </span><span>timeout-per-shutdown-phase</span><span>: </span><span>30s</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>默认是 <code>immediate</code>，即立即关闭。</p></section>
<section><h2>配合负载均衡<a href="#配合负载均衡"><span>#</span></a></h2><p>仅仅应用内优雅还不够。流量还在往一个即将退出的实例上打，就会出错。一个稳妥的顺序是：</p><ol>
<li>先把实例从负载均衡中摘除（或让就绪探针失败）；</li>
<li>等待几秒，让流量完全切走；</li>
<li>再发送 <code>SIGTERM</code>，触发优雅停机。</li>
</ol><p>在 Kubernetes 中可以用 <code>preStop</code> 钩子留出缓冲时间：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>lifecycle</span><span>:</span></div></div><div><div><div>2</div></div><div><span>  </span><span>preStop</span><span>:</span></div></div><div><div><div>3</div></div><div><span>    </span><span>exec</span><span>:</span></div></div><div><div><div>4</div></div><div><span>      </span><span>command</span><span>: [</span><span>"sh"</span><span>, </span><span>"-c"</span><span>, </span><span>"sleep 10"</span><span>]</span></div></div><div><div><div>5</div></div><div><span>terminationGracePeriodSeconds</span><span>: </span><span>60</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>注意：<code>terminationGracePeriodSeconds</code> 必须大于 <code>preStop</code> 时间加上应用的停机超时。</p></section>
<section><h2>自己的资源也要收尾<a href="#自己的资源也要收尾"><span>#</span></a></h2><p>对于自己创建的线程池、消息消费者，要在 <code>@PreDestroy</code> 或 <code>SmartLifecycle</code> 中显式停止：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>@</span><span>PreDestroy</span></div></div><div><div><div>2</div></div><div><span>public</span><span> </span><span>void</span><span> </span><span>shutdown</span><span>() {</span></div></div><div><div><div>3</div></div><div><span>    </span><span>executor</span><span>.</span><span>shutdown</span><span>();</span></div></div><div><div><div>4</div></div><div><span>    </span><span>try</span><span> {</span></div></div><div><div><div>5</div></div><div><span>        </span><span>if</span><span><span> (</span><span>!</span></span><span>executor</span><span>.</span><span>awaitTermination</span><span>(</span><span>20</span><span>, </span><span>TimeUnit</span><span>.</span><span>SECONDS</span><span><span>)</span><span>) {</span></span></div></div><div><div><div>6</div></div><div><span>            </span><span>executor</span><span>.</span><span>shutdownNow</span><span>();</span></div></div><div><div><div>7</div></div><div><span><span>        </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span><span>    </span></span><span>} </span><span>catch</span><span> (</span><span>InterruptedException</span><span><span> </span><span>e</span><span>) {</span></span></div></div><div><div><div>9</div></div><div><span>        </span><span>executor</span><span>.</span><span>shutdownNow</span><span>();</span></div></div><div><div><div>10</div></div><div><span>        </span><span>Thread</span><span>.</span><span>currentThread</span><span>().</span><span>interrupt</span><span>();</span></div></div><div><div><div>11</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>12</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>小结<a href="#小结"><span>#</span></a></h2><p>优雅停机是「应用 + 基础设施」共同的事。把它配置好，发布就不再是一件提心吊胆的事情。</p></section>]]></content>
    </entry>
    <entry>
      <id>https://www.s9mc.cn/posts/redis-distributed-lock/</id>
      <title type="text">Redis 分布式锁：从 SETNX 到 Redisson 的坑</title>
      <published>2026-08-15T00:00:00.000Z</published>
      <updated>2026-08-15T00:00:00.000Z</updated>
      <author><name>南柯一梦</name></author>
      <link rel="alternate" href="https://www.s9mc.cn/posts/redis-distributed-lock/"/>
      <summary type="text">一把分布式锁要考虑的事情远比想象中多：原子性、过期时间、误删、续期。本文逐个拆解。</summary>
      <content type="html"><![CDATA[<p>分布式锁看上去只是「加锁、解锁」两步，实际上一个坑接一个坑。</p>
<section><h2>第一版：SETNX<a href="#第一版setnx"><span>#</span></a></h2><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Boolean</span><span> ok </span><span><span>=</span><span> </span></span><span>redis</span><span>.</span><span>opsForValue</span><span>().</span><span>setIfAbsent</span><span>(</span><span>"lock:order:1001"</span><span>, </span><span>"1"</span><span>);</span></div></div><div><div><div>2</div></div><div><span>if</span><span> (</span><span>Boolean</span><span>.</span><span>TRUE</span><span>.</span><span>equals</span><span><span>(ok)</span><span>) {</span></span></div></div><div><div><div>3</div></div><div><span>    </span><span>try</span><span> {</span></div></div><div><div><div>4</div></div><div><span>        </span><span>// 业务逻辑</span></div></div><div><div><div>5</div></div><div><span><span>    </span></span><span>} </span><span>finally</span><span> {</span></div></div><div><div><div>6</div></div><div><span>        </span><span>redis</span><span>.</span><span>delete</span><span>(</span><span>"lock:order:1001"</span><span>);</span></div></div><div><div><div>7</div></div><div><span><span>    </span></span><span>}</span></div></div><div><div><div>8</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>问题：如果进程在持有锁时崩溃，锁永远不会被释放。</p></section>
<section><h2>第二版：加上过期时间<a href="#第二版加上过期时间"><span>#</span></a></h2><p>必须把「加锁」和「设置过期」做成一个原子操作：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>Boolean</span><span> ok </span><span><span>=</span><span> </span></span><span>redis</span><span>.</span><span>opsForValue</span><span>()</span></div></div><div><div><div>2</div></div><div><span><span>    </span></span><span>.</span><span>setIfAbsent</span><span>(</span><span>"lock:order:1001"</span><span>, token, </span><span>Duration</span><span>.</span><span>ofSeconds</span><span>(</span><span>30</span><span>));</span></div></div></code></pre><div><div></div><div></div></div></figure></div><p>不要先 <code>SETNX</code> 再 <code>EXPIRE</code>，两步之间崩溃就又回到了死锁。</p></section>
<section><h2>第三版：防止误删<a href="#第三版防止误删"><span>#</span></a></h2><p>假如业务执行超过 30 秒，锁已经过期并被别人拿走，此时你的 <code>delete</code> 会把别人的锁删掉。</p><p>做法：加锁时写入唯一标识（token），解锁时先比较再删除，并且这两步要用 Lua 脚本保证原子性：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>if</span><span> </span><span>redis</span><span>.</span><span>call</span><span>(</span><span>"get"</span><span>, </span><span>KEYS</span><span>[</span><span>1</span><span>]) == </span><span>ARGV</span><span>[</span><span>1</span><span>] </span><span>then</span></div></div><div><div><div>2</div></div><div><span>    </span><span>return</span><span> </span><span>redis</span><span>.</span><span>call</span><span>(</span><span>"del"</span><span>, </span><span>KEYS</span><span>[</span><span>1</span><span>])</span></div></div><div><div><div>3</div></div><div><span>else</span></div></div><div><div><div>4</div></div><div><span>    </span><span>return</span><span> </span><span>0</span></div></div><div><div><div>5</div></div><div><span>end</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>第四版：续期<a href="#第四版续期"><span>#</span></a></h2><p>业务耗时不确定时，固定的过期时间要么太短要么太长。解决方案是「看门狗」：持有锁期间，后台线程定时延长过期时间。</p><p>自己实现很容易出错，推荐直接使用 Redisson：</p><div><figure><figcaption></figcaption><pre><code><div><div><div>1</div></div><div><span>RLock</span><span> lock </span><span><span>=</span><span> </span></span><span>redissonClient</span><span>.</span><span>getLock</span><span>(</span><span>"lock:order:1001"</span><span>);</span></div></div><div><div><div>2</div></div><div><span>lock</span><span>.</span><span>lock</span><span><span>();</span><span> </span></span><span>// 默认开启看门狗，自动续期</span></div></div><div><div><div>3</div></div><div><span>try</span><span> {</span></div></div><div><div><div>4</div></div><div><span>    </span><span>// 业务逻辑</span></div></div><div><div><div>5</div></div><div><span>} </span><span>finally</span><span> {</span></div></div><div><div><div>6</div></div><div><span>    </span><span>lock</span><span>.</span><span>unlock</span><span>();</span></div></div><div><div><div>7</div></div><div><span>}</span></div></div></code></pre><div><div></div><div></div></div></figure></div></section>
<section><h2>还要考虑什么<a href="#还要考虑什么"><span>#</span></a></h2><ul>
<li><strong>主从切换</strong>：主节点宕机、锁尚未同步到从节点时，可能出现两个客户端同时持锁。对一致性要求极高的场景，不要只依赖 Redis，考虑数据库唯一约束或 ZooKeeper 等方案。</li>
<li><strong>业务幂等</strong>：锁只是减少并发冲突，不应该是唯一的正确性保证。关键操作务必做幂等设计。</li>
<li><strong>锁粒度</strong>：锁越细越好，避免把整个服务串行化。</li>
</ul></section>
<section><h2>小结<a href="#小结"><span>#</span></a></h2><p>分布式锁是工具，不是保险箱。理解它的边界，再配合幂等与兜底，才能写出真正可靠的系统。</p></section>]]></content>
    </entry>
</feed>
