<?xml version="1.0" encoding="UTF-8"?><rss xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>xancel的自留地</title>
    <link>https://xancel.top/</link>
    <description><![CDATA[分享技术与生活]]></description>
    <language>zh-CN</language>
    <managingEditor>xancelzc@gmail.com (xancel)</managingEditor>
    <pubDate>Wed, 15 Apr 2026 12:01:27 +0000</pubDate>
    <lastBuildDate>Wed, 15 Apr 2026 12:01:27 +0000</lastBuildDate>
    <generator>grtblog v2.0.4</generator>
    <image>
      <url>https://tlias-test-1347803077.cos.ap-nanjing.myqcloud.com/%E7%BD%91%E7%AB%99%E5%9B%BE%E6%A0%87.png</url>
      <title>xancel的自留地</title>
      <link>https://xancel.top/</link>
    </image>
    <atom:link href="https://xancel.top/feed" rel="self" type="application/rss+xml"/><item>
      <title>开往</title>
      <link>https://xancel.top/go</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/go">https://xancel.top/go</a></p></blockquote><p><a href="https://www.travellings.cn/go.html">https://www.travellings.cn/go.html</a></p>]]></description>
      <guid>page-10</guid>
      <pubDate>Wed, 15 Apr 2026 12:01:27 +0000</pubDate>
    </item>
    <item>
      <title>完美的日子</title>
      <link>https://xancel.top/moments/2026/04/11/perfect-days</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/moments/2026/04/11/perfect-days">https://xancel.top/moments/2026/04/11/perfect-days</a></p></blockquote><p>2026.04.11</p>
<p>这是一部好好电影，值得看很多次，因为一次感觉有些意犹未尽，好像品出来些什么，但没有“成品”。</p>
<p>首先，这部电影很对我的审美，每一帧都很清新，整洁，有条不紊，其要表达的内涵我也能get到一点，但想要具体用语言描述清楚，也许我还得再看几遍，也许我还得再在生活中历练几段。</p>
<p>所以就我感悟到一些碎片，支离破碎的写一些，希望日后能将其拼凑完整。</p>
<p>1.主角对待生活的态度是我很喜欢，并且心底里隐约向往的，电影里提到的一句“以后是以后，现在是现在”，这句话我认为想表达的就是“活在当下”，也让我联想到《一句顶一万句》里面的“日子是过以后，不是过从前”。</p>
<p>2.主角是享受工作的吗？答案我认为是否。打扫厕所这件事对主角来说其实只是维持生活秩序的手段，从影片中可以看得出来，主角其实是不图钱不图名甚至不图感情（让其自然发生），这很接近我的“本我”，工作是生活中附带的一笔，它像是缝补衣服的麻线，让一些缺口规整起来。</p>
<p>3.那主角在体验或者说追求的是什么？从剧情中他的姐/妹可以看得出来他本来的物质条件是很不错的，住在乡下，独居，做厕所清洁工，这一切必然是他自己的选择。在这些选择背后，我认为主角追求的是“心灵的自由与平静”，他有善良的内心和善于发现美的眼睛，在生活中他凭借这两点就足以获得自己的追求。</p>
<p>无论今天有什么样的情绪波动，发生了什么事，总是按时睡前看书，到点睡觉，准时起床（加班除外哈哈哈），能做到这一点真的难得，主角不是圣人，他会哭，会笑，或者一边哭一边笑；也许他有自己的遗憾或者烦恼，也许他有自己内心的挣扎或者彷徨，但是这一切都不会阻碍：明早我要给我的小树浇水，出门前要买一罐咖啡，路上要听我喜欢的磁带，别忘了和陌生人的井字格游戏，别忘了给大树朋友拍照，别忘了晚上去喝一杯。</p>
<p>最后，生活充满变数，充满意外，但是也充满日升日落，晴天雨天，可能不确定中的一些确定能够让我们更好的面对这一切，加油。</p>]]></description>
      <author>xancel</author>
      <guid>moment-3</guid>
      <pubDate>Sat, 11 Apr 2026 16:13:44 +0000</pubDate>
    </item>
    <item>
      <title>稳住心态，保证睡眠，坚持锻炼，加油</title>
      <link>https://xancel.top/thinkings#thinking-12</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/thinkings#thinking-12">https://xancel.top/thinkings#thinking-12</a></p></blockquote><p>稳住心态，保证睡眠，坚持锻炼，加油</p>]]></description>
      <author>xancel</author>
      <guid>thinking-12</guid>
      <pubDate>Wed, 01 Apr 2026 12:50:36 +0000</pubDate>
    </item>
    <item>
      <title>要把决定做对，而不是做对的决定</title>
      <link>https://xancel.top/thinkings#thinking-11</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/thinkings#thinking-11">https://xancel.top/thinkings#thinking-11</a></p></blockquote><p>要把决定做对，而不是做对的决定</p>]]></description>
      <author>xancel</author>
      <guid>thinking-11</guid>
      <pubDate>Sun, 29 Mar 2026 08:12:30 +0000</pubDate>
    </item>
    <item>
      <title>平常心</title>
      <link>https://xancel.top/thinkings#thinking-10</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/thinkings#thinking-10">https://xancel.top/thinkings#thinking-10</a></p></blockquote><p>平常心</p>]]></description>
      <author>xancel</author>
      <guid>thinking-10</guid>
      <pubDate>Wed, 25 Mar 2026 12:08:47 +0000</pubDate>
    </item>
    <item>
      <title>Spring MVC，我是怎么理解的</title>
      <link>https://xancel.top/posts/spring-mvc</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/posts/spring-mvc">https://xancel.top/posts/spring-mvc</a></p></blockquote><p>我一直感觉SpringMVC的执行过程有些抽象，有些概念有点绕，于是我想着先把很多概念了解清楚，最后再把流程给捋清楚，尝试之后对我的理解还是很有效的，这篇文章经过我的整理输出后由AI润色和修正了格式等内容</p>
