<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<atom:link href="https://skydevs.link/feed" rel="self" type="application/rss+xml"/>
<title>Sora</title>
<link>https://skydevs.link</link>
<description>スカイ</description>
<language>zh-CN</language>
<copyright>© sky </copyright>
<pubDate>Fri, 01 May 2026 00:25:10 GMT</pubDate>
<generator>Mix Space CMS (https://github.com/mx-space)</generator>
<docs>https://mx-space.js.org</docs>
<image>
    <url>https://skydevs.link/api/v2/objects/avatar/vyndeeulyg2x9qtjts.jpg</url>
    <title>Sora</title>
    <link>https://skydevs.link</link>
</image>
<item>
    <title>静默的自习室与飞驰的沥青路</title>
    <link>https://skydevs.link/notes/24</link>
    <pubDate>Thu, 11 Dec 2025 02:56:48 GMT</pubDate>
    <description>闹钟响之前的世界，被浸泡在一种陈旧的暖橙色里，像是一张泛黄的旧考卷。我站在大学教学楼的水磨石走廊上，</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://skydevs.link/notes/24'>https://skydevs.link/notes/24</a></blockquote>
      <p>闹钟响之前的世界，被浸泡在一种陈旧的暖橙色里，像是一张泛黄的旧考卷。我站在大学教学楼的水磨石走廊上，四周安静得能听见尘埃落地的声音。透过门上那个长方形的观察窗，我向内窥探。那是一个普通的自习室，却诡异地折叠进了我人生三个阶段的影子。</p>
<p>她们都在那里。第一排坐着那个小学同桌，她此刻意外地安静，低头看着书，记忆里那个总是变着法子“欺负”我、想引起我注意的顽皮劲儿不见了。而在离她隔着一个空位的地方，坐着一个我认识的男生。他没有看她，她也没有看他，两人只是在同一个频率下自习。但就是这种默契的共存，让我感到一种莫名的刺痛——那个能忍受她“欺负”的安全距离，现在属于别人了。</p>
<p>视线往后，是那个像小鸟一样的高中女生。她整个人几乎缩在书堆里，脊背绷得直直的，浑身散发着那种让人心疼的努力劲儿。而在她旁边，同样隔着一个座位，坐着另一个男生。他像是一尊沉默的雕像，没有递水，没有关怀，只是单纯地“在那里”。这种无声的守护，让我这个只能隔着玻璃观看的人，感到一种被彻底隔绝的酸涩。</p>
<p>最让我心里一紧的是中间那个位置。那个阳光开朗的初中同桌，那个曾经是个“手控”、喜欢把玩我的手、整天喊我“同桌”的女孩。她此刻正托着腮思考题目，侧脸依旧美好得让人心动。而在她身旁隔着一个座位的地方，那个熟悉的男生也在埋头苦读。没有交谈，没有眼神接触，甚至连纸条都没有传。他们之间横亘着那个空座位，像是一道无形的红线。但我依然感到了一阵剧烈的惶恐和醋意。因为那个“隔着一个座位”的空间，代表着一种我无法触及的“日常”。他们共享着同一片空气，同一个自习的午后，而我被关在门外，手里空空荡荡，只有满手的汗水和局促。</p>
<p>就在这种压抑到达顶点时，窗外突然传来一阵荒诞的响动。三楼或四楼的窗外竟然搭起了一架摇摇晃晃的木梯，一个卖板栗的商贩推开窗户，热气腾腾的板栗香气瞬间冲淡了室内的书卷味，他吆喝着“给考生补脑子”，把现实的逻辑撕开了一道口子。</p>
<p>随着一阵恍惚的断裂，橙色褪去，寒意袭来，梦境衔接到了第二层。</p>
<p>场景切换到了那间巨大的阶梯教室。这里比刚才的自习室大得多，也空旷得多，没有了暧昧的遗憾，只剩下冰冷的蓝色回音。我躲在教室的角落，试图用最原始、最私密的方式来安抚刚才受挫的内心。屏幕上的色情画面闪烁，呼吸变得急促，这是一种卑微的自我逃避，我以为这里绝对安全。然而，“砰”的一声，空气仿佛被撕裂了。并不是门被推开，而是一个怪物凭空出现了。它没有具体的五官，像是由扭曲的黑色线条和压力组成的肉块，带着纯粹的物理威胁向我扑来。羞耻感瞬间转化为了求生欲，裤子还没提好，我就在成排的桌椅间狼狈翻滚。我抓起椅子，在那空旷的教室里与这个未知的梦魇肉搏，那是一场没有章法的乱战，也是我第一次试图打破某种压抑的禁锢。</p>
<p>战斗的硝烟还未散尽，画面再次生硬地切断。再睁眼时，风呼啸而过。</p>
<p>我站在宽阔的沥青马路上，身后是几个刚认识的“哥们”。面前是一道如同断崖般的巨大障碍物。“比谁飞得好，”有人提议。那个教练模样的男人先上了，四平八稳，毫无惊喜，就像我一直以来“中上游”的人生，不出错，也不出彩。接着是那个小学生，他叫嚣着“我比教练厉害”，结果在前轮接触障碍的瞬间，因为技术拙劣和盲目自大，重重地摔了下来，停在原地满脸通红。</p>
<p>最后，轮到了他。那个和我年纪相仿的男生，高中生或者大学生的模样。他骑着车，侧脸冷峻，眼神里有一种我从未拥有的专注。他没有看我，也没有看任何人。他加速了，不是蛮力，而是一种对节奏的完美掌控。起跳。自行车在空中划出了一道令人窒息的弧线。他飞得比谁都高，动作轻盈得像一只掠过水面的燕子。没有犹豫，没有恐惧，也没有那些拖泥带水的儿女情长。</p>
<p>“咔哒”一声，落地轻盈，轮胎稳稳咬住地面。他没有回头庆祝，只是背对着我，继续向远方骑去。那一刻，我站在路边，看着那个背影，心中涌起一股巨大的惊叹与渴望。我知道，那个沉默不语、技术顶尖的身影，正是那个不再受困于过去、不再被怪物追赶的，理想中的我自己。</p>

      <p style='text-align: right'>
      <a href='https://skydevs.link/notes/24#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">693a32f06663146074b3c359</guid>
  <category>notes</category>
false
 </item>
  <item>
    <title>从 10Mbps 到 92Mbps：记一次被物理层和特殊字符“Gank”的家庭网络优化</title>
    <link>https://skydevs.link/posts/tech/debug-wifi-from-10mbps-to-92mbps</link>
    <pubDate>Thu, 11 Dec 2025 02:53:57 GMT</pubDate>
    <description>前言

最近在家办公（摸鱼）时，发现了一个极其诡异的现象：明明家里是百兆光纤，路由器就在离我房间 7</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://skydevs.link/posts/tech/debug-wifi-from-10mbps-to-92mbps'>https://skydevs.link/posts/tech/debug-wifi-from-10mbps-to-92mbps</a></blockquote>
      <h2>前言</h2>
<p>最近在家办公（摸鱼）时，发现了一个极其诡异的现象：明明家里是百兆光纤，路由器就在离我房间 <strong>7米</strong> 远的地方，中间甚至没有实体墙（只有一个书柜），但我的网速却只有 <strong>10Mbps</strong>，有时候甚至还会断连。</p>
<p>起初我以为是运营商的锅，但作为一个 Developer，遇到问题第一反应当然是先 Debug。经过一番排查，发现这不仅是物理层的问题，还意外踩了一个关于“字符编码”的坑。</p>
<h2>排查物理层：光猫是不是“寄”了？</h2>
<p>本着“控制变量法”的原则，我先检查了源头——那台 ZTE 的光猫。</p>
<p>观察指示灯状态：</p>
<ul>
<li><p><code>光信号</code>：熄灭（正常，红灯闪烁才是光路断了）</p>
</li>
<li><p><code>注册</code>：常亮绿灯（正常，代表已连接 ISP）</p>
</li>
<li><p><code>网口1</code>：常亮（正常，路由器的链路是通的）</p>
</li>
</ul>
<p>看起来光猫本身情绪稳定，并没有摆烂。既然进水口没问题，那瓶颈肯定在 <strong>路由器 -&gt; 终端</strong> 这“最后7米”的无线传输上。</p>
<h2>协议的瓶颈：Wi-Fi 4 的“幽灵”</h2>
<p>回到房间，我打开手机查看 Wi-Fi 的详细连接信息，看到这一行时，我大概明白为什么会卡了：</p>
<ul>
<li><p><strong>连接速度：</strong> <code>144Mbps</code></p>
</li>
<li><p><strong>技术标准：</strong> <code>Wi-Fi 4 (802.11n)</code></p>
</li>
</ul>
<p><code>144Mbps</code> 这个数值非常眼熟，它是 2.4GHz 频段在 20MHz 频宽下的理论协商速率上限。</p>
<p>众所周知，<strong>Wi-Fi 是半双工通信</strong>，实际吞吐量通常只有协商速率的 50%-60%。算一下：<code>144 * 0.5 = 72Mbps</code>。这也解释了为什么我在路由器旁边测速能到 70Mbps，但一旦回到房间，经过 7 米距离和书柜的物理衰减，加上 2.4G 频段那感人的干扰，速度直接跌到 <strong>10Mbps</strong> 也就不足为奇了。</p>
<p><strong>破局思路：</strong> 必须强制切换到 <strong>5GHz 频段 (Wi-Fi 5/6)</strong>。虽然 5G 穿墙弱，但在没有死角的情况下，它的高吞吐量才是王道。</p>
<h2>踩坑：SSID 里的“特殊字符”</h2>
<p>这台 TP-LINK 路由器默认开启了“多频合一”，导致设备总是优先连信号更强但速度更慢的 2.4G。于是我进入后台关闭了该功能，手动把 5G 频段分了出来。</p>
<p>为了彰显个性（二刺猿属性爆发），我顺手把 5G 的 Wi-Fi 名字改成了：</p>
<blockquote>
<p><code>Ciallo_～(∠・ω&lt; )_5G</code></p>
</blockquote>
<p>结果灾难发生了：</p>
<p>手机能搜到这个 5G 信号，但点击连接后，一直在转圈显示“正在连接...”，既不报错，也不提示密码错误，最后自动回落到 2.4G。</p>
<p>排查了一圈，最后才反应过来——<strong>这可能是字符编码的问题</strong>。</p>
<p>路由器的底层 OS（大概率是魔改 Linux 或 VxWorks）和手机终端在处理 <code>≧</code>、<code>∇</code> 这种特殊数学符号时，编码可能不一致（比如 UTF-8 和 GBK 的冲突），导致在 WPA2 握手阶段密钥协商失败，从而陷入死循环。</p>
<p>这就好比我给前端传了个 JSON，结果 Key 里带了特殊字符导致解析炸了一样 (￣_￣|||)</p>
<h2>最终解决 &amp; 性能起飞</h2>
<p>找到了 Root Cause，解决起来就很快了。遵循 <strong>KISS (Keep It Simple, Stupid)</strong> 原则，我含泪舍弃了颜文字，把 SSID 改回了朴实无华的纯英文，密码也暂时改成了纯数字。</p>
<p><strong>重启路由器后：</strong></p>
<ol>
<li><p>手机秒连 5G。</p>
</li>
<li><p>回到房间再次测速。</p>
</li>
</ol>
<p><strong>Result:</strong></p>
<ul>
<li><p><strong>Before (2.4G):</strong> 10 Mbps</p>
</li>
<li><p><strong>After (5G):</strong> <strong>70 Mbps</strong></p>
</li>
</ul>
<p>而在路由器旁边测速，更是直接飙到了 <strong>92 Mbps</strong>。</p>
<p>这时候可能有人会问：“为什么不是 100Mbps？”</p>
<p>其实这是正常的。百兆宽带的物理接口加上 TCP/IP 协议头的开销，92-94Mbps 基本上就是物理极限了。这就好比买个 128G 的手机，实际可用容量肯定不到 128G 一样。</p>
<p>另外还发现上传速度是 25Mbps，下载是 80Mbps+，这也是符合家庭宽带“非对称带宽”配置的（通常上行是下行的 1/5 到 1/4）。</p>
<h2>结尾</h2>
<p>总的来说，这次折腾的教训就是：</p>
<ol>
<li><p>能用 5G 频段就别用 2.4G，哪怕隔着书柜，5G 的速度优势也足以抵消衰减。</p>
</li>
<li><p><strong>千万别在 SSID 里加奇怪的符号！</strong> 尤其是数学符号和 Emoji，这种兼容性地雷踩一次就够了。</p>
</li>
</ol>
<p>看着 SpeedTest 跑满的曲线，今晚终于可以流畅打游戏了 (●&#39;◡&#39;●)。</p>

      <p style='text-align: right'>
      <a href='https://skydevs.link/posts/tech/debug-wifi-from-10mbps-to-92mbps#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">693a32456663146074b3c302</guid>
  <category>posts</category>
<category>技术</category>
 </item>
  <item>
    <title>甘织遥奈的故事其3</title>
    <link>https://skydevs.link/notes/23</link>
    <pubDate>Wed, 27 Aug 2025 14:47:43 GMT</pubDate>
    <description>没有人知道她会漂流到哪个海岸，但她永远都是甘织玲奈子。

是遥奈曾经最喜欢的甘织玲奈子。

压低声音</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://skydevs.link/notes/23'>https://skydevs.link/notes/23</a></blockquote>
      <p>没有人知道她会漂流到哪个海岸，但她永远都是甘织玲奈子。</p>
<p>是遥奈曾经最喜欢的甘织玲奈子。</p>
<p>压低声音流泪的遥奈发自心底祈祷。</p>
<p>愿世上的一切都能顺自己的心意。</p>
<p>愿今后姐姐的人生再也没有困难，能够一帆风顺。</p>
<p>愿她能得到世上所有人的喜爱。</p>
<p>愿她能在高中成功改头换面，交到好多好多朋友。</p>
<p>愿她能在哪天交到恋人，让每天充满幸福的日子永远持续下去。</p>
<p>因为这是为了自己的幸福。</p>
<p>因为姐姐的幸福就是自己的幸福。</p>
<p>自幼便相互影响，仿佛镜子的表里。这就是姐妹。</p>
<p>为了自己，遥奈希望她能幸福。</p>
<p>因为姐姐就是自己的另一半啊。</p>
<hr>
<p>口瓜！！！我要看的就是这个啊！！！みかみてれん老师！！！遥奈的对玲奈子的情感，妹妹对姐姐的情感，在这一刻终于爆发了。遥奈这一次终于看清了自己对姐姐是怎么想的，姐姐依旧是那个姐姐，她所憧憬的，依赖的姐姐啊！</p>
<p>这一次，轮到遥奈来保护姐姐了。</p>
<p>因为姐姐就是自己的另一半啊。</p>
<p></p>
<p></p>
<p></p>

      <p style='text-align: right'>
      <a href='https://skydevs.link/notes/23#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">68af1a8f6663146074ab233f</guid>
  <category>notes</category>
false
 </item>
  <item>
    <title>BeanFactoryPostProcessor的依赖注入限制</title>
    <link>https://skydevs.link/posts/tech/resolving-autowired-null-in-beanfactorypostprocessor</link>
    <pubDate>Wed, 27 Aug 2025 03:16:15 GMT</pubDate>
    <description>首先，我需要感谢wenzhuo4657给我的项目提的pr：

https://github.com/</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://skydevs.link/posts/tech/resolving-autowired-null-in-beanfactorypostprocessor'>https://skydevs.link/posts/tech/resolving-autowired-null-in-beanfactorypostprocessor</a></blockquote>
      <p>首先，我需要感谢<a href="https://github.com/wenzhuo4657">wenzhuo4657</a>给我的项目提的pr：</p>
<p><a href="https://github.com/SkyDependence/tgDrive/pull/57">https://github.com/SkyDependence/tgDrive/pull/57</a></p>
<p>其中数据库文件的生成确实是我自己考虑的不够周到，没有意识到flyway有可能会在数据库文件生成之前进行数据库的操作。</p>
<p>我在这条pr里，也学到了一些东西。</p>
<p>其一，是<code>BeanFactoryPostProcessor</code>，pr里<code>DatabaseConfig</code>实现了<code>BeanFactoryPostProcessor</code>接口，这样，<code>DatabaseConfig</code>就能在所有Bean初始化之前执行了，能够避免flyway过早操作数据库。</p>
<p>其二，就是依赖注入。</p>
<p><a href="https://github.com/wenzhuo4657">wenzhuo4657</a>还为<code>DatabaseConfig</code>实现了一个<code>EnvironmentAware</code>接口，我看了看逻辑，是为了获取到<code>spring.datasource.url</code>这个属性的值的，我之前的实现是直接用<code>@Value</code>直接依赖注入，没有接触过用<code>environment.getProperty</code>来获取属性值的，所以一开始，我认为这是多此一举，想要提一个修改请求，但是转念一想，应该不至于特地实现这么一个接口而不用<code>@Value</code>吧？抱着怀疑的态度，我去问了问Gemini，果然，实现这个接口是有道理的，不如说，在<code>BeanFactoryPostProcessor</code>里要获取属性值，就必须实现<code>EnvironmentAware</code>接口。</p>
<p>先说如果用<code>@Value</code>会怎么样：</p>
<pre><code class="language-java">@Value("${spring.datasource.url}")  
private String databaseUrl;

@Override  
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {  
    // 在所有Bean创建之前执行，确保在Flyway初始化之前创建数据库目录  
    if (databaseUrl != null) {  
        // 从数据库URL中提取路径  
        String dbPath = databaseUrl.replace("jdbc:sqlite:", "");  
        File dbFile = new File(dbPath);  
        File parentDir = dbFile.getParentFile();  
  
        if (parentDir != null && !parentDir.exists()) {  
            boolean created = parentDir.mkdirs();  
            if (created) {  
                log.info("数据库父目录创建成功: {}", parentDir.getAbsolutePath());  
            } else {  
                log.error("数据库父目录创建失败: {}", parentDir.getAbsolutePath());  
            }  
        } else if (parentDir != null) {  
            log.info("数据库父目录已存在: {}", parentDir.getAbsolutePath());  
        }  
    }else{  
        throw new RuntimeException("数据库URL未配置");  
    }  
}</code></pre><p>这时，启动程序，控制台会报错：</p>
<pre><code class="language-bash">2025-08-27 10:55:21.008 [restartedMain] WARN  o.s.b.w.s.c.AnnotationConfigServletWebServerApplicationContext - Exception encountered during context initialization - cancelling refresh attempt: java.lang.RuntimeException: 数据库URL未配置</code></pre><p>这是因为，<code>BeanFactoryPostProcessor</code>是在一切Bean创建之前执行的，也就是说，<code>@Value</code>的处理器也还没有开始工作，导致databaseUrl的值为空。</p>
<p>如果用上了<code>EnvironmentAware</code>呢？</p>
<pre><code class="language-java">private Environment environment;  
  
@Override  
public void setEnvironment(Environment environment) {  
    this.environment = environment;  
}  
  
@Override  
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {  
    // 在所有Bean创建之前执行，确保在Flyway初始化之前创建数据库目录  
    String databaseUrl = environment.getProperty("spring.datasource.url");  
    if (databaseUrl != null) {  
        // 从数据库URL中提取路径  
        String dbPath = databaseUrl.replace("jdbc:sqlite:", "");  
        File dbFile = new File(dbPath);  
        File parentDir = dbFile.getParentFile();  
  
        if (parentDir != null && !parentDir.exists()) {  
            boolean created = parentDir.mkdirs();  
            if (created) {  
                log.info("数据库父目录创建成功: {}", parentDir.getAbsolutePath());  
            } else {  
                log.error("数据库父目录创建失败: {}", parentDir.getAbsolutePath());  
            }  
        } else if (parentDir != null) {  
            log.info("数据库父目录已存在: {}", parentDir.getAbsolutePath());  
        }  
    }else{  
        throw new RuntimeException("数据库URL未配置");  
    }  
}</code></pre><p>这时，程序就能正常启动了，这是因为，<code>EnvironmentAware</code>是一个更底层的“特权”接口，实现它，就相当于有了<code>Environment</code>，而<code>Environment</code>对象是Spring容器最早准备好的东西之一，它包含了所有的配置信息，包括<code>spring.datasource.url</code>。这样，使用<code>environment.getProperty(&quot;spring.datasource.url&quot;)</code>就能获取到<code>spring.datasource.url</code>的值了。</p>
<p>同样的，在实现了<code>BeanFactoryPostProcessor</code>的类里，<code>@Autowired</code>也会失效，因为这时还没到Bean的实例化阶段。</p>
<p>不过，我在想：如果非要另一个类，另一个Bean，怎么办？</p>
<p>Gemini给出了两个方法，第一个，使用<code>beanFactory</code>对象：</p>
<pre><code class="language-java">@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    // 这是一个非常危险且不推荐的操作喵！
    MyNeededService myService = beanFactory.getBean(MyNeededService.class); 
    myService.doSomething();
}</code></pre><p>不过这个操作很危险，因为会打乱这个Bean的生命周期，也会导致它的AOP失效（早产儿说是）。</p>
<p>第二个，那就是不用Bean，不把这个类交给Spring容器统一管理，只需要在实现了<code>BeanFactoryPostProcessor</code>的Bean里创建这个类的对象，直接使用它就行了。</p>

      <p style='text-align: right'>
      <a href='https://skydevs.link/posts/tech/resolving-autowired-null-in-beanfactorypostprocessor#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">68ae787f6663146074ab1605</guid>
  <category>posts</category>
<category>技术</category>
 </item>
  <item>
    <title>环境变量中的“狸猫换太子”：解决因Java路径冲突引发的神秘崩溃事件</title>
    <link>https://skydevs.link/posts/tech/the-case-of-the-rogue-java-path</link>
    <pubDate>Wed, 27 Aug 2025 01:57:37 GMT</pubDate>
    <description>当我在命令行输入java -jar app.jar，像往常一样敲击回车后，一个我从来没见过的错误出现</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://skydevs.link/posts/tech/the-case-of-the-rogue-java-path'>https://skydevs.link/posts/tech/the-case-of-the-rogue-java-path</a></blockquote>
      <p>当我在命令行输入<code>java -jar app.jar</code>，像往常一样敲击回车后，一个我从来没见过的错误出现了：</p>
<p></p>
<p></p>
<p>我懵了，这是啥？？？</p>
<p>以前Java都用的好好的，怎么现在会有这个问题？</p>
<p>问了Gimini，她和我说是Windows的底层的错误，推荐我修改注册表把<code>HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug</code>和<code>HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug</code>里右边的<code>Debugger</code>这一项删除，或者如果有Visual Studio的话，工具(Tools)-&gt;选项(Options)-&gt;调试(Debugging)-&gt;实时(Just-In-Time)，把实时(Just-In-Time)里面能选的勾全部去掉。</p>
<p>但是她又提醒我说这是治标不治本的，她建议我去找<code>hs_err_pid&lt;数字&gt;.log</code>的日志文件，但是，我没找到这个日志文件，于是，我直接Google，依旧没找到解决办法。就在万策尽时，我回想了我之前对Java的所作所为，之前因为项目需要使用Java8，而我的电脑上的Java版本是17，所以在环境变量上做了修改，于是乎，我先去原来的Java的文件夹下运行了命令<code>java --version</code>，果然能正常显示版本号，我又去Java8的文件夹下运行命令<code>java --version</code>，也能正常显示版本号，这就奇怪了，不是他们俩的问题，那就只有一个问题了：<strong>环境变量</strong></p>
<p>于是我就去查看环境变量。这不查不知道，一查吓一跳：</p>
<p></p>
<p>这多出来的<code>Oracle\Java\java8path</code>和<code>Oracle\Java\javapath</code>是啥？？？</p>
<p></p>
<p>点进去看，这显然不是我存放Java的文件夹，甚至里面的文件也奇怪的很：</p>
<p></p>
<p>我在这个文件夹下运行<code>java --version</code>，果然出现了相同的报错。</p>
<p>将环境变量那上面两行去掉后，问题如愿解决了。</p>
<p>拿着我的解决方案再去问Gemini，这是什么原因，Gemini答：32位和64位不兼容。</p>
<p>依据我的经验，之前安装在我电脑上的32位的Java能正常运行，不过就是性能差了点，这个原因肯定是错误的，于是我再追问，Gemini答：这个<code>java.exe</code>本身就是损坏的。</p>
<p>感觉她说得对，毕竟这个文件夹里就这么三个文件，怎么想都不对吧。</p>
<p>不过，我没有关于这个Oracle文件夹的任何印象，这就很恐怖了。</p>

      <p style='text-align: right'>
      <a href='https://skydevs.link/posts/tech/the-case-of-the-rogue-java-path#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">68ae66116663146074ab14f4</guid>
  <category>posts</category>
<category>技术</category>
 </item>
  <item>
    <title>深入解析MySQL InnoDB存储引擎的ACID实现原理</title>
    <link>https://skydevs.link/posts/study/mysql-innodb-acid-implementation-deep-dive</link>
    <pubDate>Thu, 21 Aug 2025 07:45:29 GMT</pubDate>
    <description>ACID（原子性、一致性、隔离性、持久性）是关系型数据库管理系统（RDBMS）的基石，它保证了事务的</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://skydevs.link/posts/study/mysql-innodb-acid-implementation-deep-dive'>https://skydevs.link/posts/study/mysql-innodb-acid-implementation-deep-dive</a></blockquote>
      <p>ACID（原子性、一致性、隔离性、持久性）是关系型数据库管理系统（RDBMS）的基石，它保证了事务的可靠性和数据的完整性。MySQL的InnoDB存储引擎通过一系列精巧的机制，完整地实现了这四大特性。本文将结合一些关键问题，深入剖析InnoDB是如何通过Undo Log、Redo Log、锁机制、MVCC以及其他辅助结构来实现ACID的。</p>
<h4>一、一致性 (Consistency)</h4>
<blockquote>
<p><strong>问：一致性是如何保证的？</strong></p>
</blockquote>
<p>一致性是ACID模型的最终目标，它要求事务必须使数据库从一个一致性状态转变到另一个一致性状态。这意味着事务执行的结果必须满足数据库的所有预设约束，如数据类型、非空约束、唯一约束、外键约束以及自定义的业务规则。</p>
<p>一致性并非由单一技术实现，而是由数据库的完整性约束和原子性、隔离性、持久性三大特性共同保障的：</p>
<ul>
<li><p><strong>数据库约束</strong>：是保证一致性的第一道防线，任何违反预定义约束（如CHECK、FOREIGN KEY等）的操作都会导致事务失败。</p>
</li>
<li><p><strong>原子性</strong>：保证事务中途失败后，数据能回滚到初始状态，从而不会留下破坏一致性的中间数据。</p>
</li>
<li><p><strong>隔离性</strong>：防止并发事务的互相干扰，避免因数据交错读写导致的数据不一致。</p>
</li>
<li><p><strong>持久性</strong>：确保已提交事务的最终一致性状态被永久保存。</p>
</li>
</ul>
<h4>二、原子性 (Atomicity)</h4>
<blockquote>
<p><strong>问：MySQL里的原子性是怎么实现的？如果commit没有成功呢？</strong></p>
</blockquote>
<p>原子性要求一个事务内的所有操作，要么全部成功执行，要么全部失败回滚，不能停留在中间状态。</p>
<p>InnoDB通过 <strong>Undo Log（回滚日志）</strong> 来实现原子性。</p>
<ul>
<li><p><strong>工作原理</strong>：当事务对数据进行修改时，Undo Log会记录下与实际操作相反的逻辑操作。</p>
<ul>
<li><p>对于INSERT操作，Undo Log记录对应的DELETE操作。</p>
</li>
<li><p>对于DELETE操作，Undo Log记录对应的INSERT操作。</p>
</li>
<li><p>对于UPDATE操作，Undo Log记录下修改前的数据（旧版本）。</p>
</li>
</ul>
</li>
<li><p><strong>实现方式</strong>：</p>
<ul>
<li><p><strong>事务回滚</strong>：当事务执行过程中发生错误、用户显式执行ROLLBACK命令，或者<strong>事务提交失败</strong>（例如，因磁盘I/O错误导致Redo Log无法写入），系统会利用Undo Log中的记录执行反向操作，将数据恢复至事务开始前的状态，从而确保原子性。</p>
</li>
<li><p><strong>崩溃恢复</strong>：如果数据库在事务提交前崩溃，重启后会检查未完成的事务，并利用Undo Log对其进行回滚。</p>
</li>
</ul>
</li>
</ul>
<h4>三、持久性 (Durability)</h4>
<blockquote>
<p><strong>问：持久性是怎么实现的？如果commit了，Redo log没来得及记录操作，怎么办？</strong></p>
</blockquote>
<p>持久性保证一旦事务被提交，其对数据库的修改就是永久性的，即使随后系统发生崩溃也不会丢失。</p>
<p>InnoDB主要通过 <strong>Redo Log（重做日志）</strong> 和 <strong>预写式日志（Write-Ahead Logging, WAL）</strong> 策略来实现持久性。</p>
<ul>
<li><p><strong>工作原理</strong>：WAL策略要求在数据页（Data Page）写入磁盘之前，必须先将该修改操作对应的Redo Log写入磁盘。Redo Log通常是顺序写入的，其I/O效率远高于数据页的随机写入。</p>
</li>
<li><p><strong>COMMIT过程</strong>：关于“commit后Redo Log未记录”的担忧，在InnoDB的默认机制下不会发生。<strong>事务提交（COMMIT）成功的标志，正是其Redo Log被成功刷新到磁盘</strong>。流程如下：</p>
<ol>
<li><p>事务执行修改，同时在内存的Redo Log Buffer中记录日志。</p>
</li>
<li><p>用户发起COMMIT。</p>
</li>
<li><p>系统将Redo Log Buffer中的相关日志刷新到磁盘上的Redo Log File。</p>
</li>
<li><p><strong>只有当磁盘确认写入成功后</strong>，系统才会向客户端返回“提交成功”的响应。<br> 因此，不存在提交成功而Redo Log未记录的情况。</p>
</li>
</ol>
</li>
<li><p><strong>崩溃恢复</strong>：数据库重启时，会检查Redo Log，并将其中已提交但尚未写入数据文件的事务操作进行重放，从而恢复数据至崩溃前的最新状态。</p>
</li>
</ul>
<h4>四、隔离性 (Isolation) 与 MVCC</h4>
<blockquote>
<p><strong>问：Undo Log在MVCC中扮演了什么样的关键角色？</strong></p>
</blockquote>
<p>隔离性要求并发执行的事务之间互不干扰。InnoDB通过 <strong>锁机制</strong> 和 <strong>多版本并发控制（Multi-Version Concurrency Control, MVCC）</strong> 来实现隔离性。</p>
<p>MVCC是InnoDB在“可重复读”（Repeatable Read）和“读已提交”（Read Committed）隔离级别下实现高并发读写性能的核心。其精髓在于，让读写操作互不阻塞。而<strong>Undo Log正是构建MVCC机制的基石</strong>。</p>
<ul>
<li><p><strong>Undo Log作为数据历史版本链</strong>：</p>
<ul>
<li><p>InnoDB的聚簇索引记录中，除了用户定义的列，还有两个关键的隐藏列：DB_TRX_ID（记录最近一次修改该行的事务ID）和DB_ROLL_PTR（一个指向该行上一个版本Undo Log记录的指针，称为回滚指针）。</p>
</li>
<li><p>当一个事务修改某行数据时，它会把该行数据的<strong>旧版本</strong>制成一条Undo Log记录，并通过DB_ROLL_PTR将新旧版本链接起来，形成一个<strong>版本链</strong>。</p>
</li>
<li><p>这个由Undo Log串联起来的版本链，实质上存储了该行数据的多个历史快照。</p>
</li>
</ul>
</li>
<li><p><strong>ReadView与可见性判断</strong>：</p>
<ul>
<li><p>当一个读事务（SELECT）开始时，它会创建一个名为ReadView（读视图）的快照。这个ReadView记录了当前数据库中所有活跃（未提交）的事务ID列表。</p>
</li>
<li><p>当该读事务需要访问某行数据时，它会沿着该行的Undo Log版本链，从最新版本开始逐个检查。</p>
</li>
<li><p>它会用ReadView来判断每个版本对当前事务是否“可见”。判断规则大致为：如果一个版本的DB_TRX_ID不在ReadView的活跃事务列表中，并且小于ReadView的最小活跃事务ID，那么这个版本就是可见的。</p>
</li>
<li><p>事务会读取它能看到的**第一个（即最新的）**符合条件的版本。</p>
</li>
</ul>
</li>
<li><p><strong>总结</strong>：Undo Log在MVCC中的角色，已经从单纯的“用于回滚的数据备份”升华为**“构建数据多版本快照的核心组件”**。它使得不同的事务可以根据自己的ReadView，在版本链上找到属于自己的、一致的数据版本，从而实现了在不加锁的情况下读取数据，即“非阻塞读”，极大地提升了数据库的并发性能。</p>
</li>
</ul>
<h4>五、核心辅助机制解析</h4>
<p><strong>1. 刷脏页（Flushing Dirty Pages）</strong></p>
<blockquote>
<p><strong>问：都已经有Redo Log了，为什么还需要刷脏页？</strong></p>
</blockquote>
<ul>
<li><p><strong>定义</strong>：“脏页”是指在内存（Buffer Pool）中被修改，但尚未同步到磁盘数据文件的数据页。将这些脏页写回磁盘的过程，即为“刷脏页”。</p>
</li>
<li><p><strong>必要性</strong>：</p>
<ul>
<li><p><strong>缩短恢复时间</strong>：定期刷脏页可以推进检查点（Checkpoint），减少崩溃恢复时需要重放的Redo Log数量。否则，恢复时间可能过长而无法接受。</p>
</li>
<li><p><strong>释放Redo Log空间</strong>：Redo Log文件大小有限且循环使用。在覆盖旧日志前，必须确保其对应的脏页已被刷盘，否则会丢失数据。</p>
</li>
<li><p><strong>释放Buffer Pool空间</strong>：当Buffer Pool空间不足时，需要将部分脏页刷盘以腾出空间加载新的数据页。</p>
</li>
</ul>
</li>
</ul>
<p><strong>2. 双写缓冲区（Doublewrite Buffer）</strong></p>
<blockquote>
<p><strong>问：双写缓冲区？为什么这么叫？</strong></p>
</blockquote>
<ul>
<li><p><strong>问题背景</strong>：当InnoDB将一个脏页（如16KB）写入磁盘时，若发生系统崩溃，可能导致“页撕裂”（Torn Page），即数据页只写入了一部分。这种物理损坏是Redo Log无法修复的。</p>
</li>
<li><p><strong>解决方案</strong>：“双写”的命名源于其工作流程，即<strong>一次数据页的写入被分解为两次独立的物理写入</strong>：</p>
<ol>
<li><p><strong>第一次写</strong>：将脏页的完整内容先顺序写入到Doublewrite Buffer中。</p>
</li>
<li><p><strong>第二次写</strong>：确认第一次写成功后，再将脏页内容写入到它在数据文件中的实际位置。</p>
</li>
</ol>
</li>
<li><p><strong>恢复机制</strong>：如果在第二次写的过程中发生页撕裂，数据库恢复时可以从Doublewrite Buffer中找到该页的完整副本进行修复，然后再应用Redo Log。这是一种针对物理写入失败的数据冗余保护机制。</p>
</li>
</ul>

      <p style='text-align: right'>
      <a href='https://skydevs.link/posts/study/mysql-innodb-acid-implementation-deep-dive#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">68a6ce996663146074aa6ddf</guid>
  <category>posts</category>
<category>学习/踩坑</category>
 </item>
  <item>
    <title>数据库并发控制核心知识笔记</title>
    <link>https://skydevs.link/posts/study/mysql-innodb-locks-mvcc-phantom-reads-explained</link>
    <pubDate>Thu, 21 Aug 2025 06:43:19 GMT</pubDate>
    <description>一、 事务并发带来的问题

当多个事务同时访问和修改共享数据时，如果没有有效的控制机制，可能会引发以</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://skydevs.link/posts/study/mysql-innodb-locks-mvcc-phantom-reads-explained'>https://skydevs.link/posts/study/mysql-innodb-locks-mvcc-phantom-reads-explained</a></blockquote>
      <h3>一、 事务并发带来的问题</h3>
<p>当多个事务同时访问和修改共享数据时，如果没有有效的控制机制，可能会引发以下几种数据不一致的问题：</p>
<ol>
<li><p><strong>脏读 (Dirty Read)</strong>:</p>
<ul>
<li><p><strong>定义</strong>: 一个事务读取到了另一个事务<strong>尚未提交</strong>的数据。</p>
</li>
<li><p><strong>风险</strong>: 如果另一个事务最终回滚（Rollback），那么读取到的数据就是无效的“脏”数据。</p>
</li>
</ul>
</li>
<li><p><strong>不可重复读 (Non-Repeatable Read)</strong>:</p>
<ul>
<li><p><strong>定义</strong>: 在同一个事务内，两次读取<strong>同一行</strong>数据，得到的结果却不一致。</p>
</li>
<li><p><strong>核心</strong>: 关注的是<strong>单行数据</strong>的 UPDATE 或 DELETE 操作。</p>
</li>
<li><p><strong>示例</strong>: 事务A第一次读取某行数据，事务B修改了该行并提交，事务A第二次读取时，发现数据内容已改变。</p>
</li>
</ul>
</li>
<li><p><strong>幻读 (Phantom Read)</strong>:</p>
<ul>
<li><p><strong>定义</strong>: 在同一个事务内，两次执行<strong>同一个范围查询</strong>，第二次查询的结果集包含了第一次查询中未出现的“幻影”行。</p>
</li>
<li><p><strong>核心</strong>: 关注的是某个<strong>数据范围</strong>的 INSERT 操作。</p>
</li>
<li><p><strong>示例</strong>: 事务A第一次查询某个范围的数据，事务B在该范围内插入了新数据并提交，事务A第二次查询时，发现多出了新的数据行。</p>
</li>
</ul>
</li>
</ol>
<h3>二、 解决方案：事务隔离级别与MVCC</h3>
<p>为了解决上述问题，SQL标准定义了四种事务隔离级别。MySQL的InnoDB存储引擎通过<strong>多版本并发控制（MVCC）<strong>和</strong>锁机制</strong>的结合来实现这些级别。</p>
<ul>
<li><p><strong>MVCC (Multi-Version Concurrency Control)</strong>: 是实现“读不加锁”的核心机制。它为每一行数据都维护了多个版本（通过隐藏列trx_id和roll_pointer）。当一个事务开始时，它会获得一个“快照”（Read View），在事务期间，普通的SELECT（快照读）只会看到在该快照创建之前就已经提交的数据版本。</p>
<ul>
<li><strong>主要作用</strong>: 解决【<strong>不可重复读</strong>】问题。</li>
</ul>
</li>
</ul>
<h3>三、 锁的核心机制</h3>
<p>锁是保证数据一致性的强制手段，主要分为<strong>共享锁</strong>和<strong>排他锁</strong>两大类。</p>
<ol>
<li><p><strong>共享锁 (Share Lock / S Lock)</strong>:</p>
<ul>
<li><p><strong>别称</strong>: 读锁。</p>
</li>
<li><p><strong>特性</strong>: 多个事务可以同时对同一数据持有共享锁。共享锁之间不互斥。</p>
</li>
<li><p><strong>作用</strong>: 允许并发读取，但<strong>阻止</strong>任何事务获取排他锁（即阻止写操作）。</p>
</li>
<li><p><strong>触发</strong>: SELECT ... LOCK IN SHARE MODE;</p>
</li>
</ul>
</li>
<li><p><strong>排他锁 (Exclusive Lock / X Lock)</strong>:</p>
<ul>
<li><p><strong>别称</strong>: 写锁。</p>
</li>
<li><p><strong>特性</strong>: 在任何时候，一份数据上只能有一个排他锁。排他锁与任何其他锁（包括共享锁）都互斥。</p>
</li>
<li><p><strong>作用</strong>: 在数据被修改时，<strong>阻止</strong>任何其他事务的读或写操作，保证数据修改的原子性。</p>
</li>
<li><p><strong>触发</strong>: UPDATE, DELETE, INSERT 会自动施加。也可手动触发：SELECT ... FOR UPDATE;</p>
</li>
</ul>
</li>
</ol>
<h3>四、 InnoDB中的锁粒度与幻读的终极解决方案</h3>
<p>InnoDB的锁不仅仅是锁住一行数据那么简单，它有更精细的粒度来解决幻读问题。</p>
<ol>
<li><p><strong>行锁 (Record Lock)</strong>:</p>
<ul>
<li><p><strong>锁定目标</strong>: <strong>单个已存在的索引记录</strong>。</p>
</li>
<li><p><strong>作用</strong>: 锁定一个“实体”，防止 UPDATE 和 DELETE。这是共享锁和排他锁的基本表现形式。</p>
</li>
</ul>
</li>
<li><p><strong>间隙锁 (Gap Lock)</strong>:</p>
<ul>
<li><p><strong>锁定目标</strong>: <strong>索引记录之间的逻辑空位（开区间）</strong>。</p>
</li>
<li><p><strong>作用</strong>: 锁定一片“虚空”，<strong>防止其他事务在此范围内 INSERT 新数据</strong>。这是解决【<strong>幻读</strong>】问题的关键。</p>
</li>
<li><p><strong>特性</strong>: 间隙锁之间是兼容的，即多个事务可以同时在同一个间隙上持有间隙锁。</p>
</li>
</ul>
</li>
<li><p><strong>临键锁 (Next-Key Lock)</strong>:</p>
<ul>
<li><p><strong>定义</strong>: <strong>行锁 + 该行记录之前的间隙锁</strong>的组合。</p>
</li>
<li><p><strong>锁定目标</strong>: 一个已存在的索引记录及其前面的逻辑空位（左开右闭区间）。</p>
</li>
<li><p><strong>作用</strong>: 这是InnoDB在**可重复读（Repeatable Read）**隔离级别下，执行范围查询时的默认锁策略，它通过同时锁定实体和间隙，完美地解决了【<strong>幻读</strong>】问题。</p>
</li>
</ul>
</li>
</ol>
<h3>五、 核心疑问解答总结</h3>
<ol>
<li><p><strong>为什么INSERT会加排他锁？</strong></p>
<ul>
<li>它锁的不是尚未存在的数据，而是即将在<strong>索引</strong>中占据的那个<strong>位置</strong>。这本质上是在该索引记录上施加了一个排他的行锁，以保证主键或唯一索引的唯一性，防止并发插入冲突。</li>
</ul>
</li>
<li><p><strong>数据库中为什么会存在“间隙”？</strong></p>
<ul>
<li>数据库中的行在物理上并非连续存储。我们所见的顺序是由<strong>索引</strong>提供的<strong>逻辑顺序</strong>。“间隙”是存在于<strong>索引</strong>中的、逻辑上的<strong>空位</strong>。这种设计是为了极大地提高 INSERT 和 DELETE 操作的性能，避免大规模数据迁移。</li>
</ul>
</li>
<li><p><strong>为什么共享锁无法防止幻读？</strong></p>
<ul>
<li>因为共享锁（及其底层的行锁）只能锁定<strong>已经存在</strong>的数据行。而幻读是由<strong>新插入</strong>的数据行引起的。用一把只能锁住“实体”的锁，无法阻止在“虚空”（间隙）中诞生新的实体。只有<strong>间隙锁</strong>才能完成这个任务。</li>
</ul>
</li>
<li><p><strong>SELECT COUNT(*)会加锁吗？</strong></p>
<ul>
<li><p>在<strong>可重复读</strong>隔离级别下，普通的SELECT COUNT(*)执行的是<strong>快照读（MVCC）</strong>，<strong>不加锁</strong>。</p>
</li>
<li><p>只有当明确使用LOCK IN SHARE MODE或FOR UPDATE时，才会升级为<strong>当前读</strong>，并通过<strong>临键锁</strong>来防止幻读。</p>
</li>
</ul>
</li>
<li><p><strong>为什么临键锁是 (previous_key, current_key] 的形式？</strong></p>
<ul>
<li><p><strong>本质</strong>: 临键锁是数据库在扫描索引时，对索引结构的基本锁定单元。索引本身是逻辑上连续的，临键锁的设计旨在将这个连续的空间，<strong>无缝、无重叠地</strong>进行划分。</p>
</li>
<li><p><strong>左开右闭的哲学</strong>:</p>
<ul>
<li><p><strong>避免重叠</strong>: 如果是闭区间 [a, b]，那么下一个锁 [b, c] 就会导致 b 被重复锁定。</p>
</li>
<li><p><strong>避免遗漏</strong>: 如果是开区间 (a, b)，那么 a 和 b 这两个实体本身就会被遗漏。</p>
</li>
<li><p><strong>结论</strong>: 只有左开右闭的设计，才能像拉链一样，将整个索引空间 (负无穷, 正无穷) 完美地、不多不少地分割成一系列连续的锁定单元，确保了并发控制的严谨性。</p>
</li>
</ul>
</li>
</ul>
</li>
</ol>
<h3>六、 读取模式与锁的触发</h3>
<p>数据库的读取操作分为两种模式，这直接决定了是否会触发复杂的锁机制：</p>
<ol>
<li><p><strong>快照读 (Consistent Read)</strong></p>
<ul>
<li><p><strong>触发条件</strong>: 普通的 SELECT 语句。</p>
</li>
<li><p><strong>机制</strong>: 基于<strong>MVCC</strong>，读取事务开始时创建的数据快照。</p>
</li>
<li><p><strong>行为</strong>: <strong>不加任何锁</strong>。它通过读取历史版本的数据来避免与其他事务的写操作发生冲突。这是“可重复读”隔离级别能够解决“不可重复读”问题的根本原因。</p>
</li>
</ul>
</li>
<li><p><strong>当前读 (Current Read)</strong></p>
<ul>
<li><p><strong>触发条件</strong>: SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE, 以及所有的 INSERT, UPDATE, DELETE 操作。</p>
</li>
<li><p><strong>机制</strong>: 读取数据库中<strong>最新提交</strong>的版本，并对读取到的记录<strong>施加锁</strong>。</p>
</li>
<li><p><strong>行为</strong>:</p>
<ul>
<li><p>在<strong>可重复读</strong>隔离级别下，当前读会使用<strong>临键锁 (Next-Key Lock)<strong>来锁定扫描到的索引范围，从而彻底解决</strong>幻读</strong>问题。</p>
</li>
<li><p>普通的SELECT COUNT(*)属于快照读，因此<strong>不会</strong>产生幻读，也<strong>不会</strong>加锁。</p>
</li>
</ul>
</li>
</ul>
</li>
</ol>

      <p style='text-align: right'>
      <a href='https://skydevs.link/posts/study/mysql-innodb-locks-mvcc-phantom-reads-explained#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">68a6c0076663146074aa6cf8</guid>
  <category>posts</category>
<category>学习/踩坑</category>
 </item>
  <item>
    <title>sa-Token的JWT验证问题</title>
    <link>https://skydevs.link/posts/study/sa-token-jwt-session-persistence</link>
    <pubDate>Wed, 20 Aug 2025 08:01:38 GMT</pubDate>
    <description>写项目的时候，发现自己的项目在后端服务重启后，登入状态会失效，一查配置发现sa-Token用的默认是</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://skydevs.link/posts/study/sa-token-jwt-session-persistence'>https://skydevs.link/posts/study/sa-token-jwt-session-persistence</a></blockquote>
      <p>写项目的时候，发现自己的项目在后端服务重启后，登入状态会失效，一查配置发现sa-Token用的默认是UUID作为token，于是便着手修改为JWT。众所周知，JWT会记录信息，包括<strong>你是谁、你有什么权限、什么时候过期</strong>，理所当然的，我就认为换成JWT后，登入状态失效的问题就能得到解决。</p>
<p>在更换为JWT的过程中，我遇到了两个问题：</p>
<p>其一，是在application.yml里配置token-style为jwt后，前端收到的token依旧是UUID的问题。</p>
<p>这个是因为我为了实现权限管理，实现了<code>StpInterface</code>这个接口，这就导致“约定大于配置”被打破了，所以，我还需要通过注入 StpLogic 对象，告知 Sa-Token 在 StpUtil 中要使用的 StpLogic 的具体实现类</p>
<p>其二，是登入状态依旧没有被保存的问题。</p>
<p>这个我思来想去都没能搞懂，最后，在Gemini大人的帮助下，我才知道这是sa-Token实现的问题：三次检查。
	
第一次，是根据密钥，用相同的算法，对head和payload进行运算，然后对JWT自带的那个值进行比较。
	
第二次，是检查有效期。
	
前两次都很正规，最头疼的就算这个第三次检查：<strong>Sa-Token的核心会话状态</strong>。</p>
<p>sa-Token为了实现“把人踢下线”这个功能，会从JWT里读取loginId，去内存里查有没有这个id，正是这个步骤，导致我之前所作的将UUID替换为JWT的工作都白费了。</p>

      <p style='text-align: right'>
      <a href='https://skydevs.link/posts/study/sa-token-jwt-session-persistence#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">68a580e2fe67e3e4e8b76f90</guid>
  <category>posts</category>
<category>学习/踩坑</category>
 </item>
  <item>
    <title>甘城光辉游乐园——经营游乐场的日常</title>
    <link>https://skydevs.link/notes/22</link>
    <pubDate>Mon, 21 Apr 2025 08:58:25 GMT</pubDate>
    <description>虽然这部番有一个主线——三个月内达到50w游客，但是我依然感觉它偏向日常，它更多的是讲述在经营过程中</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://skydevs.link/notes/22'>https://skydevs.link/notes/22</a></blockquote>
      <p>虽然这部番有一个主线——三个月内达到50w游客，但是我依然感觉它偏向日常，它更多的是讲述在经营过程中发生的种种，至于经营状况是否在好转，更多的是凭借每一话结尾的“本日游客数”和“累计游客数”来体现。除了结尾冲刺50w人数，作为观众的我没怎么感觉压力和紧张感。</p>
<p>我来看这番有一个原因就是五十铃，我本来以为恋爱元素会比较多，结果不说通篇下来他俩感情没什么进展，男主还在开头因为要附魔把初吻给了公主？！再者说公主也才14岁啊（虽然过了10年的14岁），这绝对是犯罪吧？虽然没有不支持这一对的想法，但是还是五十铃更对我xp。</p>
<p>作画就不用说了，毕竟是京阿尼，总体而言，算是一部中规中矩的游乐园经营日常，6/10。</p>

      <p style='text-align: right'>
      <a href='https://skydevs.link/notes/22#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">680608b12e68ffb74866a1c9</guid>
  <category>notes</category>
false
 </item>
  <item>
    <title>香格里拉边境——玩游戏开心最重要</title>
    <link>https://skydevs.link/notes/21</link>
    <pubDate>Sun, 20 Apr 2025 13:26:33 GMT</pubDate>
    <description>看完的最大感受是——怎么第二季结尾在这种地方啊喂(#`O′)

说回正题，看完的感受，或者说在观看过</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://skydevs.link/notes/21'>https://skydevs.link/notes/21</a></blockquote>
      <p>看完的最大感受是——怎么第二季结尾在这种地方啊喂(#`O′)</p>
<p>说回正题，看完的感受，或者说在观看过程中的感受，最大的就是作者确实是玩过游戏的，很多剧情都能让我们游戏玩家有共鸣，特别是RPG玩家吧，感觉会让玩过老头环的玩家很有共鸣，顺带一提，我只玩过一会儿老头环，因为操作手感我实在不敢恭维（先玩的只狼）。</p>
<p>故事是围绕着“游戏”这一主题进行叙述的，从没离题过，这是香格里拉边境的优点，相对的，这对我来说也是个“缺点”吧，因为太过注重游戏的描写，从而忽略了故事一开始作为引子的感情的描写（有点心疼斋贺玲），本来以为拿到的是女主的戏份，结果到第二季结束，戏份少得可怜，最多的戏份就是出现在每集结尾的彩蛋里。</p>
<p>香格里拉边境还有一个优点，那就是扎实的作画，打戏部分做的确实不错，最令人印象深刻的莫过于讨伐守墓的韦札卫门时BOSS释放的晴天大征，“万化为晴 我等极致一斩 我等亦能断龙”，压迫感拉满。</p>
<p>在这番播出的时候，就常常听人诟病这番太水了，进度慢，不过，对于补番的我来说，这简直就是优点，一口气补完不要太爽！不过还有一个问题就是前情回顾这东西，印象中我跳过最长的一次是跳过开头5min……再减去ED和彩蛋时间，正片的时间只剩下可怜的15min，而且它每话前面都要讲一段这番的介绍，这确实很水。</p>
<p>嘛嘛，不过这番的补番体验确实好啊，10分满分能在我这给个7分。</p>
<p>这部番的核心观点，在我看来就是——玩游戏开心最重要，主角——桑乐的行动模式一直都是以自己开心为主，这无时无刻不在传达“游戏就是要自己玩的爽”这一观点，本来现实生活就已经够糟的了，结果还要为了游戏头痛，那玩游戏是为了什么？唉，说到这就不得不提某些玩起来和上班一样的游戏了，这也是我半弃坑这些游戏的原因。番剧中有一个片段，是桑乐在犹豫要不要和塞卡-0一起讨伐夜袭的鲁卡翁，这个时候桑乐在担心一起讨伐会导致情报泄露，这时，塞卡-0说“既然要玩，就要尽全力享受”，这一说就给桑乐说醒了，是啊，玩游戏嘛，不享受游玩过程的乐趣，而一直担心这担心那的，这哪有什么开心的？生活也是同理，我们总是担心未来的事，过度担心了，因而忘记了我们现在活在的每一个瞬间，痛苦就随之降临了。我们应该享受当下的每一个瞬间，未来是命运最好的安排。</p>
<p>永远姐踩我。</p>

      <p style='text-align: right'>
      <a href='https://skydevs.link/notes/21#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">6804f6092e68ffb748668de1</guid>
  <category>notes</category>
false
 </item>
  
</channel>
</rss>