<h2>1. 什么是 Spring MVC</h2>
<p>Spring MVC 是 Spring 体系中的 Web MVC 框架，用来帮助我们基于 Servlet 快速开发 Web 应用。</p>
<p>这里的 MVC 指的是：</p>
<ul>
<li><code>Model</code>：模型数据，或者说业务数据</li>
<li><code>View</code>：视图，负责展示页面</li>
<li><code>Controller</code>：控制器，负责接收请求、组织调用、返回结果</li>
</ul>
<p>它的核心思想是把请求接入、参数处理、业务调用、结果响应这些职责拆开，让代码结构更清晰。</p>
<p>需要注意一点：<code>Controller</code> 负责的是请求入口和流程协调，真正的业务逻辑通常应该放在 <code>Service</code> 层，而不是全部堆在 <code>Controller</code> 里。</p>
<h2>2. 什么是 Servlet</h2>
<p>Servlet 本质上是 Java Web 的一套规范，用来定义服务器如何接收 HTTP 请求、如何处理请求、如何返回 HTTP 响应。</p>
<p>像 Tomcat、Jetty 这样的 Web 容器会实现这套规范，并负责：</p>
<ul>
<li>加载 Servlet</li>
<li>管理 Servlet 生命周期</li>
<li>在请求到来时调用对应的 Servlet</li>
</ul>
<p>可以把它理解成：Servlet 提供了 Java Web 最基础的请求处理能力，而 Spring MVC 是构建在这套能力之上的更高层封装。</p>
<h2>3. Spring MVC 和 Servlet 是什么关系</h2>
<p>Spring MVC 是建立在 Servlet 规范之上的。</p>
<p>它的核心入口是 <code>DispatcherServlet</code>。<code>DispatcherServlet</code> 本身就是一个 Servlet，所以它可以像普通 Servlet 一样被 Web 容器加载和调用。</p>
<p>也就是说：</p>
<ul>
<li>Web 容器负责把请求交给 <code>DispatcherServlet</code></li>
<li><code>DispatcherServlet</code> 负责在 Spring MVC 内部完成后续流程</li>
</ul>
<p>后续这些流程包括：</p>
<ul>
<li>路由分发</li>
<li>查找处理器</li>
<li>参数绑定</li>
<li>调用控制器方法</li>
<li>处理返回值</li>
<li>视图解析或响应体输出</li>
</ul>
<h2>4. Spring MVC 的几个核心组件</h2>
<h3>4.1 DispatcherServlet 是什么</h3>
<p><code>DispatcherServlet</code> 是 Spring MVC 的核心调度器，也是整个请求处理流程的统一入口。</p>
<p>浏览器发来的请求，通常会先到它这里，再由它决定后续交给谁处理。</p>
<p>它的职责可以概括为：</p>
<ul>
<li>接收请求</li>
<li>分发请求</li>
<li>协调各个组件一起工作</li>
<li>最终生成响应</li>
</ul>
<h3>4.2 HandlerMapping 是什么</h3>
<p><code>HandlerMapping</code> 可以理解成“请求路径到处理器的映射表”。</p>
<p>当请求到来时，它会根据请求信息去找到对应的处理器。匹配条件通常包括：</p>
<ul>
<li>URL</li>
<li>HTTP 请求方法</li>
<li>其他请求条件</li>
</ul>
<p>在注解驱动开发里，Spring 启动时会解析诸如 <code>@RequestMapping</code>、<code>@GetMapping</code>、<code>@PostMapping</code> 这类映射信息，并建立对应关系。</p>
<h3>4.3 Controller 和处理器是什么关系</h3>
<p>在日常开发里，我们会写很多 <code>@Controller</code> 或 <code>@RestController</code> 类。</p>
<p>对于注解驱动的 Spring MVC 来说，真正执行请求处理逻辑的，通常是 <code>Controller</code> 中某一个具体的方法。这个方法通常就可以理解为当前请求对应的处理器。</p>
<p>也就是说：</p>
<ul>
<li><code>Controller</code> 是处理请求的载体</li>
<li>具体的处理器，通常是其中某个带映射注解的方法</li>
</ul>
<p>所以 <code>HandlerMapping</code> 最终定位到的，通常不是整个类，而是类中的某个方法。</p>
<h3>4.4 HandlerAdapter 是什么</h3>
<p><code>HandlerAdapter</code> 可以理解成“处理器执行适配器”。</p>
<p><code>DispatcherServlet</code> 找到处理器之后，并不是自己直接用反射去调用，而是交给合适的 <code>HandlerAdapter</code> 来执行。</p>
<p>它主要负责：</p>
<ul>
<li>参数解析与绑定</li>
<li>类型转换</li>
<li>调用目标方法</li>
<li>处理方法返回值</li>
</ul>
<p>在基于注解的 Spring MVC 中，最常见的是 <code>RequestMappingHandlerAdapter</code>。</p>
<h3>4.5 ViewResolver 是什么</h3>
<p><code>ViewResolver</code> 是视图解析器。</p>
<p>当控制器返回的是一个“视图名”时，Spring MVC 会通过 <code>ViewResolver</code> 把这个名字解析成真正的视图对象，比如 JSP 或 Thymeleaf 模板。</p>
<h3>4.6 HttpMessageConverter 是什么</h3>
<p><code>HttpMessageConverter</code> 是消息转换器。</p>
<p>当前后端分离接口返回的是对象、JSON、XML 等响应体数据时，Spring MVC 会通过它把 Java 对象转换成 HTTP 响应内容。</p>
<p>最常见的场景就是把 Java 对象转换为 JSON。</p>
<h2>5. Controller 执行完之后，会返回什么</h2>
<p>这部分要分成两种场景来看。</p>
<h3>5.1 传统服务端页面渲染</h3>
<p>如果是传统 Web 项目，控制器方法通常可能返回：</p>
<ul>
<li>视图名，比如 <code>&quot;userList&quot;</code></li>
<li><code>ModelAndView</code></li>
</ul>
<p>这时 Spring MVC 的处理思路是：</p>
<ol>
<li>控制器方法执行完成</li>
<li>得到视图相关结果</li>
<li>交给 <code>ViewResolver</code> 解析视图</li>
<li>由视图技术渲染页面</li>
<li>最终把 HTML 返回给浏览器</li>
</ol>
<h3>5.2 前后端分离接口返回</h3>
<p>如果是前后端分离项目，控制器方法通常返回：</p>
<ul>
<li>一个普通对象</li>
<li><code>ResponseEntity</code></li>
<li>标注了 <code>@ResponseBody</code> 的返回结果</li>
</ul>
<p>如果类上使用了 <code>@RestController</code>，本质上也等价于默认带有 <code>@ResponseBody</code> 语义。</p>
<p>这时流程就不是“返回视图”，而是“直接返回数据”。</p>
<p>更准确地说，这类返回值会由 Spring MVC 的返回值处理机制配合 <code>HttpMessageConverter</code> 写入响应体，而不是再走视图解析。</p>
<p>所以这里不要简单理解成“HandlerAdapter 返回了 <code>null</code>”，更准确的理解应该是：</p>
<ul>
<li>控制器方法返回的是数据</li>
<li>Spring MVC 把这些数据写进 HTTP 响应体</li>
<li><code>DispatcherServlet</code> 不再进行视图渲染</li>
</ul>
<h2>6. 一次请求在 Spring MVC 中是怎么流转的</h2>
<p>把前面的内容串起来，一次典型请求大致会经过下面这些步骤。</p>
<ol>
<li>浏览器发起 HTTP 请求。</li>
<li>Web 容器接收到请求后，把请求交给 <code>DispatcherServlet</code>。</li>
<li><code>DispatcherServlet</code> 调用 <code>HandlerMapping</code> 查找当前请求对应的处理器。</li>
<li>找到处理器后，<code>DispatcherServlet</code> 再交给合适的 <code>HandlerAdapter</code> 去执行。</li>
<li><code>HandlerAdapter</code> 完成参数绑定、类型转换，然后调用目标控制器方法。</li>
<li>控制器方法执行业务流程，并返回结果。</li>
<li>如果返回的是视图名或 <code>ModelAndView</code>，则进入视图解析和页面渲染流程。</li>
<li>如果返回的是对象或 <code>ResponseEntity</code>，则通过 <code>HttpMessageConverter</code> 直接写入响应体。</li>
<li>最终响应返回给客户端。</li>
</ol>
<p>为了先把主线讲清楚，这里暂时没有展开拦截器、异常处理器、文件上传解析器这些扩展机制，但它们本质上也都是围绕这条主流程工作的。</p>
<h2>7. 几个容易混淆的点</h2>
<h3>7.1 Controller 不等于业务逻辑</h3>
<p><code>Controller</code> 的主要职责是接收请求、做参数承接、组织调用和返回结果。</p>
<p>真正的业务逻辑通常应该放在 <code>Service</code> 层。这样分层才清晰，也更容易维护。</p>
<h3>7.2 DispatcherServlet 不是“一个类包办一切”</h3>
<p>虽然 <code>DispatcherServlet</code> 是统一入口，但它并不是自己完成所有工作。</p>
<p>它更像一个总调度器，会把不同阶段的工作分别交给：</p>
<ul>
<li><code>HandlerMapping</code></li>
<li><code>HandlerAdapter</code></li>
<li><code>ViewResolver</code></li>
<li><code>HttpMessageConverter</code></li>
</ul>
<h3>7.3 前后端分离场景下，通常不会再走视图解析</h3>
<p>如果控制器返回的是 JSON 数据，那么重点就不再是 <code>ViewResolver</code>，而是返回值处理机制和 <code>HttpMessageConverter</code>。</p>
<p>也就是说，前后端分离项目和传统服务端页面渲染项目，后半段流程是不一样的。</p>]]></description>
      <author>xancel</author>
      <guid>article-7</guid>
      <pubDate>Tue, 24 Mar 2026 13:02:26 +0000</pubDate>
    </item>
    <item>
      <title>AI时代，如何放大凸显自己的长处，变得更加重要</title>
      <link>https://xancel.top/thinkings#thinking-9</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/thinkings#thinking-9">https://xancel.top/thinkings#thinking-9</a></p></blockquote><p>AI时代，如何放大凸显自己的长处，变得更加重要</p>]]></description>
      <author>xancel</author>
      <guid>thinking-9</guid>
      <pubDate>Fri, 20 Mar 2026 10:48:06 +0000</pubDate>
    </item>
    <item>
      <title>有时候感觉有那种摇滚精神，反叛精神是十分十分可贵的，那种燥的气息仿佛是生命力的外...</title>
      <link>https://xancel.top/thinkings#thinking-8</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/thinkings#thinking-8">https://xancel.top/thinkings#thinking-8</a></p></blockquote><p>有时候感觉有那种摇滚精神，反叛精神是十分十分可贵的，那种燥的气息仿佛是生命力的外显，怀念，昔日我曾风华正茂，如今我正苍老。</p>]]></description>
      <author>xancel</author>
      <guid>thinking-8</guid>
      <pubDate>Thu, 19 Mar 2026 16:40:12 +0000</pubDate>
    </item>
    <item>
      <title>AI 时代说个小暴论，越是在这种鼓吹人人都可以创造的时代，越要静下心来做一些长期...</title>
      <link>https://xancel.top/thinkings#thinking-6</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/thinkings#thinking-6">https://xancel.top/thinkings#thinking-6</a></p></blockquote><p>AI 时代说个小暴论，越是在这种鼓吹人人都可以创造的时代，越要静下心来做一些长期主义的事情。LLM 是一把锋利的刀，你买了这把刀，别人也可以买，这并不能带来什么本质性的差距。所以还是要沉下来，做一些长期主义、有积累的事情。</p>
<!-- raw HTML omitted -->]]></description>
      <author>xancel</author>
      <guid>thinking-6</guid>
      <pubDate>Tue, 17 Mar 2026 15:46:53 +0000</pubDate>
    </item>
    <item>
      <title>死亡是每个人必修的课题&#xA; &lt;p align=&#34;right&#34;&gt;—— 读《当呼吸化为...</title>
      <link>https://xancel.top/thinkings#thinking-5</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/thinkings#thinking-5">https://xancel.top/thinkings#thinking-5</a></p></blockquote><p>死亡是每个人必修的课题</p>
<!-- raw HTML omitted -->]]></description>
      <author>xancel</author>
      <guid>thinking-5</guid>
      <pubDate>Tue, 17 Mar 2026 12:31:36 +0000</pubDate>
    </item>
    <item>
      <title>Elasticsearch 入门</title>
      <link>https://xancel.top/posts/elasticsearch-guide</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/posts/elasticsearch-guide">https://xancel.top/posts/elasticsearch-guide</a></p></blockquote><h1>1.基础学习</h1>
<h2>一、 什么是 Elasticsearch？</h2>
<p><strong>Elasticsearch（简称 ES）</strong> 是一个基于 Lucene 的开源、分布式、RESTful 搜索引擎。它不仅是一个数据库，更是为了解决传统关系型数据库（如 MySQL）在<strong>全文检索</strong>、<strong>海量数据搜索</strong>和<strong>实时分析</strong>方面的不足而设计的。</p>
<h3>为什么不用 MySQL 的 LIKE 查询？</h3>
<ul>
<li><strong>性能差</strong>：<code>LIKE '%关键词%'</code> 无法利用索引，数据量大时查询极慢。</li>
<li><strong>结果不精准</strong>：无法根据相关性进行排序（比如“最匹配”的内容排在前面）。</li>
<li><strong>缺乏分词能力</strong>：难以处理复杂的自然语言搜索。</li>
</ul>
<hr>
<h2>二、 核心概念与实战操作</h2>
<h3>1. 核心术语对照</h3>
<table>
<thead>
<tr>
<th>ES 概念</th>
<th>MySQL 对应概念</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Index (索引)</strong></td>
<td>Table (表)</td>
<td>存储数据的容器。</td>
</tr>
<tr>
<td><strong>Mapping (映射)</strong></td>
<td>Schema (表结构)</td>
<td>定义字段类型、分词规则等。</td>
</tr>
<tr>
<td><strong>Document (文档)</strong></td>
<td>Row (行)</td>
<td>以 JSON 格式存储的具体数据。</td>
</tr>
</tbody>
</table>
<h3>2. 基础操作（DSL 语法）</h3>
<p>ES 使用 JSON 格式的 <strong>DSL</strong>（领域特定语言）进行交互，建议配合可视化工具 <strong>Kibana</strong> 使用。</p>
<ul>
<li>
<p><strong>创建索引</strong>：需指定字段类型，也就是映射mapping。<code>text</code> 用于全文检索（会分词），<code>keyword</code> 用于精确匹配（不分词）。
<!-- raw HTML omitted --></p>
</li>
<li>
<p><strong>插入数据</strong>：直接发送 POST 请求提交 JSON 文档。</p>
</li>
</ul>
<!-- raw HTML omitted -->
<ul>
<li><strong>搜索数据</strong>：使用 <code>match</code> 进行全文搜索，ES 会自动分词并计算匹配度。</li>
</ul>
<!-- raw HTML omitted -->
<h3>3. Java 客户端开发</h3>
<p>在生产中，通常使用 <strong>Spring Data Elasticsearch</strong>。它允许开发者通过定义接口（继承 <code>ElasticsearchRepository</code>）并使用注解（如 <code>@Document</code>）来操作 ES，就像使用 MyBatis-Plus 一样简单。后文会有代码示例</p>
<hr>
<h2>三、 ES 为什么这么快？（核心特性）</h2>
<h3>1. 倒排索引 (Inverted Index)</h3>
<p>这是 ES 的灵魂。它不直接存整行数据，而是记录<strong>单词 -&gt; 文档 ID 列表</strong>的映射。搜索时，系统先对搜索词进行分词，然后通过倒排索引直接定位包含这些词的文档，无需全表扫描。</p>
<h3>2. 分词器 (Analyzer)</h3>
<ul>
<li><strong>标准分词器</strong>：对中文支持较差（按字拆分）。</li>
<li><strong>IK 分词器</strong>：中文搜索必备，提供 <code>ik_smart</code>（智能切分）和 <code>ik_max_word</code>（最细粒度切分）模式。
<!-- raw HTML omitted --></li>
</ul>
<h3>3. 相关性评分</h3>
<p>ES 默认采用 <strong>BM25 算法</strong>，通过<strong>词频（TF）</strong>、<strong>逆文档频率（IDF）<strong><strong>和</strong></strong>文档长度</strong>来计算分数（_score），确保用户最想要的结果排在最前面。
<!-- raw HTML omitted --></p>
<hr>
<h2>四、 生产环境的挑战与方案</h2>
<h3>1. 数据同步</h3>
<p>由于 ES 和 MySQL 是独立系统，需要解决数据一致性问题：</p>
<ul>
<li><strong>定时任务</strong>：适合实时性要求不高的场景。</li>
<li><strong>双写</strong>：代码层面同时写入两端，但需处理写入失败。</li>
<li><strong>Canal 监听</strong>：实时监听 MySQL 的 Binlog 变更并同步，延迟低。</li>
</ul>
<h3>2. 高可用与扩展</h3>
<ul>
<li><strong>集群部署</strong>：通过多节点（主节点、数据节点）保证服务不宕机。</li>
<li><strong>分片 (Shard)</strong>：将大索引拆分存放在不同节点，提升存储上限和并发能力。</li>
<li><strong>副本 (Replica)</strong>：分片的备份，防止节点损坏导致数据丢失。</li>
</ul>
<h3>3. 常见进阶问题</h3>
<ul>
<li><strong>深度分页</strong>：ES 默认限制查询前 10,000 条。深层次翻页建议使用 <code>search_after</code>。</li>
<li><strong>ELK 生态</strong>：Elasticsearch (存储) + Logstash (收集) + Kibana (展示)，是目前主流的日志分析方案。</li>
</ul>
<hr>
<p><strong>建议</strong>：学习 ES 最好的方式是动手实践，先在本地搭建一个单机版环境，尝试通过 Kibana 练习各种 DSL 查询。</p>
<h1>2.代码实现</h1>
<p>我们通常会选择在 Spring Boot 项目中集成 <strong>Spring Data Elasticsearch</strong>。这是目前 Java 后端最主流、也是最高效的开发方式。</p>
<h2>1. 环境准备与配置</h2>
<p>首先，在项目的 <code>pom.xml</code> 中引入依赖：</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-data-elasticsearch&lt;/artifactId&gt;
&lt;/dependency&gt;

</code></pre>
<p>在 <code>application.yml</code> 中配置 ES 服务器地址：</p>
<pre><code class="language-yaml">spring:
  elasticsearch:
    uris: http://localhost:9200

</code></pre>
<hr>
<h2>2. 定义实体类（Mapping 映射）</h2>
<p>通过注解将 Java 对象映射为 ES 的索引结构。<code>text</code> 类型用于搜索，<code>keyword</code> 用于精确过滤。</p>
<pre><code class="language-java">@Data
@Document(indexName = &quot;article&quot;) // 指定索引名称
public class Article {
    @Id
    private Long id;

    // type = Text 表示支持全文检索，analyzer 指定分词器
    @Field(type = FieldType.Text, analyzer = &quot;ik_max_word&quot;, searchAnalyzer = &quot;ik_smart&quot;)
    private String title;

    @Field(type = FieldType.Text, analyzer = &quot;ik_max_word&quot;)
    private String content;

    // type = Keyword 表示不分词，直接匹配
    @Field(type = FieldType.Keyword)
    private String category;

    @Field(type = FieldType.Date, format = DateFormat.basic_date_time)
    private LocalDateTime createTime;
}

</code></pre>
<hr>
<h2>3. 编写 Repository 接口</h2>
<p>这是 Spring Data 的精髓，你只需要定义方法名，框架会自动帮你生成 DSL 查询语句。</p>
<pre><code class="language-java">public interface ArticleRepository extends ElasticsearchRepository&lt;Article, Long&gt; {
    
    // 自动解析：根据标题全文检索
    List&lt;Article&gt; findByTitle(String title);
    
    // 自动解析：根据分类精确匹配
    List&lt;Article&gt; findByCategory(String category);
    
    // 复合查询：标题匹配且分类一致
    List&lt;Article&gt; findByTitleAndCategory(String title, String category);
}

</code></pre>
<hr>
<h2>4. 高级搜索：使用 ElasticsearchTemplate</h2>
<p>如果 Repository 的方法名无法满足复杂的业务逻辑（如高亮、聚合、复杂的布尔过滤），可以使用 <code>ElasticsearchTemplate</code>。</p>
<h3>示例：带高亮和过滤的复合搜索</h3>
<pre><code class="language-java">@Autowired
private ElasticsearchRestTemplate elasticsearchRestTemplate;

public void complexSearch(String keyword) {
    // 1. 构建查询条件（类似 SQL 的 WHERE）
    NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
            .withQuery(QueryBuilders.boolQuery()
                    .must(QueryBuilders.matchQuery(&quot;title&quot;, keyword)) // 必须匹配标题
                    .should(QueryBuilders.matchQuery(&quot;content&quot;, keyword))) // 内容匹配可加分
            .withHighlightFields(new HighlightBuilder.Field(&quot;title&quot;)) // 设置高亮字段
            .withPageable(PageRequest.of(0, 10)) // 分页
            .build();

    // 2. 执行搜索
    SearchHits&lt;Article&gt; searchHits = elasticsearchRestTemplate.search(searchQuery, Article.class);

    // 3. 处理高亮结果
    searchHits.getSearchHits().forEach(hit -&gt; {
        Article article = hit.getContent();
        List&lt;String&gt; highlightTitle = hit.getHighlightField(&quot;title&quot;);
        if (!highlightTitle.isEmpty()) {
            article.setTitle(highlightTitle.get(0)); // 替换为带 &lt;em&gt; 标签的内容
        }
        System.out.println(&quot;搜索结果: &quot; + article);
    });
}

</code></pre>
<hr>
<h2>5. 核心逻辑总结</h2>
<p>在实际开发中，代码流程通常是：</p>
<ol>
<li><strong>MySQL 写入</strong>：文章发布成功。</li>
<li><strong>触发同步</strong>：通过 <code>articleRepository.save(article)</code> 同步到 ES。</li>
<li><strong>用户搜索</strong>：调用自定义的 <code>search</code> 方法，获取经过分词和评分排序后的结果。</li>
</ol>
<blockquote>
<p>注意：<br>
在本地调试时，请确保已经安装了 <strong>IK 分词器</strong> 插件，否则代码中的 <code>analyzer = &quot;ik_max_word&quot;</code> 会导致 ES 报错。</p>
</blockquote>]]></description>
      <author>xancel</author>
      <guid>article-6</guid>
      <pubDate>Mon, 16 Mar 2026 12:42:09 +0000</pubDate>
    </item>
    <item>
      <title></title>
      <link>https://xancel.top/message</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/message">https://xancel.top/message</a></p></blockquote>]]></description>
      <guid>page-9</guid>
      <pubDate>Mon, 16 Mar 2026 08:37:32 +0000</pubDate>
    </item>
    <item>
      <title>关于我</title>
      <link>https://xancel.top/about</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/about">https://xancel.top/about</a></p></blockquote><p>一个正在成长的大三学生</p>]]></description>
      <guid>page-8</guid>
      <pubDate>Mon, 16 Mar 2026 08:35:36 +0000</pubDate>
    </item>
    <item>
      <title>如果思考的页面可以插入图片就好了</title>
      <link>https://xancel.top/thinkings#thinking-4</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/thinkings#thinking-4">https://xancel.top/thinkings#thinking-4</a></p></blockquote><p>如果思考的页面可以插入图片就好了</p>]]></description>
      <author>xancel</author>
      <guid>thinking-4</guid>
      <pubDate>Sun, 15 Mar 2026 16:04:16 +0000</pubDate>
    </item>
    <item>
      <title>深入理解MySQL MVCC</title>
      <link>https://xancel.top/posts/mysql-mvcc</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/posts/mysql-mvcc">https://xancel.top/posts/mysql-mvcc</a></p></blockquote><h2>首先，什么是 MVCC</h2>
<p>MVCC 的中文是<strong>多版本并发控制</strong>，它是 InnoDB 在处理普通 <code>SELECT</code> 这类<strong>快照读</strong>时实现一致性读的核心机制，主要服务于<strong>读已提交（RC）</strong> 和 <strong>可重复读（RR）</strong> 这两个隔离级别。</p>
<p>并发事务里常见的冲突主要有两类：</p>
<ul>
<li>读写冲突：事务 A 在读一行数据，事务 B 同时修改这行数据</li>
<li>写写冲突：两个事务同时修改同一行数据</li>
</ul>
<p>写写冲突仍然需要依赖锁来保证串行化；MVCC 主要解决的是<strong>读写并发</strong>时，如何让读操作尽量不阻塞写操作。</p>
<p>需要注意，MVCC 主要用于<strong>快照读</strong>，不适用于所有读操作。像 <code>SELECT ... FOR UPDATE</code>、<code>SELECT ... FOR SHARE</code>、<code>UPDATE</code>、<code>DELETE</code> 这类<strong>当前读</strong>，仍然要结合锁来工作。</p>
<h2>MVCC 的三个核心点</h2>
<ol>
<li>
<p><strong>隐藏字段</strong></p>
<ul>
<li><code>trx_id</code>：最后一次修改该记录版本的事务 ID</li>
<li><code>roll_pointer</code>：指向该记录上一个历史版本的 undo log 指针</li>
</ul>
</li>
<li>
<p><strong>undo log 版本链</strong></p>
<p>一行记录每次被修改时，旧版本不会立刻消失，而是被写入 undo log，再通过 <code>roll_pointer</code> 串起来，形成一条从新到旧的版本链。</p>
</li>
<li>
<p><strong>Read View</strong></p>
<p>Read View 可以理解为事务做快照读时拿到的一份“可见性规则”。事务会基于这份规则，从版本链上判断哪个版本对自己可见。</p>
</li>
</ol>
<h2>Read View 中几个关键字段</h2>
<ul>
<li><code>m_ids</code>：创建 Read View 时，系统中活跃的读写事务 ID 集合</li>
<li><code>min_trx_id</code>：<code>m_ids</code> 中最小的事务 ID</li>
<li><code>max_trx_id</code>：创建 Read View 时，系统将要分配给下一个事务的 ID</li>
<li><code>creator_trx_id</code>：创建这个 Read View 的事务 ID</li>
</ul>
<p>这里要特别注意：</p>
<ul>
<li>记录上的 <code>trx_id</code>，表示“<strong>这个版本</strong>是由哪个事务生成的”</li>
<li><code>creator_trx_id</code>，表示“<strong>当前正在读取数据的事务</strong>是谁”</li>
</ul>
<p><strong>这两个不是同一个概念。</strong></p>
<h2>可见性判断算法</h2>
<p>当事务进行快照读时，会从版本链的最新版本开始判断可见性。这里参与判断的 <code>trx_id</code>，指的是“<strong>当前正在检查的那个记录版本的事务 ID</strong>”，不是当前读请求所属事务的 ID。</p>
<p>判断规则可以概括为：</p>
<ol>
<li>
<p>如果版本的 <code>trx_id == creator_trx_id</code>，<strong>该版本可见</strong></p>
<p>原因：这是当前事务自己生成的版本，当前事务当然可以看到。</p>
</li>
<li>
<p>如果版本的 <code>trx_id &lt; min_trx_id</code>，<strong>该版本可见</strong></p>
<p>原因：说明生成这个版本的事务，在 Read View 创建前就已经提交了。</p>
</li>
<li>
<p>如果版本的 <code>trx_id &gt;= max_trx_id</code>，<strong>该版本不可见</strong></p>
<p>原因：说明这个版本对应的事务，是在 Read View 创建之后才开始的。</p>
</li>
<li>
<p>如果 <code>min_trx_id &lt;= trx_id &lt; max_trx_id</code></p>
<ul>
<li><code>trx_id</code> 在 <code>m_ids</code> 中：<strong>不可见</strong>，说明该事务在创建 Read View 时仍然活跃，尚未提交</li>
<li><code>trx_id</code> 不在 <code>m_ids</code> 中：<strong>可见</strong>，说明该事务在创建 Read View 前已经提交</li>
</ul>
</li>
</ol>
<p>如果某个版本不可见，就顺着版本链继续找更老的版本，直到找到<strong>第一个可见版本</strong>为止。</p>
<h2>RC 和 RR 的本质区别</h2>
<ol>
<li>
<p><strong>RC（读已提交）</strong></p>
<p>每次执行快照读时，都会创建新的 Read View，所以同一个事务里两次普通 <code>SELECT</code>，可能看到不同的已提交结果，这就是不可重复读产生的原因。</p>
</li>
<li>
<p><strong>RR（可重复读）</strong></p>
<p>默认情况下，事务中的第一次快照读会创建 Read View，后续快照读复用同一个 Read View，因此同一个事务里的多次普通 <code>SELECT</code> 通常会看到同一份快照。</p>
</li>
</ol>
<p>这里的表述要稍微严谨一点：</p>
<ul>
<li>RR 下“保持一致”的是<strong>普通快照读</strong>看到的历史快照</li>
<li>如果当前事务自己更新了数据，那么它仍然可以看到自己更新后的结果</li>
<li>如果执行的是<strong>当前读</strong>，也不走这套快照可见性逻辑</li>
</ul>
<p>另外，InnoDB 中 RR 并不是“事务一启动就一定创建 Read View”。通常是<strong>第一次执行快照读时</strong>才创建；如果显式使用了 <code>START TRANSACTION WITH CONSISTENT SNAPSHOT</code>，则会在事务开始时创建一致性视图。</p>
<h2>举例说明版本选择过程</h2>
<p>假设有三个事务并发执行：</p>
<ul>
<li>事务 101：较早启动，修改了某行数据，但还没有提交</li>
<li>事务 102：稍后修改了同一行数据，并且已经提交</li>
<li>事务 103：当前事务，正在执行一次快照读</li>
</ul>
<p>假设这行数据原本的最后已提交版本是 <code>trx_id=100</code>。</p>
<p>之后：</p>
<ul>
<li>事务 102 修改该行，生成一个新版本，<code>trx_id=102</code></li>
<li>事务 101 又基于更新后的数据生成更“新”的版本，<code>trx_id=101</code>，但它尚未提交</li>
</ul>
<p>于是版本链可以表示为：</p>
<p><code>(101) -&gt; (102) -&gt; (100)</code></p>
<p>其中 <code>(101)</code> 是当前记录上最新的版本。</p>
<p>现在事务 103 在执行<strong>第一次快照读</strong>时创建 Read View。此时：</p>
<ul>
<li>活跃事务只有 101，所以 <code>m_ids = {101}</code></li>
<li><code>min_trx_id = 101</code></li>
<li><code>max_trx_id = 104</code>，表示下一个将被分配的事务 ID 是 104</li>
<li><code>creator_trx_id = 103</code></li>
</ul>
<p>事务 103 读取这行数据时，会这样判断：</p>
<ol>
<li>
<p><strong>先看最新版本 <code>(101)</code></strong></p>
<p><code>trx_id=101</code> 落在 <code>min_trx_id &lt;= trx_id &lt; max_trx_id</code> 区间内，并且 101 在 <code>m_ids</code> 中，说明该版本对应的事务在创建 Read View 时仍然活跃，因此不可见。</p>
</li>
<li>
<p><strong>顺着版本链看下一个版本 <code>(102)</code></strong></p>
<p><code>trx_id=102</code> 也还是落在<code>min_trx_id &lt;= trx_id &lt; max_trx_id</code> 区间内，但 102 不在 <code>m_ids</code> 中，说明它在 Read View 创建前已经提交，因此这个版本可见。</p>
</li>
<li>
<p><strong>返回版本 <code>(102)</code> 的数据</strong></p>
</li>
</ol>
<p>所以，事务 103 读到的是<strong>事务 102 提交后的值</strong>，而不是事务 101 尚未提交的值。</p>
<p>如果隔离级别是 RC，那么事务 103 每次执行快照读都会重新创建 Read View：</p>
<ul>
<li>只要事务 101 还没提交，新的 Read View 中仍然会把 101 视为活跃事务，因此看不到版本 101</li>
<li>一旦事务 101 提交，下一次快照读重新生成 Read View 时，101 不再出现在活跃事务列表中，此时就可能读到版本 101
::: link-card href=&quot;https://www.bilibili.com/video/BV1Yc411N7tK&quot; title=&quot;10分钟带你深刻理解MySQL中的MVCC机制&quot; desc=&quot;参考引用&quot; newtab=&quot;true&quot;</li>
</ul>
<p>:::
::: link-card href=&quot;https://www.bilibili.com/video/BV1Kr4y1i7ru&quot; title=&quot;MySQL数据库入门到精通&quot; desc=&quot;参考引用&quot; newtab=&quot;true&quot;</p>
<p>:::</p>]]></description>
      <author>xancel</author>
      <guid>article-5</guid>
      <pubDate>Sun, 15 Mar 2026 15:53:17 +0000</pubDate>
    </item>
    <item>
      <title>Life, the Universe and Everything.</title>
      <link>https://xancel.top/thinkings#thinking-2</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/thinkings#thinking-2">https://xancel.top/thinkings#thinking-2</a></p></blockquote><p>Life, the Universe and Everything.</p>]]></description>
      <author>xancel</author>
      <guid>thinking-2</guid>
      <pubDate>Sun, 15 Mar 2026 07:30:00 +0000</pubDate>
    </item>
    <item>
      <title>hello world</title>
      <link>https://xancel.top/thinkings#thinking-1</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/thinkings#thinking-1">https://xancel.top/thinkings#thinking-1</a></p></blockquote><p>hello world</p>]]></description>
      <author>xancel</author>
      <guid>thinking-1</guid>
      <pubDate>Fri, 13 Mar 2026 06:43:41 +0000</pubDate>
    </item>
    <item>
      <title>Cookie、Session与Token详解</title>
      <link>https://xancel.top/posts/cookie-session-token</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/posts/cookie-session-token">https://xancel.top/posts/cookie-session-token</a></p></blockquote><p>2025-11-10</p>
<h1>前言</h1>
<h2>无状态的HTTP协议</h2>
<p>很久很久之前， Web基本都是文档的浏览而已。既然是浏览， 作为服务器， 不需要记录在某一段时间里都浏览了什么文档， 每次请求都是一个新的HTTP协议，就是请求加响应。不用记录谁刚刚发了HTTP请求， 每次请求都是全新的。</p>
<h2>如何管理会话</h2>
<p>随着交互式Web应用的兴起， 像在线购物网站，需要登录的网站等，马上面临一个问题，就是要管理回话，记住那些人登录过系统，哪些人往自己的购物车中放商品，也就是说我必须把每个人区分开。</p>
<p>本文主要讲解cookie，session, token 这三种是如何管理会话的；</p>
<h1>cookie</h1>
<p>cookie 是一个非常具体的东西，指的就是浏览器里面能永久存储的一种数据。跟服务器没啥关系，仅仅是浏览器实现的一种数据存储功能。</p>
<p>cookie由服务器生成，发送给浏览器，浏览器把cookie以KV形式存储到某个目录下的文本文件中，下一次请求同一网站时会把该cookie发送给服务器。由于cookie是存在客户端上的，所以浏览器加入了一些限制确保cookie不会被恶意使用，同时不会占据太多磁盘空间。所以每个域的cookie数量是有限制的。</p>
<h2>如何设置</h2>
<h3>客户端设置</h3>
<pre><code class="language-javascript"> document.cookie = &quot;name=xiaoming; age=12 &quot;
</code></pre>
<ul>
<li>客户端可以设置cookie的一下选项: expires, domain, path, secure(只有在https协议的网页中, 客户端设置secure类型cookie才能生效), 但无法设置httpOnly选项</li>
</ul>
<blockquote>
<p>设置cookie =&gt; cookie被自动添加到request header中 =&gt; 服务端接收到cookie</p>
</blockquote>
<h3>服务端设置</h3>
<p>不管你是请求一个资源文件(如html/js/css/图片), 还是发送一个ajax请求, 服务端都会返回response.而response header中有一项叫<code>set-cookie</code>, 是服务端专门用来设置cookie的;</p>
<ul>
<li>一个set-cookie只能设置一个cookie, 当你想设置多个, 需要添加同样多的<code>set-cookie</code></li>
<li>服务端可以设置cookie的所有选项: expires, domain, path, secure, HttpOnly</li>
</ul>
<h3>Cookie，SessionStorage，LocalStorage</h3>
<p>HTML5提供了两种本地存储的方式 sessionStorage 和 localStorage；</p>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/6/13/16b4fb158b9ac13b~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png" alt="img"></p>
<h1>session</h1>
<h2>什么是session</h2>
<p>session从字面上讲，就是会话。这个就类似你和一个人交谈，你怎么知道当时和你交谈的是张三而不是李四呢？对方肯定有某种特征（长相等）表明他是张三； session也是类似的道理，服务器要知道当前请求发给自己的是谁。为了做这种区分，服务器就是要给每个客户端分配不同的&quot;身份标识&quot;，然后客户端每次向服务器发请求的时候，都带上这个”身份标识“，服务器就知道这个请求来自与谁了。 至于客户端怎么保存这个”身份标识“，可以有很多方式，对于浏览器客户端，大家都采用cookie的方式。</p>
<h2>过程(服务端session + 客户端 sessionId)</h2>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/6/13/16b4fb158d3a7cbb~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png" alt="session"></p>
<ul>
<li>1.用户向服务器发送用户名和密码</li>
<li>2.服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色, 登陆时间等;</li>
<li>3.服务器向用户返回一个<code>session_id</code>, 写入用户的<code>cookie</code></li>
<li>4.用户随后的每一次请求, 都会通过<code>cookie</code>, 将<code>session_id</code>传回服务器</li>
<li>5.服务端收到 <code>session_id</code>, 找到前期保存的数据, 由此得知用户的身份</li>
</ul>
<h2>存在的问题</h2>
<h3>扩展性不好</h3>
<p>单机当然没问题， 如果是服务器集群， 或者是跨域的服务导向架构， 这就要求session数据共享，每台服务器都能够读取session。</p>
<p>举例来说， A网站和B网站是同一家公司的关联服务。现在要求，用户只要在其中一个网站登录，再访问另一个网站就会自动登录，请问怎么实现？这个问题就是如何实现单点登录的问题</p>
<ol>
<li>
<p>Nginx ip_hash 策略，服务端使用 Nginx 代理，每个请求按访问 IP 的 hash 分配，这样来自同一 IP 固定访问一个后台服务器，避免了在服务器 A 创建 Session，第二次分发到服务器 B 的现象。</p>
</li>
<li>
<p>Session复制：任何一个服务器上的 Session 发生改变（增删改），该节点会把这个 Session 的所有内容序列化，然后广播给所有其它节点。</p>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/6/13/16b4fb158be01294~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png" alt="SessionCopy"></p>
</li>
<li>
<p>共享Session：将Session Id 集中存储到一个地方，所有的机器都来访问这个地方的数据。这种方案的优点是架构清晰，缺点是工程量比较大。另外，持久层万一挂了，就会单点失败；</p>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/6/13/16b4fb158bbf763c~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png" alt="session共享"></p>
</li>
</ol>
<p>另一种方案是服务器索性不保存session数据了，所有数据就保存在客户端，每次请求都发回服务器。这种方案就是接下来要介绍的基于Token的验证;</p>
<h1>Token</h1>
<h2>过程</h2>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/6/13/16b4fb158d0b2254~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png" alt="Token"></p>
<ol>
<li>用户通过用户名和密码发送请求</li>
<li>程序验证</li>
<li>程序返回一个签名的token给客户端</li>
<li>客户端储存token, 并且每次用每次发送请求</li>
<li>服务端验证Token并返回数据</li>
</ol>
<p>这个方式的技术其实很早就已经有很多实现了，而且还有现成的标准可用，这个标准就是JWT;</p>
<h2>JWT(JSON Web Token)</h2>
<h3>数据结构</h3>
<p>实际的JWT大概就像下面这样：</p>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/6/13/16b4fb176b42367c~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png" alt="JWT"></p>
<p>JSON Web Tokens由dot（.）分隔的三个部分组成，它们是：</p>
<ul>
<li>Header（头部）</li>
<li>Payload（负载）</li>
<li>Signature（签名）</li>
</ul>
<p>因此，JWT通常如下展示：</p>
<p><strong>xxxxx.yyyyy.zzzz</strong></p>
<h4>Header（头部）</h4>
<p>Header 是一个 JSON 对象</p>
<pre><code class="language-json"> {
  &quot;alg&quot;: &quot;HS256&quot;, // 表示签名的算法，默认是 HMAC SHA256（写成 HS256）
  &quot;typ&quot;: &quot;JWT&quot;  // 表示Token的类型，JWT 令牌统一写为JWT
}
</code></pre>
<h4>Payload（负载）</h4>
<p>Payload 部分也是一个 JSON 对象，用来存放实际需要传递的数据</p>
<pre><code class="language-json"> {
  // 7个官方字段
  &quot;iss&quot;: &quot;a.com&quot;, // issuer：签发人
  &quot;exp&quot;: &quot;1d&quot;, // expiration time： 过期时间
  &quot;sub&quot;: &quot;test&quot;, // subject: 主题
  &quot;aud&quot;: &quot;xxx&quot;, // audience： 受众
  &quot;nbf&quot;: &quot;xxx&quot;, // Not Before：生效时间
  &quot;iat&quot;: &quot;xxx&quot;, // Issued At： 签发时间
  &quot;jti&quot;: &quot;1111&quot;, // JWT ID：编号
  // 可以定义私有字段
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;admin&quot;: true
}
</code></pre>
<p>JWT 默认是不加密的，任何人都可以读到，所以不要把秘密信息放在这个部分。</p>
<h4>Signature（签名）</h4>
<p>Signature 是对前两部分的签名，防止数据被篡改。</p>
<p>首先，需要指定一个密钥(secret)。这个密钥只有服务器才知道，不能泄露给用户。然后，使用Header里面指定的签名算法（默认是 HMAC SHA256），按照下面的公式产生签名。</p>
<pre><code class="language-js"> HMACSHA256(base64UrlEncode(header) + &quot;.&quot; + base64UrlEncode(payload), secret)
</code></pre>
<p>算出签名后，把 Header、Payload、Signature 三个部分拼成一个字符串，每个部分之间用&quot;点&quot;（.）分隔，就可以返回给用户。</p>
<pre><code class="language-js"> JWT = Base64(Header) + &quot;.&quot; + Base64(Payload) + &quot;.&quot; + $Signature
</code></pre>
<blockquote>
<p>如何保证安全？</p>
</blockquote>
<ul>
<li>发送JWT要使用HTTPS；不使用HTTPS发送的时候，JWT里不要写入秘密数据</li>
<li>JWT的payload中要设置expire时间</li>
</ul>
<h3>使用方式</h3>
<p>客户端收到服务器返回的 JWT，可以储存在 Cookie 里面，也可以储存在 localStorage。此后，客户端每次与服务端通信，都要带上这个JWT。你可以把它放在Cookie里面自动发送，但是这样不能跨域，所以更好的做法是放在HTTP请求的头信息 Authorization 字段里面。</p>
<pre><code class="language-json">Authorization: Bearer &lt;token&gt;&lt;/token&gt;
</code></pre>
<p>另一种做法是， 跨域的时候， JWT就放在POST请求的数据体里。</p>
<h3>JWT 的作用</h3>
<p>JWT最开始的初衷是为了实现授权和身份认证作用的，可以实现无状态，分布式的Web应用授权。大致实现的流程如下</p>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/6/13/16b4fb15a64494a1~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png" alt="JWT"></p>
<ol>
<li>客户端需要携带用户名/密码等可证明身份的的内容去授权服务器获取JWT信息；</li>
<li>每次服务都携带该Token内容与Web服务器进行交互，由业务服务器来验证Token是否是授权发放的有效Token，来验证当前业务是否请求合法。</li>
</ol>
<p>这里需要注意：不是每次请求都要申请一次Token，这是需要注意，如果不是对于安全性要求的情况，不建议每次都申请，因为会增加业务耗时；比如只在登陆时申请，然后使用JWT的过期时间或其他手段来保证JWT的有效性；</p>
<h3>Acesss Token，Refresh Token</h3>
<p>JWT最大的优势是服务器不再需要存储Session，使得服务器认证鉴权业务可以方便扩展。这也是JWT最大的缺点由于服务器不需要存储Session状态，因此使用过程中无法废弃某个Token，或者更改Token的权限。也就是说一旦JWT签发了，到期之前就会始终有效。 我们可以基于上面提到的问题做一些改进。</p>
<p>前面讲的Token，都是Acesss Token，也就是访问资源接口时所需要的Token，还有另外一种Token，Refresh Token。一般情况下，Refresh Token的有效期会比较长。而Access Token的有效期比较短，当Acesss Token由于过期而失效时，使用Refresh Token就可以获取到新的Token，如果Refresh Token也失效了，用户就只能重新登录了。Refresh Token及过期时间是存储在服务器的数据库中，只有在申请新的Acesss Token时才会验证，不会对业务接口响应时间造成影响，也不需要向Session一样一直保持在内存中以应对大量的请求。</p>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/6/14/16b5568b15b433a3~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png" alt="Refresh Token"></p>
<h3>一个简单的JWT使用示例</h3>
<h4>JWT工具类</h4>
<pre><code class="language-java">package com.example.jwtdemo.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class JwtUtil {
    
    private static final String SECRET = &quot;your_secret_string&quot;;
    private static final long EXPIRATION = 60 * 1000; // 1分钟
    
    /**
     * 生成Token
     */
    public String generateToken(String userName) {
        Map&lt;String, Object&gt; claims = new HashMap&lt;&gt;();
        claims.put(&quot;iss&quot;, userName);
        
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
                .signWith(SignatureAlgorithm.HS256, SECRET)
                .compact();
    }
    
    /**
     * 从Token中获取用户名
     */
    public String getUserNameFromToken(String token) {
        Claims claims = getClaimsFromToken(token);
        return claims.get(&quot;iss&quot;, String.class);
    }
    
    /**
     * 验证Token是否过期
     */
    public boolean isTokenExpired(String token) {
        Claims claims = getClaimsFromToken(token);
        Date expiration = claims.getExpiration();
        return expiration.before(new Date());
    }
    
    /**
     * 解析Token
     */
    private Claims getClaimsFromToken(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}
</code></pre>
<h4>请求/响应DTO类</h4>
<pre><code class="language-javascript">package com.example.jwtdemo.dto;

import lombok.Data;

@Data
public class LoginRequest {
    private String userName;
    private String password;
}

</code></pre>
<pre><code class="language-java">package com.example.jwtdemo.dto;

import lombok.Data;

@Data
public class ResponseResult&lt;T&gt; {
    private String errorMsg;
    private String msg;
    private T data;
    
    public static &lt;T&gt; ResponseResult&lt;T&gt; success(T data, String msg) {
        ResponseResult&lt;T&gt; result = new ResponseResult&lt;&gt;();
        result.setData(data);
        result.setMsg(msg);
        result.setErrorMsg(&quot;&quot;);
        return result;
    }
    
    public static &lt;T&gt; ResponseResult&lt;T&gt; error(String errorMsg) {
        ResponseResult&lt;T&gt; result = new ResponseResult&lt;&gt;();
        result.setErrorMsg(errorMsg);
        result.setMsg(&quot;&quot;);
        result.setData(null);
        return result;
    }
}
</code></pre>
<pre><code class="language-java">package com.example.jwtdemo.dto;

import lombok.Data;

@Data
public class UserInfo {
    private String username;
}
</code></pre>
<h4>控制器类 (AuthController.java)</h4>
<pre><code class="language-java">package com.example.jwtdemo.controller;

import com.example.jwtdemo.dto.LoginRequest;
import com.example.jwtdemo.dto.ResponseResult;
import com.example.jwtdemo.dto.UserInfo;
import com.example.jwtdemo.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@CrossOrigin(origins = &quot;*&quot;) // 允许跨域
public class AuthController {
    
    @Autowired
    private JwtUtil jwtUtil;
    
    @PostMapping(&quot;/login&quot;)
    public ResponseResult&lt;String&gt; login(@RequestBody LoginRequest request) {
        String userName = request.getUserName();
        String password = request.getPassword();
        
        // 简单的验证逻辑，实际项目中需要查询数据库
        if (userName == null || userName.trim().isEmpty()) {
            return ResponseResult.error(&quot;用户名不能为空&quot;);
        }
        
        String token = jwtUtil.generateToken(userName);
        return ResponseResult.success(token, &quot;登陆成功&quot;);
    }
    
    @GetMapping(&quot;/getUsername&quot;)
    public ResponseResult&lt;UserInfo&gt; getUsername(@RequestHeader(&quot;Authorization&quot;) String authHeader) {
        try {
            // 提取Bearer Token
            String token = authHeader.substring(7);
            
            if (jwtUtil.isTokenExpired(token)) {
                return ResponseResult.error(&quot;Token已过期，请重新登录&quot;);
            }
            
            String userName = jwtUtil.getUserNameFromToken(token);
            UserInfo userInfo = new UserInfo();
            userInfo.setUsername(userName);
            
            return ResponseResult.success(userInfo, &quot;获取用户名成功&quot;);
        } catch (Exception e) {
            return ResponseResult.error(&quot;Token验证失败&quot;);
        }
    }
}
</code></pre>
<h4>主应用类 (JwtDemoApplication.java)</h4>
<pre><code class="language-java">package com.example.jwtdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JwtDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(JwtDemoApplication.class, args);
        System.out.println(&quot;启动成功，端口：3200&quot;);
    }
}
</code></pre>
<h4>Maven依赖</h4>
<pre><code class="language-xml">&lt;dependencies&gt;
        &lt;!-- Spring Boot Web --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
            &lt;version&gt;2.7.0&lt;/version&gt;
        &lt;/dependency&gt;
        
        &lt;!-- JWT --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
            &lt;artifactId&gt;jjwt&lt;/artifactId&gt;
            &lt;version&gt;0.9.1&lt;/version&gt;
        &lt;/dependency&gt;
        
        &lt;!-- Lombok --&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
            &lt;artifactId&gt;lombok&lt;/artifactId&gt;
            &lt;version&gt;1.18.24&lt;/version&gt;
            &lt;scope&gt;provided&lt;/scope&gt;
        &lt;/dependency&gt;
    &lt;/dependencies&gt;
</code></pre>
<h4>配置文件 (application.properties)</h4>
<pre><code class="language-properties">server.port=3200
server.servlet.context-path=/
# 日志配置
logging.level.com.example.jwtdemo=DEBUG
</code></pre>
<h4>前端HTML页面 (resources/static/index.html)</h4>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;head&gt;
    &lt;meta charset=&quot;UTF-8&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot;&gt;
    &lt;title&gt;JWT-demo&lt;/title&gt;
    &lt;style&gt;
        .login-wrap {
            height: 100px;
            width: 200px;
            border: 1px solid #ccc;
            padding: 20px;
            margin-bottom: 20px;
        }
        input {
            margin: 5px 0;
            padding: 5px;
            width: 180px;
        }
        button {
            padding: 5px 10px;
            margin: 5px 0;
        }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div class=&quot;login-wrap&quot;&gt;
        &lt;input type=&quot;text&quot; placeholder=&quot;用户名&quot; class=&quot;userName&quot;&gt;
        &lt;br&gt;
        &lt;input type=&quot;password&quot; placeholder=&quot;密码&quot; class=&quot;password&quot;&gt;
        &lt;br&gt;
        &lt;br&gt;
        &lt;button class=&quot;btn&quot;&gt;登陆&lt;/button&gt;
    &lt;/div&gt;
    
    &lt;button class=&quot;btn1&quot;&gt;获取用户名&lt;/button&gt;
    &lt;p class=&quot;username&quot;&gt;&lt;/p&gt;

    &lt;script&gt;
        var btn = document.querySelector('.btn');
        
        btn.onclick = function () {
            var userName = document.querySelector('.userName').value;
            var password = document.querySelector('.password').value;
            
            fetch('http://localhost:3200/login', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    userName: userName,
                    password: password
                })
            })
            .then(function (response) {
                return response.json();
            })
            .then(function (res) {
                // 获取到Token，将Token存储在localStorage
                if (res.errorMsg) {
                    alert('登录失败: ' + res.errorMsg);
                    return;
                }
                
                localStorage.setItem('token', res.data);
                localStorage.setItem('token_exp', new Date().getTime());
                alert(res.msg);
            })
            .catch(err =&gt; {
                alert('本地测试错误: ' + err.message);
                console.error('本地测试错误', err);
            });
        }
        
        var btn1 = document.querySelector('.btn1');
        btn1.onclick = function () {
            var usernameElement = document.querySelector('.username');
            const token = localStorage.getItem('token');
            
            if (!token) {
                alert('请先登录');
                return;
            }
            
            fetch('http://localhost:3200/getUsername', {
                headers: {
                    'Authorization': 'Bearer ' + token
                }
            })
            .then(function (response) {
                return response.json();
            })
            .then(function (res) {
                console.log('返回用户信息结果', res);
                if (res.errorMsg) {
                    alert(res.errorMsg);
                    usernameElement.innerHTML = '';
                } else {
                    usernameElement.innerHTML = `姓名：${res.data.username}`;
                }
            })
            .catch(err =&gt; {
                console.error(err);
                alert('请求失败: ' + err.message);
            });
        }
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h4>运行说明</h4>
<ol>
<li>
<p><strong>启动应用</strong>：运行 <code>JwtDemoApplication.java</code>的 main 方法</p>
</li>
<li>
<p><strong>访问页面</strong>：打开浏览器访问 <code>http://localhost:3200/index.html</code></p>
</li>
<li>
<p><strong>测试流程</strong>：</p>
<ul>
<li>
<p>输入用户名密码点击登录</p>
</li>
<li>
<p>登录成功后点击&quot;获取用户名&quot;按钮</p>
</li>
<li>
<p>等待1分钟后Token过期，再次点击&quot;获取用户名&quot;会显示过期提示</p>
</li>
</ul>
</li>
</ol>
<h4>运行代码</h4>
<p><img src="https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/6/13/16b518ca6e85a854~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.png" alt="JWT"></p>
<h1>区别</h1>
<h2>Cookie和Session的区别</h2>
<ol>
<li>存储位置不同： cookie数据存放在客户的浏览器上，session数据放在服务器上</li>
<li>隐私策略不同：cookie不是很安全， 别人可以分析存放在本地的cookie并进行cookie欺骗，考虑到安全应当使用session</li>
<li>session会在一定时间内保存在服务器上。当访问增多，就会比较占用你服务器的性能，考虑到减轻服务器性能方面，应当使用cookie</li>
<li>存储大小不同： 单个cookie保存的数据不能超过4k, 很多浏览器都限制一个站点最多保存20个cookie</li>
</ol>
<blockquote>
<p>一般建议： 将登陆信息等重要信息存放为session, 其他信息如果需要保留，可以放在cookie中</p>
</blockquote>
<h2>Token和Session的区别</h2>
<p>Session是一种HTTP储存机制， 为无状态的HTTP提供持久机制; Token就是令牌， 比如你授权(登录)一个程序时，它就是个依据，判断你是否已经授权该软件；</p>
<p>Session和Token并不矛盾，作为身份认证Token安全性比Session好，因为每一个请求都有签名还能防止监听以及重放攻击，而Session就必须依赖链路层来保障通讯安全了。如上所说，如果你需要实现有状态的回话，仍然可以增加Session来在服务端保存一些状态。</p>
<h1>总结</h1>
<p>cookie，session，Token没有绝对的好与坏之分，只要还是要结合实际的业务场景和需求来决定采用哪种方式来管理回话，当然也可以三种都用。</p>
<h2>参考引用</h2>
<ul>
<li><a href="https://juejin.cn/post/6844903864810864647#heading-5">详解 Cookie，Session，Token</a></li>
<li><a href="https://www.cnblogs.com/moyand/p/9047978.html" title="https://www.cnblogs.com/moyand/p/9047978.html">彻底理解cookie，session，token</a></li>
<li><a href="https://www.jianshu.com/p/bd1be47a16c1" title="https://www.jianshu.com/p/bd1be47a16c1">Cookie、Session、Token那点事儿（原创）</a></li>
<li><a href="https://www.cnblogs.com/lyzg/p/6067766.html" title="https://www.cnblogs.com/lyzg/p/6067766.html">3种web会话管理的方式</a></li>
<li><a href="https://juejin.cn/post/6844903842773991431" title="https://juejin.cn/post/6844903842773991431">你真的了解 Cookie 和 Session 吗</a></li>
<li><a href="https://zhuanlan.zhihu.com/p/38942172" title="https://zhuanlan.zhihu.com/p/38942172">不要用JWT替代session管理（上）：全面了解Token,JWT,OAuth,SAML,SSO</a></li>
<li><a href="https://wuch886.gitbooks.io/front-end-handbook/content/session-cookiehe-token-san-zhe-de-guan-xi-he-qu-bie.html">Session 、Cookie和Token三者的关系和区别</a></li>
</ul>]]></description>
      <author>xancel</author>
      <guid>article-4</guid>
      <pubDate>Fri, 13 Mar 2026 06:42:45 +0000</pubDate>
    </item>
    <item>
      <title>Redission分布式锁</title>
      <link>https://xancel.top/posts/redission-lock</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/posts/redission-lock">https://xancel.top/posts/redission-lock</a></p></blockquote><p>2025-06-01</p>
<h1>1 分布式锁-redission功能介绍</h1>
<p>基于setnx实现的分布式锁存在下面的问题：</p>
<p><strong>重入问题</strong>：重入问题是指 获得锁的线程可以再次进入到相同的锁的代码块中，可重入锁的意义在于防止死锁，比如HashTable这样的代码中，他的方法都是使用synchronized修饰的，假如他在一个方法内，调用另一个方法，那么此时如果是不可重入的，不就死锁了吗？所以可重入锁他的主要意义是防止死锁，我们的synchronized和Lock锁都是可重入的。</p>
<p><strong>不可重试</strong>：是指目前的分布式只能尝试一次，我们认为合理的情况是：当线程在获得锁失败后，他应该能再次尝试获得锁。</p>
<p><strong>超时释放</strong>：我们在加锁时增加了过期时间，这样的我们可以防止死锁，但是如果卡顿的时间超长，虽然我们采用了lua表达式防止删锁的时候，误删别人的锁，但是毕竟没有锁住，有安全隐患</p>
<p><strong>主从一致性：</strong> 如果Redis提供了主从集群，当我们向集群写数据时，主机需要异步的将数据同步给从机，而万一在同步过去之前，主机宕机了，就会出现死锁问题。</p>
<p>那么什么是Redission呢
<img src="/uploads/pictures/2026-03-13-06:35:10-c1.png" alt="image.png"></p>
<p>Redisson是一个在Redis的基础上实现的Java驻内存数据网格（In-Memory Data Grid）。它不仅提供了一系列的分布式的Java常用对象，还提供了许多分布式服务，其中就包含了各种分布式锁的实现。</p>
<p>Redission提供了分布式锁的多种多样的功能</p>
<h1>2 分布式锁-Redission快速入门</h1>
<p>引入依赖：</p>
<pre><code class="language-java">&lt;dependency&gt;
	&lt;groupId&gt;org.redisson&lt;/groupId&gt;
	&lt;artifactId&gt;redisson&lt;/artifactId&gt;
	&lt;version&gt;3.13.6&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>配置Redisson客户端：</p>
<pre><code class="language-java">@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress(&quot;redis://192.168.150.101:6379&quot;)
            .setPassword(&quot;123321&quot;);
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
}

</code></pre>
<p>如何使用Redission的分布式锁</p>
<pre><code class="language-java">@Resource
private RedissionClient redissonClient;

@Test
void testRedisson() throws Exception{
    //获取锁(可重入)，指定锁的名称
    RLock lock = redissonClient.getLock(&quot;anyLock&quot;);
    //尝试获取锁，参数分别是：获取锁的最大等待时间(期间会重试)，锁自动释放时间，时间单位
    boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
    //判断获取锁成功
    if(isLock){
        try{
            System.out.println(&quot;执行业务&quot;);    
        }finally{
            //释放锁
            lock.unlock();
        }
  
    }
  
  
  
}
</code></pre>
<p>在 VoucherOrderServiceImpl</p>
<p>注入RedissonClient</p>
<pre><code class="language-java">@Resource
private RedissonClient redissonClient;

@Override
public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail(&quot;秒杀尚未开始！&quot;);
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail(&quot;秒杀已经结束！&quot;);
        }
        // 4.判断库存是否充足
        if (voucher.getStock() &lt; 1) {
            // 库存不足
            return Result.fail(&quot;库存不足！&quot;);
        }
        Long userId = UserHolder.getUser().getId();
        //创建锁对象 这个代码不用了，因为我们现在要使用分布式锁
        //SimpleRedisLock lock = new SimpleRedisLock(&quot;order:&quot; + userId, stringRedisTemplate);
        RLock lock = redissonClient.getLock(&quot;lock:order:&quot; + userId);
        //获取锁对象
        boolean isLock = lock.tryLock();
   
		//加锁失败
        if (!isLock) {
            return Result.fail(&quot;不允许重复下单&quot;);
        }
        try {
            //获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
 }
</code></pre>
<h1>3 分布式锁-redission可重入锁原理</h1>
<p>在Lock锁中，他是借助于底层的一个voaltile的一个state变量来记录重入的状态的，比如当前没有人持有这把锁，那么state=0，假如有人持有这把锁，那么state=1，如果持有这把锁的人再次持有这把锁，那么state就会+1 ，如果是对于synchronized而言，他在c语言代码中会有一个count，原理和state类似，也是重入一次就加一，释放一次就-1 ，直到减少成0 时，表示当前这把锁没有被人持有。</p>
<p>在redission中，我们的也支持支持可重入锁</p>
<p>在分布式锁中，他采用hash结构用来存储锁，其中大key表示表示这把锁是否存在，用小key表示当前这把锁被哪个线程持有，所以接下来我们一起分析一下当前的这个lua表达式</p>
<p>这个地方一共有3个参数</p>
<p><strong>KEYS[1] ： 锁名称</strong></p>
<p><strong>ARGV[1]：  锁失效时间</strong></p>
<p><strong>ARGV[2]：  id + &quot;:&quot; + threadId; 锁的小key</strong></p>
<p>exists: 判断数据是否存在  name：是lock是否存在,如果==0，就表示当前这把锁不存在</p>
<p>redis.call('hset', KEYS[1], ARGV[2], 1);此时他就开始往redis里边去写数据 ，写成一个hash结构</p>
<p>Lock{</p>
<pre><code>id +
</code></pre>
<p><strong>&quot;:&quot;</strong> + threadId :  1</p>
<p>}</p>
<p>如果当前这把锁存在，则第一个条件不满足，再判断</p>
<p>redis.call('hexists', KEYS[1], ARGV[2]) == 1</p>
<p>此时需要通过大key+小key判断当前这把锁是否是属于自己的，如果是自己的，则进行</p>
<p>redis.call('hincrby', KEYS[1], ARGV[2], 1)</p>
<p>将当前这个锁的value进行+1 ，redis.call('pexpire', KEYS[1], ARGV[1]); 然后再对其设置过期时间，如果以上两个条件都不满足，则表示当前这把锁抢锁失败，最后返回pttl，即为当前这把锁的失效时间</p>
<p>如果小伙帮们看了前边的源码， 你会发现他会去判断当前这个方法的返回值是否为null，如果是null，则对应则前两个if对应的条件，退出抢锁逻辑，如果返回的不是null，即走了第三个分支，在源码处会进行while(true)的自旋抢锁。</p>
<pre><code class="language-lua">&quot;if (redis.call('exists', KEYS[1]) == 0) then &quot; +
                  &quot;redis.call('hset', KEYS[1], ARGV[2], 1); &quot; +
                  &quot;redis.call('pexpire', KEYS[1], ARGV[1]); &quot; +
                  &quot;return nil; &quot; +
              &quot;end; &quot; +
              &quot;if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then &quot; +
                  &quot;redis.call('hincrby', KEYS[1], ARGV[2], 1); &quot; +
                  &quot;redis.call('pexpire', KEYS[1], ARGV[1]); &quot; +
                  &quot;return nil; &quot; +
              &quot;end; &quot; +
              &quot;return redis.call('pttl', KEYS[1]);&quot;
</code></pre>
<p><img src="/uploads/pictures/2026-03-13-06:35:46-22.png" alt="image.png"></p>
<h1>4 分布式锁-redission锁重试和WatchDog机制</h1>
<p><strong>说明</strong>：由于课程中已经说明了有关tryLock的源码解析以及其看门狗原理，所以笔者在这里给大家分析lock()方法的源码解析，希望大家在学习过程中，能够掌握更多的知识</p>
<p>抢锁过程中，获得当前线程，通过tryAcquire进行抢锁，该抢锁逻辑和之前逻辑相同</p>
<p>1、先判断当前这把锁是否存在，如果不存在，插入一把锁，返回null</p>
<p>2、判断当前这把锁是否是属于当前线程，如果是，则返回null</p>
<p>所以如果返回是null，则代表着当前这哥们已经抢锁完毕，或者可重入完毕，但是如果以上两个条件都不满足，则进入到第三个条件，返回的是锁的失效时间，同学们可以自行往下翻一点点，你能发现有个while( true) 再次进行tryAcquire进行抢锁</p>
<pre><code class="language-java">long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
    return;
}
</code></pre>
<p>接下来会有一个条件分支，因为lock方法有重载方法，一个是带参数，一个是不带参数，如果带带参数传入的值是-1，如果传入参数，则leaseTime是他本身，所以如果传入了参数，此时leaseTime != -1 则会进去抢锁，抢锁的逻辑就是之前说的那三个逻辑</p>
<pre><code class="language-java">if (leaseTime != -1) {
    return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
</code></pre>
<p>如果是没有传入时间，则此时也会进行抢锁， 而且抢锁时间是默认看门狗时间 commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()</p>
<p>ttlRemainingFuture.onComplete((ttlRemaining, e) 这句话相当于对以上抢锁进行了监听，也就是说当上边抢锁完毕后，此方法会被调用，具体调用的逻辑就是去后台开启一个线程，进行续约逻辑，也就是看门狗线程</p>
<pre><code class="language-java">RFuture&lt;Long&gt; ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                        commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                        TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -&gt; {
    if (e != null) {
        return;
    }

    // lock acquired
    if (ttlRemaining == null) {
        scheduleExpirationRenewal(threadId);
    }
});
return ttlRemainingFuture;
</code></pre>
<p>此逻辑就是续约逻辑，注意看commandExecutor.getConnectionManager().newTimeout（） 此方法</p>
<p>Method(  <strong>new</strong> TimerTask() {},参数2 ，参数3  )</p>
<p>指的是：通过参数2，参数3 去描述什么时候去做参数1的事情，现在的情况是：10s之后去做参数一的事情</p>
<p>因为锁的失效时间是30s，当10s之后，此时这个timeTask 就触发了，他就去进行续约，把当前这把锁续约成30s，如果操作成功，那么此时就会递归调用自己，再重新设置一个timeTask()，于是再过10s后又再设置一个timerTask，完成不停的续约</p>
<p>那么大家可以想一想，假设我们的线程出现了宕机他还会续约吗？当然不会，因为没有人再去调用renewExpiration这个方法，所以等到时间之后自然就释放了。</p>
<pre><code class="language-java">private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
  
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
      
            RFuture&lt;Boolean&gt; future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -&gt; {
                if (e != null) {
                    log.error(&quot;Can't update lock &quot; + getName() + &quot; expiration&quot;, e);
                    return;
                }
          
                if (res) {
                    // reschedule itself
                    renewExpiration();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
  
    ee.setTimeout(task);
}
</code></pre>
<h1>5 分布式锁-redission锁的MutiLock原理</h1>
<p>为了提高redis的可用性，我们会搭建集群或者主从，现在以主从为例</p>
<p>此时我们去写命令，写在主机上， 主机会将数据同步给从机，但是假设在主机还没有来得及把数据写入到从机去的时候，此时主机宕机，哨兵会发现主机宕机，并且选举一个slave变成master，而此时新的master中实际上并没有锁信息，此时锁信息就已经丢掉了。</p>
<p><img src="/uploads/pictures/2026-03-13-06:36:37-c6.png" alt="image.png"></p>
<p>为了解决这个问题，redission提出来了MutiLock锁，使用这把锁咱们就不使用主从了，每个节点的地位都是一样的， 这把锁加锁的逻辑需要写入到每一个主丛节点上，只有所有的服务器都写入成功，此时才是加锁成功，假设现在某个节点挂了，那么他去获得锁的时候，只要有一个节点拿不到，都不能算是加锁成功，就保证了加锁的可靠性。</p>
<p><img src="/uploads/pictures/2026-03-13-06:36:51-6c.png" alt="image.png"></p>
<p>那么MutiLock 加锁原理是什么呢？笔者画了一幅图来说明</p>
<p>当我们去设置了多个锁时，redission会将多个锁添加到一个集合中，然后用while循环去不停去尝试拿锁，但是会有一个总共的加锁时间，这个时间是用需要加锁的个数 * 1500ms ，假设有3个锁，那么时间就是4500ms，假设在这4500ms内，所有的锁都加锁成功， 那么此时才算是加锁成功，如果在4500ms有线程加锁失败，则会再次去进行重试.</p>
<p><img src="/uploads/pictures/2026-03-13-06:37:04-3b.png" alt="image.png"></p>]]></description>
      <author>xancel</author>
      <guid>article-3</guid>
      <pubDate>Fri, 13 Mar 2026 06:37:22 +0000</pubDate>
    </item>
    <item>
      <title>各式各样的工具</title>
      <link>https://xancel.top/posts/toolbox</link>
      <description><![CDATA[<blockquote><p>该内容由 RSS 渲染生成，最佳阅读体验请前往：<a href="https://xancel.top/posts/toolbox">https://xancel.top/posts/toolbox</a></p></blockquote><p>2025-06-10</p>
<h2><strong>图片类</strong></h2>
<h3><strong>作图</strong></h3>
<ul>
<li>
<p>在线手写风格作图白板<a href="https://excalidraw.com/">Excalidraw</a></p>
</li>
<li>
<p>在线矢量图作图软件（draw.io）<a href="https://app.diagrams.net/">app.diagrams.net</a></p>
</li>
<li>
<p>在线图论作图网站<a href="https://csacademy.com/app/graph_editor/">Graph Editor</a></p>
</li>
<li>
<p>tikz 有限状态机可视化制作<a href="https://madebyevan.com/fsm/">Finite State Machine Designer</a></p>
</li>
</ul>
<h3><strong>识图</strong></h3>
<ul>
<li><a href="https://saucenao.com/">SauceNAO</a></li>
</ul>
<h3><strong>修改图片</strong></h3>
<ul>
<li>
<p>移除背景（完全免费）<a href="https://www.remove.bg/">RemoveBg</a></p>
</li>
<li>
<p>纯前端免费图片压缩<a href="https://www.picdiet.com/zh-cn">PicDiet</a></p>
</li>
<li>
<p>图片压缩（需上传）<a href="https://docsmall.com/image-compress">DocSmall</a></p>
</li>
<li>
<p>擦除图片上物品（免费功能受限）<a href="https://cleanup.pictures/">cleanup.pictures</a></p>
</li>
</ul>
<h2><strong>代码类</strong></h2>
<h3><strong>可视化</strong></h3>
<ul>
<li>
<p>正则表达式可视化<a href="https://jex.im/regulex/">Regulex</a></p>
</li>
<li>
<p>Python 执行过程可视化<a href="https://pythontutor.com/visualize.html#mode=edit">PythonTutor</a></p>
</li>
<li>
<p>GLSL 流体场可视化<a href="https://anvaka.github.io/fieldplay/">Field Play</a></p>
</li>
</ul>
<h3><strong>编解码</strong></h3>
<ul>
<li>
<p>超强多合一编解码工具<a href="https://gchq.github.io/CyberChef/">CyberChef</a></p>
</li>
<li>
<p>三词地址（用三个单词编码一个地点）<a href="https://map.what3words.com/">what3words</a></p>
</li>
</ul>
<h3><strong>学习工具</strong></h3>
<ul>
<li>
<p>缓动曲线<a href="https://easings.net/">Easing Function Cheat Sheet</a></p>
</li>
<li>
<p>多编译器编译结果浏览<a href="https://godbolt.org/">Compiler Explorer</a></p>
</li>
<li>
<p>大 O 复杂度比较查询<a href="https://www.bigocheatsheet.com/">Big O Cheat Sheet</a></p>
</li>
<li>
<p>C 类型声明转英文描述<a href="https://cdecl.org/">cdecl</a></p>
</li>
</ul>
<h2><strong>设计类</strong></h2>
<h3><strong>配色</strong></h3>
<ul>
<li>
<p>配色方案网站<a href="https://colorhunt.co/">Color Hunt</a></p>
</li>
<li>
<p>配色方案网站（带 Figma 插件） <a href="https://coolors.co/">Coolors</a></p>
</li>
<li>
<p>颜色查找及转换工具<a href="https://hexcolor16.com/">HexColor16</a></p>
</li>
<li>
<p>日式传统颜色列表<a href="https://nipponcolors.com/">Nippon Colors</a></p>
</li>
<li>
<p>日系颜色名称列表 <a href="https://colorsite.librian.net/">Color Site</a></p>
</li>
<li>
<p>Flat UI 配色方案<a href="https://flatuicolors.com/">Flat UI Colors</a></p>
</li>
</ul>
<h3><strong>图标</strong></h3>
<ul>
<li>各系列图标合集<a href="https://icones.netlify.app/">icones</a></li>
</ul>
<h2><strong>数学类</strong></h2>
<ul>
<li>数列查找<a href="http://oeis.org/">OEIS</a></li>
</ul>
<h2><strong>其它</strong></h2>
<ul>
<li>
<p>Emoji 查询<a href="https://www.emojiall.com/zh-hans">EmojiAll</a></p>
</li>
<li>
<p>ffmpeg 指令生成<a href="https://alfg.dev/ffmpeg-commander/">ffmpeg-commander</a></p>
</li>
</ul>]]></description>
      <author>xancel</author>
      <guid>article-2</guid>
      <pubDate>Fri, 13 Mar 2026 06:15:03 +0000</pubDate>
    </item>
  </channel>
</rss>