<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="/feeds/rss-style.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title></title>
        <link>https://agility6.me</link>
        <description>The essence of magic is thought.</description>
        <lastBuildDate>Mon, 13 Apr 2026 12:59:41 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Astro Chiri Feed Generator</generator>
        <language>zh-CN</language>
        <copyright>Copyright © 2026 Agility6</copyright>
        <atom:link href="https://agility6.me/rss.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Codex Learn Rust — WIP]]></title>
            <link>https://agility6.me/codex-learn-rust</link>
            <guid isPermaLink="false">https://agility6.me/codex-learn-rust</guid>
            <pubDate>Mon, 13 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[背景 本篇文章打算使用Codex来学习一下Rust语言，本人是Gopher对Rust只能说是略有耳闻，一直听说Rust是一门学习路线陡峭的、入坑到放弃只需要 Hello World 。在AI时代下，我不打算采用阅读官方的“圣经”进行学习，直接通过迁移一个项目来进行学习。 使用该开源项目作为引子来学习Rust，HuffmanImageCompression 使用Huffman编码对图像进行无损压缩和...]]></description>
            <content:encoded><![CDATA[<h2>背景</h2>
<p>本篇文章打算使用Codex来学习一下Rust语言，本人是Gopher对Rust只能说是略有耳闻，一直听说Rust是一门学习路线陡峭的、入坑到放弃只需要 <code>Hello World</code> 。在AI时代下，我不打算采用阅读官方的“圣经”进行学习，直接通过迁移一个项目来进行学习。</p>
<p>使用该开源项目作为引子来学习Rust，<a href="https://github.com/xujj25/HuffmanImageCompression">HuffmanImageCompression</a> 使用Huffman编码对图像进行无损压缩和解压。</p>
<blockquote>
<p>可参考Huffman相关的文章： <a href="https://agility6.me/huffman-trees-experiment/">https://agility6.me/huffman-trees-experiment/</a></p>
</blockquote>
<p>先来对Rust做一次体感过山车吧</p>
<p>Rust的定位是什么</p>
<ul>
<li>接近 C/C++ 的性能</li>
<li>同时提供内存安全（无 GC）</li>
<li>现代语言抽象能力（类似 Go + 一部分函数式特性）</li>
<li>关键词：Ownership（所有权）、Borrowing（借用）、Zero-cost abstraction（零成本抽象）、Fearless concurrency（无畏并发）</li>
</ul>
<p>Go vs Rust</p>
<ul>
<li>没有 GC 的内存安全：所有权 (Ownership) 与借用 (Borrowing)</li>
<li>错误处理：告别 <code>if err != nil</code></li>
<li>并发模型：无畏并发 (Fearless Concurrency)</li>
<li>Go 的哲学是 <strong>“简单、直接、高并发”</strong>，那么 Rust 的哲学就是 <strong>“零成本抽象、内存安全、无畏并发”</strong>。</li>
</ul>
<p>Agent驱动学习也算是探索一种新的学习模式，使用类似Agent Team的模式进行学习。大致的方法就是一共有4个Agent，各司其职</p>
<ol>
<li>Agent 1 执行全局拆解与排期</li>
<li>Agent 2 制定教学规范并出码 (落盘到 specs)</li>
<li>唤醒 Agent 3（排错专家）</li>
<li>唤醒 Agent 4（QA）</li>
</ol>
<pre><code class="language-md"># Huffman-Rust Agent Team 全局指南 (文档驱动版)

## 核心铁律：万物皆落盘
任何讨论、架构决策、代码设计都必须写入 `docs/` 或 `specs/` 目录下的 Markdown 文件中，Agent 之间通过读取这些文件进行上下文同步。

## 角色定义
1. **Agent 1 (Architect &amp; PM)**：分析整个 C++ 项目。产出系统架构文档 `docs/architecture.md` 和分步开发路线图 `docs/roadmap.md`。
2. **Agent 2 (Rust Mentor)**：根据路线图的当前任务，产出具体的模块实现规范和保姆级注释代码，保存至 `specs/&lt;当前模块&gt;_guide.md`。
3. **Agent 3 (Reviewer)**：在人类抄写代码出错时，解释编译错误并引导修复。
4. **Agent 4 (QA)**：为跑通的代码编写测试，并在通过后，提示人类更新 `docs/roadmap.md` 的任务状态。
   
## 核心铁律
所有模块的开发必须经过：Agent 1 拆解 -&gt; Agent 2 教学与出码 -&gt; 人类抄写 -&gt; Agent 3 查漏 -&gt; Agent 4 测试。Agent 之间不得越权。
</code></pre>
<h3>具体步骤</h3>
<p><strong>Agent 1 执行全局拆解与排期</strong></p>
<pre><code class="language-text">“请阅读 `AGENT_TEAM_GUIDE.md`。现在你严格扮演 **Agent 1 (Architect &amp; PM)**。 我提供给你了完整的 HuffmanImageCompression C++ 项目源码。请你不要写任何 Rust 代码，完成以下两项任务：

**任务 1：输出系统架构** 请分析 C++ 项目的核心流程、数据结构和模块划分。请输出 Markdown 格式，准备让我全盘复制保存为 `docs/architecture.md`。

**任务 2：制定拆解路线图** 结合 Rust 的特性，将这个项目拆解为循序渐进的开发 Milestone（里程碑），比如 M1 (基础数据结构)、M2 (文件IO)、M3 (核心算法)。每个里程碑下划分子任务。 请输出带有复选框（`- [ ]`）的 Markdown 格式，明确指出我应该‘先写哪个，再写哪个’。准备让我全盘复制保存为 `docs/roadmap.md`。
</code></pre>
<p>人类动作：将 Agent 1 生成的两个文档分别新建并保存到 <code>docs/architecture.md</code> 和 <code>docs/roadmap.md</code> 中。**以后的开发，你每天第一件事就是打开 <code>roadmap.md</code> 看今天要推进哪个复选框。</p>
<p><strong>单模块开发循环（文档驱动执行）</strong></p>
<pre><code class="language-text">“请阅读 `AGENT_TEAM_GUIDE.md` 和 `docs/roadmap.md`。现在你严格扮演 **Agent 2 (Rust Mentor)**。 我现在的进度是执行任务：**[M1.1: 实现哈夫曼树节点定义]**。 请你：

1. 查阅 C++ 源码中对应的部分，讲解在 Rust 中实现该逻辑需要掌握的 1-2 个核心知识点。
    
2. 按照符合 Rust 的习惯，编写出完整的实现代码。
    
3. 代码必须带有极度详细的中文注释（解释‘为什么这样写’），方便我临摹。 请将以上内容组织为一篇完整的 Markdown 教学文档，准备让我保存为 `specs/m1_1_tree_node_guide.md`。”
</code></pre>
<p><strong>人类动作：</strong> 将 Agent 2 生成的文档保存到 <code>specs/</code> 目录下。然后，打开你的 <code>.rs</code> 源码文件，<strong>照着 spec 文档把代码敲进去</strong>。</p>
<p><strong>Agent 3 结对查漏补缺 (不落盘，作为实时辅助)</strong></p>
<p>在实现的过程中遇到了编译器报错</p>
<pre><code class="language-text">“请扮演 **Agent 3 (Reviewer)**。我在实现 `specs/m1_1_tree_node_guide.md` 时报错了。 报错信息：[贴报错] 当前代码：[贴代码] 请解释原因并引导我修复，不要直接给完整代码。”
</code></pre>
<p><strong>Agent 4 编写测试并推动进度 (更新落盘文件)</strong></p>
<p>代码跑通后，你需要通过测试来闭环这个任务。</p>
<pre><code class="language-text">“请扮演 **Agent 4 (QA)**。我已完成了 M1.1 任务。这是我的代码：[贴代码]。 请你：

1. 提供针对该模块的严谨的单元测试代码 (`#[cfg(test)]`)，带有注释。
    
2. 提醒我：‘测试通过后，请去 `docs/roadmap.md` 中将 M1.1 任务勾选完成（`- [x]`）’。”
</code></pre>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[A Trip to Dalian]]></title>
            <link>https://agility6.me/trip_dalian</link>
            <guid isPermaLink="false">https://agility6.me/trip_dalian</guid>
            <pubDate>Wed, 28 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[地点：2026.01.28 Seed咖啡店 ✍🏻 歌曲：I Know You Are But What Am I?（我是POI死忠粉!!!） 01.26的23点抵达大连，第一感受就是机场无廊桥（冷 酒店的位置是在中山广场附近，因此需要打车过去顺便看看大连的夜间，道路空旷以至于平凡听到”您已超速“的提示和师傅闲聊（不得不说大连人民的说话风格，总有一种豁达大气的感觉）师傅说东北是没有夜生活的，对于南...]]></description>
            <content:encoded><![CDATA[<p>地点：2026.01.28 Seed咖啡店 ✍🏻</p>
<p>歌曲：I Know You Are But What Am I?（我是POI死忠粉!!!）</p>
<p>01.26的23点抵达大连，第一感受就是机场无廊桥（冷</p>
<p>酒店的位置是在中山广场附近，因此需要打车过去顺便看看大连的夜间，道路空旷以至于平凡听到”您已超速“的提示和师傅闲聊（不得不说大连人民的说话风格，总有一种豁达大气的感觉）师傅说东北是没有夜生活的，对于南方孩子来说挺难以想象的。</p>
<h3>🚀</h3>
<blockquote>
<p>以前旅行总喜欢规划路线，但这次独自旅行想尝试随性一点，毕竟酒店的预定都是飞机起飞的最后一刻才订下来的😇</p>
</blockquote>
<p>这几天主要途径这几个景点（反复二刷）星海广场、莲花山观景台、跨海大桥观景台、银沙滩、城市街道</p>
<p>早上在酒店吃完早餐就出发了，走在中山广场附近的街道莫名有一种很放松的感觉，可能是冷、新鲜感等作怪吧。大连房屋的建筑有一种不符合传统“新时代“的风格，走着走着就想停下来欣赏一下。</p>
<p>星海广场，海映射着光（星海）海鸥完美融合了这道风景，像有种魔力站在那里心中会不由自主的平静下来，当然仅仅是内心的平静，外部的海风会让人颤抖。</p>
<p>站在那里看着就足以说不虚此行了！相机调整到连续对焦疯狂按下记录海鸥！</p>
<p>开始觅食，海胆水饺（喜鼎版）距离星海广场步行5分钟左右就可以抵达，最好是提前取号十分火爆的一家店，入口即鲜，百分之百的推荐。吃完之后又返回星海广场就静静的看着看着，实在是太有魔力了。</p>
<p>莲花山观景台，冬季的树总是渲染出凄凉的气息，步行15min即可上去了，中途有不少位置可以看到大桥的面貌，抵达观景台已经是快进入落日的状态</p>
<blockquote>
<p>一个人旅行好像更容易去观察任何的事物，无论是风景还是自身都会被无限去放大</p>
</blockquote>
<hr />
<h4>光线和心境</h4>
<p><strong>光线和心境总是能有微妙的共鸣</strong></p>
<blockquote>
<p>🤔 这是一份只有独行者才能领受的、关于自由、孤独与永恒的礼物。请珍视这份感受，它是你与自己、与世界深度连接的明证。</p>
</blockquote>
<h5>晨光</h5>
<p>它不仅仅是明亮，也带着一种”可能性“，光线清透锐利，万物轮廓分明，影子很短，一切都像是刚刚开始、充满能量，也往往是旅途中最精力充沛、好奇心最盛的时刻，眼前的风景和你内心的状态同频共振。</p>
<hr />
<h5>午后斜阳 —— “没落”</h5>
<p>下午三、四点的太阳，位置已偏，光线变得绵长、金黄，给万物拉出长长的影子</p>
<p>光影呈现出“倾斜感”和”流失感“，它会安静的提示你一天的黄金时刻已过，这种“没落”感并非绝望，而是一种带着温度与质感的成熟。</p>
<hr />
<h5>黄昏与夜的临界 —— 极致的平静</h5>
<p>是最深邃的华章，任何地方都存在这个短暂的“<strong>临界点</strong>”，但是大连似乎是最好的诠释。</p>
<p>面对壮丽的日落，没有同伴需要交谈，没有感叹需要交换，形成了一种毫无中介的、直接的交换。</p>
<p>自我的消融在昼夜交替的魔法时刻，当色彩在天际流淌，世界仿佛变成一个巨大的、缓慢呼吸的生命体。</p>
<p>独自一人时，更容易放下“游客”或“个体”的身份感，不再是“在看风景的人”，而是成为了这场宏大落幕仪式的一部分</p>
<p>呼吸仿佛与光线的明灭同步，个体的孤独感在宇宙的节律中消解，取而代之的是一种与天地共息的融合感，这才是终极的平静——不是没有情绪，而是情绪找到了比自身更广大的容器。</p>
<hr />
<h3>未完待续💡</h3>
<p>大连：平静、慢</p>
<h3>📷</h3>
<h4>海鸥</h4>
<p><img src="https://agility6.me/posts/trip_dalian/01.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/02.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/03.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/04.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/05.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/06.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/07.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/08.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/09.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/10.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/11.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/12.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/13.JPG" alt="" /></p>
<h4>景</h4>
<p><img src="https://agility6.me/posts/trip_dalian/14.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/15.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/16.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/17.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/18.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/20.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/21.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/22.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/23.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/24.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/25.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/26.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/27.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/28.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/29.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/30.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/31.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/32.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/33.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/34.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/35.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/36.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/37.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/38.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/39.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/40.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/47.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/48.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/49.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/50.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/51.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/52.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/53.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/54.JPG" alt="" />
<img src="https://agility6.me/posts/trip_dalian/55.JPG" alt="" /></p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Final Commit in 2025 👋🏻]]></title>
            <link>https://agility6.me/2025</link>
            <guid isPermaLink="false">https://agility6.me/2025</guid>
            <pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[纠结过去其实毫无意义，枯萎的枝条就让它枯萎吧。重要的是现在要为自己生命出的新芽而高兴 💭 2025年已然结束，又到了年底的头脑风暴、疯狂复盘的时候了，靠着照片和零散的手记开始回顾这一整年所发生的事情。 2025年的记忆要从三月份的转正开启，而这一年也将围绕工作而展开。 2025年春节过后，得到有转正机会的消息也算是25年比较好的开端，是幸运的，面对如今地狱级工作环境中，能够找到一份自己热爱的、工...]]></description>
            <content:encoded><![CDATA[<blockquote>
<p><strong>纠结过去其实毫无意义，枯萎的枝条就让它枯萎吧。重要的是现在要为自己生命出的新芽而高兴 💭</strong></p>
</blockquote>
<p>2025年已然结束，又到了年底的头脑风暴、疯狂复盘的时候了，靠着照片和零散的手记开始回顾这一整年所发生的事情。</p>
<blockquote>
<p>2025年的记忆要从三月份的转正开启，而这一年也将围绕工作而展开。</p>
</blockquote>
<p>2025年春节过后，得到有转正机会的消息也算是25年比较好的开端，是幸运的，面对如今地狱级工作环境中，能够找到一份自己热爱的、工作环境良好的的确是不容易的。在经历转部门面试一系列流程后，顺利加入目前所有的业务部门中了（很开心🚀</p>
<p>随着正式工作有了着落之后，也来到了校园的最后倒计时了</p>
<hr />
<h3>真正告别校园 🏫</h3>
<p>作为一名钟爱水 「牛客」 的大学生在实习的时候看到一段话（不得不说牛客真的就是，焦虑的源泉。</p>
<blockquote>
<p>实习完回校有一种割裂感，感觉自己已经不属于校园了，晚上去操场散步看着草坪的人，尸体暖暖的。</p>
</blockquote>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2Ft-01.tj8fPlhI.png&amp;fm=webp&amp;w=800&amp;h=418&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p>一阵强烈的共情感涌入，突然发觉，自己的大学生活一直在追寻实习机会、逃离校园。等真正准备离去的时候，会发现好像在校园中“玩”的不够多、不够尽兴。答辩当天和舍友逛完了校园的每一个角落，每一个必经之路，同时我也知道新的生活已经开启了。</p>
<p><em><strong>于是荣获最暖评论，或者是大多数人的感受吧</strong></em></p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2Ft-02.B92LJL1R.png&amp;fm=webp&amp;w=800&amp;h=550&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h4>如何定义它</h4>
<p>回想大学生活，更多是打上“不合群”的标签，可能大多数人都听过这一句话，大学是自由的。</p>
<p>自由的定义是什么，是行为上的自由还是精神上的自由。我倾向于后者，高中的知识是固定的，所有的一切都是成体系的，因此容不了有一丝的偏科。</p>
<p>而大学自由，你可以不用拘泥于专业的限制，书籍就在那里，只要你感兴趣就可以去获取。快速试错、快速了解，可以把 “三分钟热度" 发挥到极致。</p>
<p>这是我对大学的理解。</p>
<blockquote>
<p><strong>谁也没有权力能支配 一生一趟忠于自己的表演</strong></p>
</blockquote>
<h4>一些其他</h4>
<p>对于大学的生活、实习、身边的伙伴、老师真的就是无可挑剔的存在了!!!</p>
<h5>实习</h5>
<p>每一段实习都离不开亲爱的舍友，没有大哥们的掩护就没有光明的未来（bushi</p>
<p>很庆幸大学能够抓住一切机会出去实习，也可能自己始终保持一种快一步、多焦虑一步的观念。</p>
<ul>
<li>第一段坐落于23年的时候，详情可看23年的总结👀 一个稚嫩的大学生第一次外出实习的恐惧（不过环境简直是世外桃源 💯）</li>
</ul>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F03-t.C_QlTd_w.png&amp;fm=webp&amp;w=800&amp;h=971&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<ul>
<li>第二段坐落于24年，这是第一次面临就业的选择，详情可看24年的总结👀 不得不说中秋礼盒是十分气派的。</li>
</ul>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F04-t.DI55iqZ7.png&amp;fm=webp&amp;w=800&amp;h=585&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<ul>
<li>第三段也是如今就职的公司，未完待续…</li>
</ul>
<h5>照片记忆</h5>
<blockquote>
<p><strong>照片和歌曲这两个载体，不仅仅只是感官上的触动，也承载着时光机的作用。</strong></p>
</blockquote>
<p>收获很大的一次比赛，也成功成立了EchoJobs工作室，也第一次尝试当队长。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2Ft-07.BJemzHDO.png&amp;fm=webp&amp;w=800&amp;h=567&amp;dpl=69dce8926406240008354a25" alt="photo" />
<img src="https://agility6.me/.netlify/images?url=_astro%2Ft-08.B0aPGZX-.png&amp;fm=webp&amp;w=800&amp;h=1147&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p>每次看到这张照片，好像就已经身处在那个自由，比较无忧无虑的环境之中，校园的味道总是独一无二的。
<img src="https://agility6.me/.netlify/images?url=_astro%2Ft-06.BQwgiV6b.png&amp;fm=webp&amp;w=800&amp;h=1021&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p>大学超级可爱的老师 + 项目带队老师 + 论文指导老师。
<img src="https://agility6.me/.netlify/images?url=_astro%2Ft-05.TZHd7dXj.png&amp;fm=webp&amp;w=800&amp;h=1130&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p>完结毕业!!! 🎓
<img src="https://agility6.me/.netlify/images?url=_astro%2F12-t.BjlzXVu7.png&amp;fm=webp&amp;w=800&amp;h=1021&amp;dpl=69dce8926406240008354a25" alt="photo" />
<img src="https://agility6.me/.netlify/images?url=_astro%2F13-t.C6TcxPVw.png&amp;fm=webp&amp;w=800&amp;h=1021&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h3>工作 🧑‍💻</h3>
<blockquote>
<p><strong>世界有浮力，放松就能被托举</strong></p>
</blockquote>
<p>实习和工作最大的不同，倒不是压力和工作内容的不同。而是自身的身份转变不同，实习的唯一目的是，能够在秋招/春招脱颖而。</p>
<p>25年最大的变化就是，自我身份的转变这一点在24年或者更早就意识到了，但是真正在其中的时候，焦虑感会具体化。</p>
<p>大学的焦虑或许是一种带有目的性的焦虑，简而言之是为了 「毕业能够有更好的工作」 而焦虑。</p>
<p>如今工作上不得不的面临的是，可能会伴随着无意义、无目的性的焦虑。</p>
<p>25年的后半段也在不停的想这个问题，现在我应该为什么而焦虑呢？或者用 「福格行为模型」 这本书来说现阶段自己的 「动机」 是什么（WIP中…</p>
<h4>区分可控 let them 和不可控 let me</h4>
<p>今年看到一个比较有趣的观念，需要在生活中区分好这两个类型，进而去获取掌控感。</p>
<ul>
<li>「let them」 随它们去吧，清醒的认识与接受，能控制的只有自己。</li>
<li>「let me」 让我来吧，行为的指挥官由我来接纳。</li>
</ul>
<h5>压力来临时</h5>
<p>当压力来临时，大脑通常会混在一起处理三件事：</p>
<ul>
<li>
<p>发生了什么</p>
</li>
<li>
<p>别人怎么看</p>
</li>
<li>
<p>我现在怎么办</p>
</li>
</ul>
<p>「let them」 先把前两件事卸载掉，事情已经发生了，别人也已经有反应了。记录日志，不再重试，停止无效的心理重算。</p>
<p>「let me」 启动在当前约束条件下，我下一步可控的最小动作是什么？注意这个“最小”。压力大时，<strong>人最容易犯的错是试图一次性解决“整个人生”。</strong></p>
<h2>其他</h2>
<blockquote>
<p><strong>一个健康的人，需要学会去“玩”</strong></p>
</blockquote>
<ul>
<li>
<p>今年更新了电脑64G!!!</p>
</li>
<li>
<p>大学想要的HHKB键盘!!!</p>
</li>
</ul>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F14.D864Usuh.png&amp;fm=webp&amp;w=800&amp;h=625&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<ul>
<li>Nikon Z30!!!</li>
</ul>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F15.DdtZPtp4.png&amp;fm=webp&amp;w=800&amp;h=568&amp;dpl=69dce8926406240008354a25" alt="photo" />
<img src="https://agility6.me/.netlify/images?url=_astro%2F16.D-hSwTnm.png&amp;fm=webp&amp;w=800&amp;h=802&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h2>最后</h2>
<ul>
<li>
<p>希望能够保持思考，保持进步，敬畏工程师这个职位，提升硬实力!</p>
</li>
<li>
<p>保持折腾、保持“玩”（Lightroom启动!</p>
</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[论文分享：An Implementation and Analysis of a Kernel Network Stack in Go with the CSP Style（WIP)]]></title>
            <link>https://agility6.me/network-stack-in-go</link>
            <guid isPermaLink="false">https://agility6.me/network-stack-in-go</guid>
            <pubDate>Fri, 26 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[https://arxiv.org/abs/1603.05636 Network Stack in Go with CSP Style 该论文尝试使用Golang和 CSP 风格对内核网络栈进行实现 Golang具有垃圾回收机制 &amp; 强类型特性 CSP (Communicating Sequential Processes) 利用 goroutine 和 channel 并行机制，把复杂任...]]></description>
            <content:encoded><![CDATA[<ul>
<li><a href="https://arxiv.org/abs/1603.05636">https://arxiv.org/abs/1603.05636</a></li>
</ul>
<h3>Network Stack in Go with CSP Style</h3>
<p>该论文尝试使用Golang和 CSP 风格对内核网络栈进行实现</p>
<ul>
<li>
<p>Golang具有垃圾回收机制 &amp; 强类型特性</p>
</li>
<li>
<p>CSP (Communicating Sequential Processes) 利用 goroutine 和 channel 并行机制，把复杂任务拆分多个并发子任务</p>
</li>
</ul>
<p>GoNet通过虚拟的tab网络接口在<strong>用户态模拟</strong>一个独立的网络栈，协议栈主要分为三层，数据链路层、网络层和传输层</p>
<p>主要通过 packet dealer 设计，每一层主要的 goroutine 负责接收来自下层的数据包，通过 channel 进行分发给多个 goroutine 处理</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F1.C8OhRlbN.png&amp;fm=webp&amp;w=800&amp;h=461&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Plan9 & Golang]]></title>
            <link>https://agility6.me/plan9go</link>
            <guid isPermaLink="false">https://agility6.me/plan9go</guid>
            <pubDate>Sat, 04 Jan 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[如果你还没有阅读Plan 9相关的知识，推荐阅读Plan9 &amp; Go Assembler 环境说明：Mac m1 （ARM架构）、Golang v1.23.2 简单回顾 这里简单回顾几个比较重要的知识点吧 Plan 9汇编伪寄存器 SB（Static base pointer）用于访问全局符号，比如函数、全局变量 FP（Frame pointer）用于访问函数的参数和返回值 PC（Prog...]]></description>
            <content:encoded><![CDATA[<p>如果你还没有阅读Plan 9相关的知识，推荐阅读<a href="https://tinyfun.club/blog/assembler">Plan9 &amp; Go Assembler</a></p>
<blockquote>
<p>环境说明：Mac m1 （ARM架构）、Golang v1.23.2</p>
</blockquote>
<h2>简单回顾</h2>
<p>这里简单回顾几个比较重要的知识点吧</p>
<ul>
<li>
<p>Plan 9汇编伪寄存器</p>
<ul>
<li>SB（Static base pointer）用于访问全局符号，比如函数、全局变量</li>
<li>FP（Frame pointer）用于访问函数的参数和返回值</li>
<li>PC（Program counter）保存CPU下一条要运行的指令</li>
<li>SP（Stack pointer）指向当前栈帧的栈顶</li>
</ul>
</li>
<li>
<p>Plan 9源操作数与目标操作数方向，源操作数在前，目的操作数在后</p>
<ul>
<li><code>movl $0x2, %eax</code> 将立即数0x2移动到eax寄存器</li>
</ul>
</li>
</ul>
<h2>再次窥探函数</h2>
<h3>函数序言</h3>
<p>在函数调用的时候，会经常看到这样一段的函数序言，主要的作用就是保存「调用者的」<code>BP</code></p>
<pre><code class="language-asm">pushq     %rbp
movq      %rsp,  %rbp
subq      %16,   %rsp
</code></pre>
<p>大致步骤如下</p>
<ol>
<li>初始状态：函数尚未被调用</li>
</ol>
<p>此时函数只包含返回地址（即调用函数的下一条指令的地址）</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F01.CeUOFW1f.png&amp;fm=webp&amp;w=800&amp;h=385&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
<ol>
<li><code>pushq %rbp</code></li>
</ol>
<p>将调用者的栈帧指针（<code>%rbp</code>）压入栈
保存上一个栈帧的基地址，用于函数返回时恢复调用者的栈帧</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F02.DHegFrC4.png&amp;fm=webp&amp;w=800&amp;h=387&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
<ol>
<li><code>moveq %rsp,  %rbp</code></li>
</ol>
<p>将当前栈指针%rsp的值赋给%rbp, 建立当前函数的栈帧基地址
标记当前函数的栈帧起点</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F03.D-ln0XxE.png&amp;fm=webp&amp;w=800&amp;h=402&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
<ol>
<li>
<p><code>subq $16, %rsp</code></p>
<p>将栈指针%rsp向下移动16字节，为局部变量分配空间
完成局部变量栈空间的分配</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F04.B4d1POOq.png&amp;fm=webp&amp;w=800&amp;h=456&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
</li>
</ol>
<p>下面就来编写代码，验证一下吧</p>
<p><code>go tool compile -S -N -l add_func.go</code></p>
<pre><code class="language-go">// base/prologue/add_func.go
package main  
  
func add(a, b int) int {  
    return a + b  
}  
  
func main() {  
    _ = add(1, 2)  
}

</code></pre>
<ul>
<li><code>MOVD.W  R30, -32(RSP)</code> 保存调用者的链接寄存器（R30）到栈中。</li>
<li><code>MOVD    R29, -8(RSP)</code> 保存当前帧指针（R29）到栈中。</li>
<li><code>SUB     $8, RSP, R29</code> 更新栈帧指针R29。</li>
</ul>
<pre><code class="language-assembly">0x0000 00000         TEXT    main.add(SB), NOSPLIT|LEAF|ABIInternal, $32-16  
0x0000 00000         MOVD.W  R30, -32(RSP)  
0x0004 00004         MOVD    R29, -8(RSP)  
0x0008 00008         SUB     $8, RSP, R29
</code></pre>
<p>再来看看函数尾声的分析</p>
<p><code>ADD $24, RSP, R29</code> 恢复栈帧指针</p>
<ul>
<li><code>ADD $32, RSP</code> 释放栈空间</li>
<li><code>RET (R30)</code> 返回调用者。</li>
</ul>
<pre><code class="language-assembly">0x0024 00036         ADD     $24, RSP, R29
0x0028 00040         ADD     $32, RSP
0x002c 00044         RET     (R30)
</code></pre>
<blockquote>
<p>注意架构差异，Arm架构中使用的是<code>MOVD.W</code>保存寄存器到栈上</p>
</blockquote>
<h3>调用规约</h3>
<p>函数的调用规约用于规定如何在程序中调用函数，例如参数的传递方式、返回值的处理和寄存器的使用等<a href="https://en.wikipedia.org/wiki/X86_calling_conventions">x86 calling conventions</a></p>
<p>在这里只需要明白，Go在1.17之后使用了基于寄存器的调用规约。当然寄存器不是无限使用的，当达到一定程度就会使用栈传递。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F05.BM4mJc-a.png&amp;fm=webp&amp;w=800&amp;h=530&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
<blockquote>
<p>不同的架构对于寄存器的使用可能会不一样～</p>
</blockquote>
<h2>Plan9 &amp; 基础数据结构</h2>
<h3>string</h3>
<p>创建一个简单的字符串，提取出关键的汇编代码<code>var a = "hello"</code></p>
<pre><code class="language-go">// data/string/main.go
package main

func main() {
	var a = "hello"
	println(a)
}

</code></pre>
<ul>
<li>
<p>将<code>"hello"</code>字符串加载到寄存器<code>R0</code>中</p>
</li>
<li>
<p>随后将立即数5，也就是我们程序字符串的长度加载到寄存器<code>R0</code>中</p>
</li>
<li>
<p><code>R0, main.a-16(SP)</code>和<code>R0, main.a-8(SP)</code>也就是它们相隔的位置，如图所示</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F06.BA6uqixP.png&amp;fm=webp&amp;w=800&amp;h=490&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
</li>
</ul>
<pre><code class="language-assembly">	0x0018 00024 	MOVD	$go:string."hello"(SB), R0 # 这里仅仅是地址
	0x0020 00032 	MOVD	R0, main.a-16(SP)
	0x0024 00036 	MOVD	$5, R0
	0x0028 00040 	MOVD	R0, main.a-8(SP)
</code></pre>
<p>通过汇编也可以看到Go中的字符串结构是很简单的</p>
<pre><code class="language-go">type stringStruct struct {
	str unsafe.Pointer
	len int
}
</code></pre>
<h4>stirng作为参数传递</h4>
<p>如果问你在Go中字符串是如何进行参数传递的呢？这个时候只需要写一个简单的测试done，关注<strong>重点</strong>的汇编部分就可以解答了</p>
<pre><code class="language-go">// data/string/string_param.go
package main

func foo(str string) {
	println("foo =&gt; " + str)
	str = "hello-golang"
	println("foo =&gt; " + str)
}

func main() {
	var a = "hello"
	println("main =&gt; " + a)
	foo(a)
	println("main =&gt; " + a)
}
</code></pre>
<p>十分清楚的可以看到，将<code>main.a-48(SP)</code>和<code>main.a-40(SP)</code>（就是前面分析的字符串的值和长度）分别拷贝到<code>R0</code>和<code>R1</code>寄存器中；最后调用<code>foo</code>函数。</p>
<p>因此可以说明**<code>string</code>作为参数传递是会拷贝一份的，但是注意底层数组是不是拷贝的**</p>
<pre><code class="language-assembly">	0x0064 00100 	MOVD	main.a-48(SP), R0
	0x0068 00104 	MOVD	main.a-40(SP), R1
	0x006c 00108 	CALL	main.foo(SB)
</code></pre>
<h3>Array</h3>
<p>日常在Golang开发中Array使用的其实并不多，更多作为一个底层的实现。array的结构是不需要长度这个字段的，因为它是定长的，也就是说它在编译期就能够确定长度是一个连续的内存区域。</p>
<p>那么就来验证一下是不是真的如上所说</p>
<pre><code class="language-go">// data/array/main.go
package main

func main() {
	array := [5]int{10, 1, 2, 4, 5}
	_ = array
}

</code></pre>
<ul>
<li><strong>LDP</strong>：是 ARM 架构的 “Load Pair” 指令，表示加载两个连续的值，到寄存器对 (R0, R1) 中。</li>
<li>总结一下这里的操作都是在获取值并且存入到寄存器中，并没有向string初始化到时候看到有关长度的信息</li>
</ul>
<pre><code class="language-assembly">	0x000c 00012 	LDP	main..stmp_0(SB), (R0, R1)
	0x0018 00024 	PCDATA	$0, $-4
	0x0018 00024 	LDP	main..stmp_0+16(SB), (R2, R3)
	0x0024 00036 	PCDATA	$0, $-1
	0x0024 00036 	STP	(R0, R1), main.array-40(SP)
	0x0028 00040 	STP	(R2, R3), main.array-24(SP)
	0x002c 00044 	MOVD	$5, R0
	0x0030 00048 	MOVD	R0, main.array-8(SP)
</code></pre>
<h4>默认值</h4>
<p>相信你一定知道，在定义array的时候如果是没有进行初始化，那么默认值就是<code>0</code>，在汇编层面上处理默认值，也是会根据不同的定长，选择对应的方法。</p>
<pre><code class="language-go">array := [2]int // 在汇编中默认直接使用STP	(ZR, ZR), main.array-16(SP)进行0值的初始化

array1 := [10]int // 使用runtime.duffzero进行初始化
</code></pre>
<blockquote>
<p>duffzero也是汇编代码</p>
</blockquote>
<p>Go 编译器会插入所谓的 duffzero 函数调用，以此来提高清零的效率<a href="https://en.wikipedia.org/wiki/Duff%27s_device">达夫设备(Duff’s device)</a></p>
<h3>Slice</h3>
<p>相比于array，slice可谓是开发使用频率最高的。不同于数组单单是一块连续内存；slice支持动态扩容，所以在底层的数据结构中就会有所变化。</p>
<ol>
<li>指向底层数据的指针</li>
<li>切片长度</li>
<li>切片容量</li>
</ol>
<pre><code class="language-go">type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}
</code></pre>
<p>按照惯例看看汇编中slice是如何的</p>
<pre><code class="language-go">// data/slice/main.go
package main

func main() {
	slice := []int{1, 2, 3, 4, 5}
	_ = slice
}

</code></pre>
<p>大致总结一下步骤</p>
<ol>
<li>
<p>栈空间初始化</p>
</li>
<li>
<p>向切片赋值</p>
<ol>
<li>
<p>取地址</p>
</li>
<li>
<p>设置值</p>
</li>
<li>
<p>写入对应偏移量</p>
</li>
</ol>
</li>
<li>
<p>构造切片结构（指针、长度、容量）</p>
</li>
<li>
<p>完成切片初始化</p>
</li>
</ol>
<pre><code class="language-assembly">	# 栈空间初始化
	0x000c 00012 	STP	(ZR, ZR), main..autotmp_2-72(SP)
	0x0010 00016 	STP	(ZR, ZR), main..autotmp_2-56(SP)
	0x0014 00020 	MOVD	ZR, main..autotmp_2-40(SP)
	
	0x0018 00024 	MOVD	$main..autotmp_2-72(SP), R0
	0x001c 00028 	MOVD	R0, main..autotmp_1-32(SP)
	0x0020 00032 	PCDATA	$0, $-2
	0x0020 00032 	MOVB	(R0), R27
	0x0024 00036 	PCDATA	$0, $-1
	
	# 常量1
	0x0024 00036 	MOVD	$1, R1
	0x0028 00040 	MOVD	R1, (R0)
	
	0x002c 00044 	PCDATA	$0, $-2
	0x002c 00044 	MOVB	(R0), R27
	0x0030 00048 	PCDATA	$0, $-1
	
	# 常量2
	0x0030 00048 	MOVD	$2, R1
	0x0034 00052 	MOVD	R1, 8(R0)
	
	0x0038 00056 	PCDATA	$0, $-2
	0x0038 00056 	MOVB	(R0), R27
	0x003c 00060 	PCDATA	$0, $-1
	
	# 常量3
	0x003c 00060 	MOVD	$3, R1
	0x0040 00064 	MOVD	R1, 16(R0)
	
	0x0044 00068 	PCDATA	$0, $-2
	0x0044 00068 	MOVB	(R0), R27
	0x0048 00072 	PCDATA	$0, $-1
	
	# 常量4
	0x0048 00072 	MOVD	$4, R1
	0x004c 00076 	MOVD	R1, 24(R0)
	
	0x0050 00080 	MOVD	main..autotmp_1-32(SP), R0
	0x0054 00084 	PCDATA	$0, $-2
	0x0054 00084 	MOVB	(R0), R27
	0x0058 00088 	PCDATA	$0, $-1
	
	# 常量5
	0x0058 00088 	MOVD	$5, R1
	0x005c 00092 	MOVD	R1, 32(R0)
	0x0060 00096 	MOVD	main..autotmp_1-32(SP), R0
	0x0064 00100 	PCDATA	$0, $-2
	0x0064 00100 	MOVB	(R0), R27
	0x0068 00104 	PCDATA	$0, $-1
	0x0068 00104 	JMP	108
	
	# 构造切片结构（指针、长度、容量）
	0x006c 00108 	MOVD	R0, main.slice-24(SP) # 指向底层数组的地址（R0）
	0x0070 00112 	MOVD	R1, main.slice-16(SP) # 长度
	0x0074 00116 	MOVD	R1, main.slice-8(SP)	# 容量

</code></pre>
<h2>解开魔法 🪄</h2>
<p>最后再来谈谈，掌握基础的汇编能给我们带来什么；不知道你在阅读的时候，会不会疑惑一些概念的真伪性或者说想去求证它。</p>
<p>这个时候当然是可以通过代码done的方式去验证；或者我们还可以使用汇编的方式去进行求证，以下案例是我在阅读《Go语言高级编程》所疑惑的</p>
<h3>案例一</h3>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F07.CqtRxRaj.png&amp;fm=webp&amp;w=800&amp;h=261&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
<ul>
<li>代码方式进行验证</li>
</ul>
<pre><code class="language-go">package main

import "fmt"

func main() {
	array := [...]int{1, 2, 3}
	array2 := array
	array2[0] = 100
	fmt.Println(array)
	fmt.Println(array2)
}
</code></pre>
<ul>
<li>通过汇编的方式，我们只需要关注<code>array2:=array</code>的汇编代码
<ul>
<li>很简单其实就是，这些指令将寄存器中的数据（如 R0, R1, R2）分别存储到栈上的不同位置，通常在处理数组或切片时，可能是将数组或切片的多个元素（或其它结构）存入栈空间。</li>
</ul>
</li>
</ul>
<pre><code class="language-assembly">	0x002c 00044 	MOVD	R0, main.array2-48(SP)
	0x0030 00048 	MOVD	R1, main.array2-40(SP)
	0x0034 00052 	MOVD	R2, main.array2-32(SP)
</code></pre>
<p>那么再来对比一下<code>array2:=&amp;array</code>吧</p>
<ol>
<li><code>MOVD $main.array-32(SP), R0</code>将<code>array</code>的「地址」存储到寄存器<code>R0</code>中。</li>
<li><code>R0</code>的值保存到<code>main.array2</code>到栈位置（也就是说array2 只是存储了 array 的地址，它指向的是同一块内存空间）</li>
</ol>
<pre><code class="language-assembly">	0x0024 00036 	MOVD	$main.array-32(SP), R0
	0x0028 00040 	MOVD	R0, main.array2-8(SP)
	0x002c 00044 	PCDATA	$0, $-2
</code></pre>
<h3>案例二</h3>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F08.BUhBpZJv.png&amp;fm=webp&amp;w=800&amp;h=223&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
<pre><code class="language-go">package main

func main() {
	array := [0]int{}
	_ = array
}

</code></pre>
<p>不难发现，对于零长度的数组，Go 编译器通常会优化掉数组的实际分配，因此在汇编代码中看不到该数组的相关内容。</p>
<pre><code class="language-assembly">main.main STEXT size=16 args=0x0 locals=0x0 funcid=0x0 align=0x0 leaf
	0x0000 00000 	TEXT	main.main(SB), LEAF|NOFRAME|ABIInternal, $0-0
	0x0000 00000 	FUNCDATA	$0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
	0x0000 00000 	FUNCDATA	$1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
	0x0000 00000 	RET	(R30)
	0x0000 c0 03 5f d6 00 00 00 00 00 00 00 00 00 00 00 00  .._.............
</code></pre>
<h2>最后</h2>
<p>通过汇编的方式，我们可以更加深入的了解Go语言的底层实现，也可以通过汇编的方式去验证一些疑惑的问题。当然，这里只是简单的介绍算是进行抛砖引玉。</p>
<p>后续可以关注此<a href="https://github.com/AnnularLabs/plan9-golang">repo</a>，会不定期更新一些有趣的案例，也欢迎大家一起探讨～</p>
<h2>参考</h2>
<p><a href="https://xargin.com/go1-17-new-calling-convention/">https://xargin.com/go1-17-new-calling-convention/</a></p>
<p><a href="https://en.wikipedia.org/wiki/X86_calling_conventions">https://en.wikipedia.org/wiki/X86_calling_conventions</a></p>
<p><a href="https://taoshu.in/go/duff-zero.html">https://taoshu.in/go/duff-zero.html</a></p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[2024 💃]]></title>
            <link>https://agility6.me/2024</link>
            <guid isPermaLink="false">https://agility6.me/2024</guid>
            <pubDate>Wed, 01 Jan 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Ready for 2025 💃 时间来到2024的末尾，年度总结也如约而至这几天不断的回忆；2024年我做了什么，或者说这一年我可以用什么词进行概括呢？最终选择这三个词来概括整一个2024年 校园 实习 选择 在最最最开始的时候先让「科技」告诉我2024年做了什么吧 (Github!!!) (Top应用居然是百度网盘，批评居然不是VsCode/Goland😢) (b站大学) 抓住校园的尾巴 ...]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Ready for 2025 💃</p>
</blockquote>
<p>时间来到2024的末尾，年度总结也如约而至这几天不断的回忆；2024年我做了什么，或者说这一年我可以用什么词进行概括呢？最终选择这三个词来概括整一个2024年</p>
<ol>
<li>校园</li>
<li>实习</li>
<li>选择</li>
</ol>
<hr />
<p><strong>在最最最开始的时候先让「科技」告诉我2024年做了什么吧</strong></p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F01.BnnWa8hu.png&amp;fm=webp&amp;w=800&amp;h=427&amp;dpl=69dce8926406240008354a25" alt="photo" />
(Github!!!)</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F03.CQht8N6p.png&amp;fm=webp&amp;w=800&amp;h=541&amp;dpl=69dce8926406240008354a25" alt="photo" />
(Top应用居然是百度网盘，批评居然不是<strong>VsCode/Goland</strong>😢)</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F02.AhYk-Dji.png&amp;fm=webp&amp;w=800&amp;h=1386&amp;dpl=69dce8926406240008354a25" alt="photo" />
(b站大学)</p>
<h2>抓住校园的尾巴 🎉</h2>
<p>这一年虽然名义上我还是一名大学生，但是自我的定位已经是半只脚踏出校园的人了。回想24年春节的时候，给自己下达的目标就是暑假前落实实习。所以在大三下学期的时候，并没有把太多精力花在学校中（<s>意思是经常逃课</s>）不过还是在大三下的时候参加了一些活动</p>
<ul>
<li>
<p>计算机程序设计大赛</p>
</li>
<li>
<p>蓝桥杯算法比赛</p>
</li>
</ul>
<p>虽然结果不太尽人意，计算机程序设计大赛止步于省赛；蓝桥杯省二。现在回想起来其实并不是获奖最开心（奖也很重要‼️）最容易回忆的可能永远都是<strong>过程</strong></p>
<p>对于计算机程序设计大赛团队赛来说，也是我第一次做出突破</p>
<ul>
<li>第一次以队长的身份参加比赛</li>
<li>第一次尝试比赛路演汇报</li>
</ul>
<p>总之算是突破了自己的舒适圈，尝试了一些新的挑战；十分感谢我的队员们EchoJob（很好听的队名🌱）和指导老师!!!（帮忙找字节的师兄帮我们出谋划策）</p>
<hr />
<h2>实习 💼</h2>
<p>回想23年是我第一次跨出校园参加实习，给我的体验更多是第一次的不习惯和身份的转变，那么24的两段实习相比于23年我又有什么成长呢</p>
<ol>
<li>
<p>心态上，相比于23年我可以更加从容面对实习的工作内容（虽然时常还是感到焦虑）</p>
</li>
<li>
<p>个人成长上，我更加明确自己是热爱计算机行业</p>
</li>
</ol>
<p>在5月份的时候开始集中投递简历，学历和环境决定了求职的困难度，在开始的时候已经做好了处处碰壁的心态了。</p>
<p>经历「未读」「已读」「已读不回」这三个状态下反复横跳，同时也在不断的自我调整（催眠）下，慢慢地有一些面试的机会，比较幸运的是在6月中旬比较顺利的拿到实习offer，第二段实习的持续到10月份。同月我也迎来了第三段实习，持续进行中～</p>
<p>回看这几段实习经历，很高兴的是我做到了，从一个小公司到中型公司，最后到目前所在的公司一个更加大的平台。</p>
<hr />
<h2>选择 🤔</h2>
<p>最后，我想24年最重要的一个关键词就是「选择」也想谈谈对「选择」的理解</p>
<p>关于选择，很多时候从「当下」时间点来看，会认为这次的选择是正确的；但是从选择后的某个时间段，总会去怀疑自己的选择是否是最最优的。(取决于「当下的状态」)</p>
<blockquote>
<p>如果选择是自己评估后所决定的，永远不要怀疑它，因为是结合你当下最优的选择*</p>
</blockquote>
<p>每次选择可能都是环环相扣的，在24年面临的最大选择就是，「机会」和「未知」的抉择。</p>
<ul>
<li>
<p>选择「挑战」</p>
<p>在第二段实习中，很意外获得了提前转正的机会，但是结合技术栈和自己的发展方向，我开始犹豫是否要选择这个机会。询问身边的朋友，都给出了接受转正offer；印象很深的是，一张纸一支笔我写下了所有的利与弊选择了放弃。</p>
<p>在一段时间里我也经常怀疑自己是不是做错了选择，正如上面所说，当发生不符合预期的时候，总是会怀疑自己的选择。</p>
</li>
<li>
<p>选择「不确定」</p>
<p>10月份加入sl已经两个多月了，大致可以总结一下我的心路历程「开心 -&gt; 害怕 -&gt; 迷茫 -&gt; 不确定 -&gt; 重新规划」，面对「不确定」唯一能做的就是做好准备迎接未知。</p>
</li>
</ul>
<hr />
<h2>一些 😶</h2>
<p>一些比较印象深刻的事情/感悟</p>
<ul>
<li>
<p>九月份和初中的朋友见面，突然发现我们讨论的话题已经从无忧无虑到各自的工作、创业以及结婚等话题了。</p>
</li>
<li>
<p>今年也算是尝试了用🍠进行社交，算是一个以前一直不敢尝试的社交方式。</p>
</li>
<li>
<p>实习过程中相识新的好朋友，和优秀（<s>抽象😋</s>）的人在一起也会不由自主的向前追赶。</p>
</li>
<li>
<p>疑犯追踪，年度美剧!</p>
</li>
<li>
<p>成为一个Gopher!!!（虽然很菜）</p>
</li>
<li>
<p>…</p>
</li>
</ul>
<hr />
<h2>Ready for 2025 💃</h2>
<p>24年流水账到这里就结束了，25年也意味着大学即将结束，在充满着未知的挑战下，希望我能够继续坚定的往下走</p>
<h3>技术方向的思考</h3>
<p>希望25年能够朝着以下方向努力</p>
<ul>
<li>计算机基础知识</li>
<li>业务「领域」问题解决方案</li>
<li>业务「技术」问题解决方案</li>
<li>热门中间件的原理</li>
</ul>
<blockquote>
<p>flag 🚩 2025年决定尝试开启一个repo用于记录!
<img src="https://agility6.me/.netlify/images?url=_astro%2F07.DxuxCEvp.png&amp;fm=webp&amp;w=800&amp;h=448&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
</blockquote>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Plan9 & Go Assembler]]></title>
            <link>https://agility6.me/plan9goassembler</link>
            <guid isPermaLink="false">https://agility6.me/plan9goassembler</guid>
            <pubDate>Fri, 20 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Plan 9 &amp; Go Plan 9 Assembler 基础 Rob_Pike多次在Golang的会议提到，Go语言的设计理念追求简洁、清晰和高效。在使用的层面上Go语言确实能够给人一种简单的感觉，但是当问出「为什么」的时候，这些简单就会变成黑魔法。因此本篇文章是作为揭开Golang外衣窥探黑魔法的基石。 在计算机里头没有任何黑魔法！— 浙江大学翁恺 学习它你能得到什么？！ 能够揭开语法...]]></description>
            <content:encoded><![CDATA[<h2>Plan 9 &amp; Go Plan 9 Assembler 基础</h2>
<p><a href="https://en.wikipedia.org/wiki/Rob_Pike">Rob_Pike</a>多次在Golang的会议提到，Go语言的设计理念追求<strong>简洁、清晰和高效</strong>。在使用的层面上Go语言确实能够给人一种简单的感觉，但是当问出「为什么」的时候，这些简单就会变成黑魔法。因此本篇文章是作为揭开Golang外衣窥探黑魔法的基石。</p>
<blockquote>
<p>在<em>计算机</em>里头<em>没有</em>任何黑<em>魔法</em>！— 浙江大学翁恺</p>
</blockquote>
<p>学习它你能得到什么？！</p>
<ol>
<li>
<p>能够揭开语法糖底层是如何运作的</p>
</li>
<li>
<p>以不变应万变或许你还可以尝试分析其他语言是如何做的</p>
</li>
<li>
<p><strong>在与对方进行对线的时候能够有理有据（Important‼️）</strong></p>
</li>
</ol>
<h2>什么是 Go Plan 9 Assembler</h2>
<blockquote>
<p>The assembler is based on the input style of the Plan 9 assemblers — <a href="https://go.dev/doc/asm">https://go.dev/doc/asm</a></p>
</blockquote>
<p>Go语言的Go Assembler（汇编器）是基于Plan 9风格进行实现（TOP级程序员们选择自己搞一套汇编器～）保留了许多Plan 9的汇编的语法和特性，在某些地方增加或者简化了，目的自然就是为了Go语言的特性如并发、垃圾回收、内存管理。</p>
<p>Plan 9是来自于大名鼎鼎的<a href="https://en.wikipedia.org/wiki/Bell_Labs">贝尔实验室(Bell Labs)</a>，你所熟悉的<a href="https://en.wikipedia.org/wiki/Unix">Unix</a>和C/C++都是出自该实验室，Plan 9其实一个分布式操作系统。其中<a href="https://en.wikipedia.org/wiki/Rob_Pike">Rob_Pike</a>也是Plan 9的团队成员（下图是Plan9的吉祥物和Gopher有几分相似）</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F01.BVLdtkgn.png&amp;fm=webp&amp;w=800&amp;h=1040&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
<p>需要注意的是Plan 9是一个操作系统，其中它有一套自己的汇编指令也就是Plan 9汇编。虽然在Go中可以使用Plan 9汇编进行编码，但是在最终的执行上还是需要翻译成对应平台的汇编，这个过程是通过Go工具链。</p>
<hr />
<h2>Plan 9 &amp; Go Plan 9 Assembler 知识点</h2>
<p>为了区分这里用Go Assembler来指代Go风格的Plan 9，下面将会从以下几个方面来介绍Plan 9汇编基础（如果你想知道更多可参考<a href="https://9p.io/sys/doc/asm.html"><strong>A Manual for the Plan 9 assembler</strong></a>）。</p>
<ol>
<li>
<p>基本指令</p>
</li>
<li>
<p>寄存器</p>
</li>
<li>
<p>变量声明</p>
</li>
<li>
<p>函数声明</p>
</li>
<li>
<p>栈结构</p>
</li>
</ol>
<blockquote>
<p>‼️ 目标是能够在后续分析的时候看懂即可</p>
</blockquote>
<h3>基本指令</h3>
<h4>数据移动</h4>
<p>使用<code>MOV</code>指令进行，例如<code>MOVB</code>后缀则是表示长度，<code>$</code>表示常数字，并且可以为负数</p>
<pre><code>MOVQ $-10, AX     // 8 bytes
</code></pre>
<p>需要注意以下Plan 9与其他汇编操作数是相反的</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F02.C25uFqWc.png&amp;fm=webp&amp;w=800&amp;h=415&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
<h4>计算指令</h4>
<ul>
<li>
<p><code>ADDQ</code>：相加并赋值，例如 <code>ADDQ BX, AX</code> 表示<code>BX</code>和<code>AX</code>的值相加并赋值给<code>AX</code></p>
</li>
<li>
<p><code>SUBQ</code>：相减并赋值，例如 <code>SUBQ AX, BX</code> 表示<code>AX</code>和<code>BX</code>的值相减并赋值给<code>BX</code></p>
</li>
<li>
<p><code>IMULQ</code>：无符号乘法，例如 <code>IMULQ AX, BX</code> 表示<code>AX</code>和<code>BX</code>的值相乘并赋值给<code>BX</code></p>
</li>
<li>
<p><code>IDIVQ</code>：无符号除法，例如 <code>IDIVQ CX</code> 表示用<code>CX</code>作为除数，<code>AX</code>作为被除数，结果存储到<code>AX</code>中</p>
</li>
<li>
<p><code>CMPQ</code>：对两数相减，比较大小，例如 <code>CMPQ SI, CX</code> 表示比较<code>SI</code>和<code>CX</code>的大小。与<code>SUBQ</code>类似，只是不返回相减的结果</p>
</li>
</ul>
<p>同样后缀表示的是不同长度的操作数</p>
<h4>条件跳转</h4>
<p>跳转可以分为无条件和有条件的跳转，分别是<code>JMP</code>和<code>JZ</code></p>
<pre><code>无条件跳转
JMP addr   // addr 为代码的地址
JMP 2(PC)  // 以当前指令为基础，向前/后跳转 ==&gt; 同理 JMP -2(PC) 
===========================================================
有条件跳转
JZ target
</code></pre>
<h4>栈调整</h4>
<p>Plan 9的<code>push</code>和<code>pop</code>指令通常在生成汇编的时候是不存在的，你将会看到使用<code>SP</code>进行运算实现。</p>
<blockquote>
<p>‼️ 注意区分Go Assembler对伪寄存器<code>SP</code>，Plan 9的<code>SP</code>是硬件寄存器直接由汇编指令操作（函数栈真实栈顶地址）</p>
</blockquote>
<pre><code>SUBQ $0x1, SP   // 对 SP 做减法，为函数分配函数栈帧
ADDQ $0x1, SP   // 对 SP 做加法，清除函数栈帧
</code></pre>
<h3>寄存器</h3>
<p>对于amd64的通用寄存器在Plan 9汇编都是可以正常使用的，需要注意的是在Plan 9中使用寄存器是不带名字的首字母，例如<code>rax</code>在Plan 9则为<code>AX</code>。</p>
<p>还记得前面栈调整的时候说到，进行栈调整使用的是<code>SP</code>，那么在使用寄存器的时候应该避免使用<code>rbp</code>和<code>rsp</code>。</p>
<h4>伪寄存器</h4>
<blockquote>
<p>‼️ 这个是由Go Assembler所定义的，注意对于伪寄存器所有的平台架构都是相同的</p>
</blockquote>
<ul>
<li>
<p><code>FP</code>: Frame pointer: arguments and locals. 用来表示函数的参数、返回值</p>
</li>
<li>
<p><code>PC</code>: Program counter: jumps and branches. 程序计数器，它与体系结构中的<code>PC</code>类似，通常都是出现在<strong>跳转指令</strong>，例如<code>JMP target</code>无条件跳转到target标签中，这里就是Go Assembler的<code>PC</code>d的一个象征</p>
</li>
<li>
<p><code>SB</code>: Static base pointer: global symbols. 静态基地址指针，一般用来声明函数或全局变量</p>
</li>
<li>
<p><code>SP</code>: Stack pointer: the highest address within the local stack frame. 用于指向当前栈帧的局部变量的开始位置，形式如<code>symbol + offset(SP)</code>。如何区分硬件寄存器SP和伪寄存器呢？看是否带<code>symbol</code></p>
</li>
</ul>
<p>一些注意点，在<code>go tool objdump/go tool compile -S</code>等指令输出的代码，是不存在伪<code>SP</code>和<code>FP</code>的，在编译和反编译下只有真实的硬件寄存器</p>
<h3>变量声明</h3>
<p>在汇编中变量通常是存在在<code>.rodata</code>或者是<code>.data</code>段中</p>
<ul>
<li>
<p><code>.data</code>段存储可修改的变量如全局变量、静态变量</p>
</li>
<li>
<p><code>.rodata</code>段存储程序的常量，在加载的时候会标记为<strong>只读</strong>，例如字符串字面量或其他数据。</p>
</li>
</ul>
<p><code>DATA</code>和<code>GLOBL</code>指令是用于定义变量</p>
<ul>
<li>
<p><code>DATA</code> 用来定义一个数据段，用于声明和初始化全局数据；</p>
</li>
<li>
<p><code>GLOBL</code>用来声明全局符号，使得该符号可以在其他模块中访问。</p>
</li>
</ul>
<p>需要注意的是<code>offset</code>偏移指的是<code>symbol</code>的偏移，这里可以看到一个熟悉的伪寄存器<code>SB</code>其表示的就是，数据在内存中的位置</p>
<pre><code>DATA symbol+offset(SB)/width, value
</code></pre>
<p>用于声明全局符号，并且指定在内存的位置、类型以及大小，这里的<code>RODATA</code>表示是一种<code>flag</code>还有其他的类型</p>
<pre><code>GLOBL divtab(SB), RODATA, &amp;1
</code></pre>
<p><code>DATA</code>与<code>GLOBL</code>两者结合</p>
<ul>
<li>
<p><code>DATA</code>首先在<code>age</code>变量的起始位置存储值<code>1</code>，同时数据的大小为<code>4</code>字节</p>
</li>
<li>
<p><code>GLOBL</code>声明了全局符号<code>age</code>，大小为<code>4</code>字节，并且是只读属性，这个数据项被初始化为<code>1</code>，并且存在<code>0x00</code>内存地址中</p>
</li>
</ul>
<pre><code>DATA age+0x00(SB)/4, $1
GLOBL age(SB), RODATA, $4
</code></pre>
<p>偏移量<code>offset</code> 什么时候为非0呢？当在全局中定义变量类型为数组、字符串等时候就需要使用到<code>offset</code>。这里的<code>&lt;&gt;</code>可以理解为该全局变量只在当前文件中生效。</p>
<pre><code>DATA bio&lt;&gt;+0(SB)/8, $"hello"
DATA bio&lt;&gt;+8(SB)/8, $" world"
GLOBL bio&lt;&gt;(SB), RODATA, $16
</code></pre>
<p>其中<code>flag</code>还有其他类型<a href="https://github.com/golang/go/blob/master/src/runtime/textflag.h">🔗</a></p>
<pre><code class="language-c">// Don't profile the marked routine. This flag is deprecated.
#define NOPROF	1
// It is ok for the linker to get multiple of these symbols. It will
// pick one of the duplicates to use.
#define DUPOK	2
// Don't insert stack check preamble.
#define NOSPLIT	4
// Put this data in a read-only section.
#define RODATA	8
// This data contains no pointers.
#define NOPTR	16
// This is a wrapper function and should not count as disabling 'recover'.
#define WRAPPER 32
// This function uses its incoming context register.
#define NEEDCTXT 64
// Allocate a word of thread local storage and store the offset from the
// thread local base to the thread local storage in this variable.
#define TLSBSS	256
// Do not insert instructions to allocate a stack frame for this function.
// Only valid on functions that declare a frame size of 0.
#define NOFRAME 512
// Function can call reflect.Type.Method or reflect.Type.MethodByName.
#define REFLECTMETHOD 1024
// Function is the outermost frame of the call stack. Call stack unwinders
// should stop at this function.
#define TOPFRAME 2048
// Function is an ABI wrapper.
#define ABIWRAPPER 4096
</code></pre>
<h3>函数声明</h3>
<p>函数的声明以<code>TEXT</code>标识开头，下面来看看一个函数的声明在汇编是怎么样的并且尝试进行分析吧</p>
<pre><code>func add(a, b int) int {
	return a + b
}
===========================================================
TEXT pkgname·add(SB), NOSPLIT, $0-8
    MOVQ a+0(FP), AX
    MOVQ b+8(FP), BX
    ADDQ AX, BX
    MOVQ BX, ret+16(FP)
    RET
</code></pre>
<p><strong>由于 Go 是栈帧管理的语言</strong>，栈帧中通常包含函数参数、局部变量等</p>
<p>函数参数：</p>
<ul>
<li>
<p><code>a</code>是<code>int</code>类型，位于<code>a+0(FP)</code>。由于<code>int</code>在 64 位架构中占 8 字节，所以<code>a</code>在栈帧中的偏移量是0</p>
</li>
<li>
<p><code>b</code>也是<code>int</code>类型，位于<code>b+8(FP)</code>，因此偏移量是 8 字节</p>
</li>
</ul>
<ol>
<li><code>TEXT pkgname·add(SB), NOSPLIT, $0-8</code></li>
</ol>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F03.YlAdr7X1.png&amp;fm=webp&amp;w=800&amp;h=335&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
<ol>
<li>
<p><code> MOVQ a+0(FP), AX</code>：取<code>a</code>参数的值，<code>a</code>是从栈中<code>offset=0</code>处进行读取，栈帧指针<code>FP</code>加上偏移量<code>0</code>得到<code>a</code>的位置，<code>AX</code>是一个寄存器</p>
</li>
<li>
<p><code>MOVQ b+8(FP)BX</code>和上面的分析是同理的</p>
</li>
<li>
<p><code>ADDQ AX, BX</code>将<code>AX</code>寄存器的值加到<code>BX</code>寄存器中，实现<code>b = a + b</code></p>
</li>
<li>
<p>`MOVQ BX, ret+16(FP)将 BX 寄存器的值（即 a + b 的结果）存储到栈上的返回值位置。通常返回值会存放在栈帧中的 ret 位置，ret+16(FP) 表示返回值在栈中的偏移量</p>
</li>
<li>
<p>结束</p>
</li>
</ol>
<p>最后再来回顾一下上面所使用到Go Assembler的伪寄存器</p>
<ul>
<li>
<p>FP（Frame Pointer）：伪寄存器，指向当前栈帧的基址。它用于定位栈中的局部变量和参数</p>
</li>
<li>
<p>SB（Stack Base）：伪寄存器，表示栈的基址，在函数调用中通常用于标识栈的起始位置</p>
</li>
</ul>
<h3>栈结构</h3>
<p>前面也有说到<strong>由于 Go 是栈帧管理的语言</strong>，因此了解栈帧的结构对后续是十分重要的，那么首先先来明确几个专业术语吧</p>
<ul>
<li>
<p>栈帧：是每一个函数在调用的过程中创建的一块区域，用于保存局部信息、返回地址等</p>
</li>
<li>
<p>调用者：caller这个很好理解，就是谁做出了调用的动作</p>
</li>
<li>
<p>被调用者：callee</p>
</li>
</ul>
<p>以下是具体的栈帧布局，对于伪寄存器<code>SP</code>可以理解为用于模拟指针的变化过程，会随着栈帧的压栈和弹栈而动态变化</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F04.Cm4qahso.png&amp;fm=webp&amp;w=800&amp;h=523&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
<p>可以抽象出大致的布局，其实就是由「局部变量」、「参数」、「返回值」进行组合而成的。</p>
<p>同时理解<code>call</code>指令只会做两件事情</p>
<ol>
<li>
<p>将下一条指令的地址入栈，被调用函数执行结束后会跳回到「返回地址」</p>
</li>
<li>
<p>跳转到被调用函数入口处执行</p>
</li>
</ol>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F05.Dbvd9vK0.png&amp;fm=webp&amp;w=800&amp;h=432&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
<blockquote>
<p>‼️  需要注意在Golang早期的版本中，参数都是通过栈进行传入的，后续也支持了通过寄存器进行传入。</p>
</blockquote>
<p>那么接下来用一个例子来具体看看函数调用者与被调用者的栈帧图</p>
<pre><code class="language-go">func main() {
	a := 1
	b := 2
	add(a, b)
}

func add(a, b int) int {
	sum := a + b
	return sum
}
</code></pre>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F06.BW0OuWrb.png&amp;fm=webp&amp;w=800&amp;h=524&amp;dpl=69dce8926406240008354a25" alt="image" /></p>
<h2>总结 &amp; 计划</h2>
<p>现在你应该明白了Plan 9的一些历史，以及Go Assembler与Plan 9的关系。</p>
<p>Plan 9的汇编对于现阶段只需要点到为止，后续如果你感兴趣可再深入进行学习。作为应用层开发者，暂时并不需要太过深究里面的细节，掌握基本的知识点后续在进行分析的时候能够看懂足以。</p>
<p>后续将会基于这些基础知识逐步转移到应用层代码进行探索与分析。</p>
<h1># 参考</h1>
<ol>
<li><a href="https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs">https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs</a></li>
<li><a href="https://en.wikipedia.org/wiki/Rob_Pike">https://en.wikipedia.org/wiki/Rob_Pike</a></li>
<li><a href="https://go.dev/doc/asm#special-instructions">https://go.dev/doc/asm#special-instructions</a></li>
<li><a href="https://go.dev/wiki/Plan9">https://go.dev/wiki/Plan9</a></li>
<li><a href="https://golang.design/under-the-hood/zh-cn/part1basic/ch01basic/asm/">https://golang.design/under-the-hood/zh-cn/part1basic/ch01basic/asm/</a></li>
<li><a href="https://xiaomi-info.github.io/2019/11/27/golang-compiler-plan9/">https://xiaomi-info.github.io/2019/11/27/golang-compiler-plan9/</a></li>
<li><a href="https://github.com/yangyuqian/technical-articles/blob/master/asm/golang-plan9-assembly-cn.md">https://github.com/yangyuqian/technical-articles/blob/master/asm/golang-plan9-assembly-cn.md</a></li>
<li><a href="https://github.com/cch123/golang-notes/blob/master/assembly.md#plan9-assembly-%E5%AE%8C%E5%85%A8%E8%A7%A3%E6%9E%90">https://github.com/cch123/golang-notes/blob/master/assembly.md#plan9-assembly-完全解析</a></li>
</ol>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Golang Slice Trick]]></title>
            <link>https://agility6.me/slicetrick</link>
            <guid isPermaLink="false">https://agility6.me/slicetrick</guid>
            <pubDate>Sat, 30 Nov 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[前言 本篇文章是根据Go官网的一个有趣的Wiki，Golang一直都是以简单著称的，如果你是一个Java选手，那么一定会给眼花缭乱的库所征服，会封装一切你能用到的方法。 这个图十分有趣（Everything is 「for」） 尽管Golang并没有给开发者封装太多的方法，反而是通过一些Tricks可以实现你想要达到的效果 这种设计仅仅是一种选择而已，并没有绝对的正确！ 下面就来看看官方给介绍的一...]]></description>
            <content:encoded><![CDATA[<h2>前言</h2>
<p>本篇文章是根据Go官网的一个有趣的<a href="https://go.dev/wiki/SliceTricks">Wiki</a>，Golang一直都是以简单著称的，如果你是一个Java选手，那么一定会给眼花缭乱的库所征服，会封装一切你能用到的方法。</p>
<p>这个图十分有趣（<strong>Everything is 「for」</strong>）</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F07.0vwrOIWM.png&amp;fm=webp&amp;w=800&amp;h=944&amp;dpl=69dce8926406240008354a25" alt="IMG_5226" /></p>
<p>尽管Golang并没有给开发者封装太多的方法，反而是通过一些Tricks可以实现你想要达到的效果</p>
<blockquote>
<p>这种设计仅仅是一种选择而已，并没有绝对的正确！</p>
</blockquote>
<p>下面就来看看官方给介绍的一些Tricks吧</p>
<h2>Slice</h2>
<p>大部分的时候使用slice都是和<code>append</code>和<code>copy</code>打交道，官方也是这样推荐的</p>
<h3>基础使用</h3>
<h4>Copy复制</h4>
<p>先来看看如果需要复制slice，可以使用什么方法</p>
<pre><code class="language-go">func main() {
  a := []int{1,2,3}
  b := make([]int, len(a))
  copy(b, a)
}
</code></pre>
<pre><code class="language-go\">func main() {
  a := []int{1, 2, 3, 4, 5}
  b := append([]int(nil), a...)
	b1 := append(a[:0:0], a...)
	fmt.Println(b)
	fmt.Println(b1)
}
</code></pre>
<p>这两种方式有什么区别吗？</p>
<ul>
<li><code>make + copy</code>：容量在初始化的时候定义了<code>len(a)</code>，所以说如果后续需要追加需要分配新的内存</li>
<li>而后面两种方式：因为它们通常会预留更大的容量，减少内存重新分配的次数。</li>
</ul>
<blockquote>
<p>如果没有额外的追加操作，或者追加的元素较少，make + copy 的性能可能更好。</p>
</blockquote>
<h4>Cut剪切</h4>
<pre><code class="language-go">a := []int{1, 2, 3, 4, 5}
b := []int{1, 2, 3}
c := append(a[:1], b[1:]...) // 1,2,3
</code></pre>
<h4>Delete删除</h4>
<p>万物皆可append+copy</p>
<pre><code class="language-go">a := []int{1, 2, 3, 4, 5} // 删除2
a = append(a[:1], a[2:]...) // append
a = a[:1+copy(a[1:], a[2:])]

</code></pre>
<p><code>a = a[:i+copy(a[i:], a[i+1:])]</code> 假设你需要删除的索引为<code>i</code></p>
<ul>
<li><code>a[i+1:]</code> 表示从索引i+1开始到切片尾部的部分</li>
<li>移动到<code>a[i:]</code>位置，<strong>也就是覆盖索引<code>i</code>的值，所有后续的元素向前移动一个位置</strong></li>
<li><code>a[:i+copy(...)]</code>相当于将slice的长度缩短到<code>i + copy(...)</code></li>
</ul>
<hr />
<blockquote>
<p>注意：如果元素的类型为<strong>指针</strong>或者是<strong>结构体</strong>，因为需要进行垃圾回收，在Cut和Delete可能会出现内存泄露问题</p>
<ul>
<li>带有值的元素仍然被切片a的底层数组引用，代码层面上使用了delete方法进行删除，但是底层依旧被引用，如果底层数组的生命周期很长的话，那么表示出现了内存泄露</li>
</ul>
</blockquote>
<h4>解决指针或者结构体的Cut/Delete</h4>
<p>Cut</p>
<ol>
<li>首先就是copy：将从索引j开始的元素移动到索引i到位置，以覆盖被删除的区间</li>
<li>清理冗余元素，显示复制为nil，确保这些位置上不再引用原来的值</li>
<li>缩短切片的长度</li>
</ol>
<pre><code class="language-go">copy(a[i:], a[j:])
for k, n := len(a)-j+i, len(a); k &lt; n; k++ {
    a[k] = nil // or the zero value of T
}
a = a[:len(a)-j+i]
// =====================================
func main() {
    a := []*int{new(int), new(int), new(int), new(int), new(int)}
    *a[0], *a[1], *a[2], *a[3], *a[4] = 1, 2, 3, 4, 5

    i, j := 1, 3 // 删除索引区间 [1, 3) 的元素

    copy(a[i:], a[j:]) // 移动数据
    for k, n := len(a)-j+i, len(a); k &lt; n; k++ {
        a[k] = nil // 设置为 nil
    }
    a = a[:len(a)-j+i] // 调整切片长度

    fmt.Println(a) // 输出: [1 4 5]
}
</code></pre>
<p>Delete</p>
<p>和Cut同理</p>
<pre><code class="language-go">copy(a[i:], a[i+1:])
a[len(a)-1] = nil // or the zero value of T
a = a[:len(a)-1]
</code></pre>
<h3>其他</h3>
<h4>在位置i插入n个元素</h4>
<p><code>a = append(a[:i], append(make([]T, n), a[i:]...)...)</code></p>
<pre><code class="language-go">func main() {
    a := []int{1, 2, 3, 4}
    i := 2  // 插入位置
    n := 3  // 插入元素数量

    a = append(a[:i], append(make([]int, n), a[i:]...)...)

    fmt.Println(a) // 输出: [1 2 0 0 0 3 4]
}
</code></pre>
<p>如果插入的元素需要具体值，可以在make之后再赋值</p>
<pre><code class="language-go">b := make([]int, n)
for i := range b {
  b[i] = 1013
}
a = append(a[:i], append(b, a[i:]...)...)
</code></pre>
<h4>添加n个元素</h4>
<p>其实就是上面所用到的<code>a = append(a, make([]T, n)...)</code></p>
<h4>扩展容量</h4>
<p>确保有足够的空间来附加n个元素，无需重新分配</p>
<pre><code class="language-go">if cap(a)-len(a) &lt; n {
    a = append(make([]T, 0, len(a)+n), a...)
}
===========================================
a := []int{1, 2, 3}
n := 5 // 需要插入的元素个数

if cap(a)-len(a) &lt; n {
	a = append(make([]int, 0, len(a)+n), a...)
}

fmt.Println(a)       // 输出: [1 2 3]
fmt.Println(cap(a))  // 输出: len(a) + n，即 8
</code></pre>
<p>这样的好处就是避免多次扩容，在Go中当发生自动扩容的时候，通常是2倍</p>
<h4>过滤原地</h4>
<pre><code class="language-go">n := 0
for _, x := range a {
    if check(x) {
        a[n] = x
        n++
    }
}
a = a[:n]
</code></pre>
<h4>插入</h4>
<p>在slice的位置i插入一个元素x，<code>a = append(a[:i], append([]T{x}, a[i:]...)...)</code></p>
<pre><code class="language-go">func main() {
    a := []int{1, 2, 3, 4}
    i := 2
    x := 99

    a = append(a[:i], append([]int{x}, a[i:]...)...)
    fmt.Println(a) // 输出: [1 2 99 3 4]
}
</code></pre>
<p>简单的性能分析：</p>
<p>内存分配：<code>append([]T{x}, a[i:]...)</code> 分配了一个新的切片，存储插入的元素和插入位置后的元素。<code>append(a[:i], ...)</code> 分配了一个新的切片。因此一共分配了两个新的切片</p>
<p>第二个<code>append</code>创建的时候，有独自的底层存储的新slice，将<code>a[i:]</code>的元素复制到该slice中，再复制到第一个中。</p>
<p>如何避免创建新的slice呢？</p>
<pre><code class="language-go">s = append(s, 0 /* use the zero value of the element type */)
copy(s[i+1:], s[i:])
s[i] = x
================================================
func main() {
    a := []int{1, 2, 3, 4}
    i := 2
    x := 99

    a = append(a, 0)         // 扩展切片容量
    copy(a[i+1:], a[i:])     // 将索引 i 之后的元素右移
    a[i] = x                 // 插入元素

    fmt.Println(a) // 输出: [1 2 99 3 4]
}
</code></pre>
<h4>常用操作</h4>
<p>Push And Push Front/Unshift 增加元素</p>
<pre><code class="language-go">a = append(a, x)
a = append([]T{x}, a...) // 向slice头部添加元素
</code></pre>
<p>Pop And Pop Front/Shift 弹出元素</p>
<pre><code class="language-go">x, a = a[len(a)-1], a[:len(a)-1]
x, a = a[0], a[1:] // 弹出头部
</code></pre>
<h2>其他Tricks</h2>
<h4>不分配过滤</h4>
<p>核心在于<code>b := a[:0]</code>slice与原始slice共享相同的backing array和capacity。</p>
<p>看一个🌰，遍历出slice中偶数</p>
<pre><code class="language-go">func main() {
    a := []int{1, 2, 3, 4, 5, 6}

    b := a[:0] // 初始化 b，与 a 共享底层数组

    for _, x := range a {
        if x%2 == 0 { // 保留偶数
            b = append(b, x)
        }
    }

    fmt.Println("b:", b) // 输出: [2 4 6]
    fmt.Println("a:", a) // 输出: [2 4 6 4 5 6]，注意 a 的后面部分未清理
}
</code></pre>
<p>优点在于：不需要额外的分配新的切片，直接复用原切片。</p>
<p>需要注意的是：</p>
<ol>
<li>未清理的数组部分，过滤后「a的逻辑长度变短」但底层数组的其余部分保留了原始值</li>
<li>如果元素类型是指针或者结构体，可能会导致内存泄漏的问题</li>
</ol>
<blockquote>
<p>过滤后「a的逻辑长度变短」</p>
<ol>
<li>逻辑长度指的是过滤后，切片中有效数据的个数。
<ul>
<li>虽然底层数组还包含所有原始数据，但你只会通过 b 访问过滤后的前 len(b) 个数据。</li>
</ul>
</li>
<li>原始切片 a 的“逻辑长度”相当于 b 的长度，因为 b 保存的是有效数据。</li>
</ol>
</blockquote>
<blockquote>
<p>「内存泄漏的问题」</p>
<p>过滤操作并没有清除 a 的多余部分（len(b) 到 len(a) 之间的数据），这可能会导致内存泄漏问题，特别是当切片元素是指针或带有指针的复杂类型时。</p>
</blockquote>
<pre><code class="language-go">for i := len(b); i &lt; len(a); i++ {
    a[i] = 0 // 或 nil，取决于类型 T 的零值
}
</code></pre>
<h4>翻转</h4>
<p>在Golang中翻转slice应该如何做呢？相当于一个小算法，只需要遍历slice一半的元素，然后进行「对称」<code>opp := len(a) - i - 1 </code>，opp是索引<code>i</code>的对称</p>
<pre><code class="language-go">func main() {
  for i := len(a)/2-1; i &gt;= 0; i-- {
    opp := len(a)-1-i
    a[i], a[opp] = a[opp], a[i]
	}
}
</code></pre>
<p>还有最常见的双指针法</p>
<pre><code class="language-glo">for left, right := 0, len(a)-1; left &lt; right; left, right = left+1, right-1 {
    a[left], a[right] = a[right], a[left]
}
</code></pre>
<h4>Fisher–Yates（现代洗牌算法）</h4>
<p>具有等概率随机性</p>
<pre><code class="language-go">for i := len(a) - 1; i &gt; 0; i-- {
    j := rand.Intn(i + 1)
    a[i], a[j] = a[j], a[i]
}
</code></pre>
<h4>最小分配的批处理</h4>
<p>当给了一个很大的slice数据，需要分批进行处理，那么这个算法就十分有用了</p>
<pre><code class="language-tex">1.初始状态：
	actions = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
	batches = []
2.第一次循环：
	提取批次：actions[0:3] = [0, 1, 2]
	更新：
	actions = [3, 4, 5, 6, 7, 8, 9]
	batches = [[0, 1, 2]]
3.第二次循环：
	提取批次：actions[0:3] = [3, 4, 5]
	更新：
	actions = [6, 7, 8, 9]
	batches = [[0, 1, 2], [3, 4, 5]]
</code></pre>
<pre><code class="language-go">func main() {
  actions := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	batchSize := 3
	batches := make([][]int, 0, (len(actions)+batchSize-1)/batchSize)

	for batchSize &lt; len(actions) {
		actions, batches = actions[batchSize:], append(batches, actions[0:batchSize:batchSize])
	}
	batches = append(batches, actions)
	fmt.Println(batches)
}
</code></pre>
<h4>去重</h4>
<p>看图！
<img src="https://agility6.me/.netlify/images?url=_astro%2FSnipaste_2024-11-30_20-49-39.DVxuebuD.png&amp;fm=webp&amp;w=800&amp;h=446&amp;dpl=69dce8926406240008354a25" alt="Snipaste_2024-11-30_20-49-39" /></p>
<pre><code class="language-go">in := []int{3, 2, 1, 4, 3, 2, 1, 4, 1} // any item can be sorted
sort.Ints(in)
j := 0
for i := 1; i &lt; len(in); i++ {
    if in[j] == in[i] {
       continue
    }
    j++
    in[j] = in[i]
}
fmt.Println(in) // [1 2 3 4]
result := in[:j+1]
fmt.Println(result) // [1 2 3 4]
</code></pre>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Research on data consistency between MySQL and Redis]]></title>
            <link>https://agility6.me/data-consistency</link>
            <guid isPermaLink="false">https://agility6.me/data-consistency</guid>
            <pubDate>Fri, 27 Sep 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[前言 MySQL和Redis，这两个都是在项目中常用的工具，MySQL作为数据的持久化保证，虽然说在MySQL默认的存储引擎InnoDB中比较好的平衡了「高性能和高可靠」，但是如果想应对互联网的大型流量上，可能还是捉襟见肘甚至可能导致整个系统崩溃。 公认的项目中通常是「读多写少」的情况，换言之可能在大部分时候数据不是那么多变的。因此缓存的引入可以解决读多情况。 Redis是什么 这里不长篇的介绍R...]]></description>
            <content:encoded><![CDATA[<h2>前言</h2>
<p>MySQL和Redis，这两个都是在项目中常用的工具，MySQL作为数据的持久化保证，虽然说在MySQL默认的存储引擎InnoDB中比较好的平衡了「高性能和高可靠」，但是如果想应对互联网的大型流量上，可能还是捉襟见肘甚至可能导致整个系统崩溃。</p>
<p>公认的项目中通常是「读多写少」的情况，换言之可能在大部分时候数据不是那么多变的。因此缓存的引入可以解决<strong>读多</strong>情况。</p>
<h2>Redis是什么</h2>
<p>这里不长篇的介绍Redis的各个细节，我们先来讨论一下缓存是什么？缓存其实本质来说就是「空间换时间」的概念，在常见的系统设计时候，无一例外都会使用到缓存这个思想，例如解析URL的DNS服务器存在缓存、InnoDB中的Buffer Pool也是缓存的一种思想。</p>
<p>回到Redis来说，Redis单实例的读QPS可以达到10w/s，其实足以因对大部分场景了。</p>
<h2>使用缓存的思想</h2>
<p>在此之前我先简单的谈一谈，我对新技术引入的一些思考。</p>
<ol>
<li>
<p>在我看来，每一个新组件的引入可能会带来性能上的提升，同时也会带来一定的开发成本，而这一个成本就是需要对项目进行评估是非有必要。</p>
</li>
<li>
<p>确定引入新组件，那么可以从该组件本身使用上可能会出现的问题、与原来的技术结合可能会出现的问题</p>
</li>
</ol>
<p>假如项目中引入了Redis来做缓存，那么数据一致性问题一定是逃离不开的，这个就是属于<strong>新组件与原来技术结合</strong>会出现的问题。</p>
<p>前面我们说到，Redis本质上是以空间换时间的思想，那么意味着数据是存在与多个空间中。简单的来说就是，只要Redis的数据没有及时的更新而导致新数据没有同步到MySQL中，那么就出现了数据不一致。</p>
<p><strong>使用了缓存一定会出现不一致的情况，我们能做的只是将这个时间窗口尽可能小</strong>。因为MySQL和Redis不在一个事务中，无法保证两个同时成功或同时失败，<strong>如果使用分布式事务等各种手段去保证强一致，那么回头看引入缓存的目的就不谋合了</strong>。</p>
<p>说到这里就可以得出结论正对数据不一致的情况，主要把关注点放在「缩短不一致的时间窗口」 + 「确保数据的最终一致性」</p>
<h2>几种更新缓存策略</h2>
<p>先来看看通常在使用缓存的伪代码吧</p>
<ol>
<li>优先查询缓存，查询不到才查询数据库</li>
<li>缓存没数据库有，更新缓存</li>
</ol>
<pre><code class="language-java">data = getDataCache(key);
if (data == null) {
    data = getDataDB(key)
    if (data != null) {
        updateDataCache(key, data)
    }
}
</code></pre>
<p>查询的逻辑比较简单，一致性的问题通常不会出现在查询中，而是<strong>写请求中</strong>。针对查询可以去了解缓存常见的三大问题。</p>
<p>针对写请求主要可以总结为4个策略</p>
<ol>
<li>更新数据库后更新缓存</li>
<li>更新缓存后更新数据库</li>
<li>删除缓存后更新数据库</li>
<li>删除缓存前更新数据库</li>
</ol>
<h3>更新数据库后更新缓存 —— 数据不一致问题</h3>
<p>图很容易可以看到，这种情况在<strong>写-写</strong>的时候会出现数据不一致的情况。如果后续没有进行数据对齐，那么这个不一致只能等到系阿姨次数据库更新或者缓存失效才可能修复。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F01.stc5y0ZC.png&amp;fm=webp&amp;w=800&amp;h=383&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<h3>更新缓存后更新数据库</h3>
<p>要注意，数据库的作用是持久化数据的，如果使用这个方法，那么可能导致<strong>错误数据</strong>，而不是<strong>脏数据</strong>。例如在更新的时候，更新缓存成功，但是更新数据库失败。那么这个问题就严重了。下一次查询的时候获取缓存中的数据，但是这个数据根本不在数据库中。还是用一张图来看看在并发的时候<strong>写-写</strong>可能出现什么问题吧。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F02.DQy7wWtu.png&amp;fm=webp&amp;w=800&amp;h=397&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<h3>删除缓存后更新数据库</h3>
<p>如果是使用删除策略呢？首先来看看有什么好处吧，在<strong>并发的写-写</strong>请求中，这样的策略是不会出现什么问题，无论是什么顺序，缓存最后都会被删除，那么就不会存在说数据不一致的情况了。</p>
<p>那么来看看在<strong>读-写</strong>并发的时候会不会有什么问题吧。</p>
<ol>
<li>线程A更新（<strong>写</strong>）这个数据的同时，线程B读取（<strong>读</strong>）这个数据</li>
<li>线程A成功删除了缓存里的老数据，这时候线程B查询数据发现缓存失效</li>
<li>线程A更新数据库成功</li>
</ol>
<blockquote>
<p>如果读请求写Cache的时机是在，写请求后面。那么就会出现数据不一致的情况</p>
</blockquote>
<h3>删除缓存前更新数据库</h3>
<p>再来考虑一下，<strong>读—写</strong>并发的情况</p>
<p>从图中可以很清楚的看到，先更细数据库再删除缓存，只有<strong>在更新数据库成功到缓存删除之间</strong>有时间差，可能会被其他线程读取到旧数据
<img src="https://agility6.me/.netlify/images?url=_astro%2F03.B1xTHbNv.png&amp;fm=webp&amp;w=800&amp;h=369&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<blockquote>
<p>引申：考虑一下这一种情况，会不会在一开始的时候，读请求就发现缓存不存在了呢？</p>
</blockquote>
<p>个人认为不太可能出现这种情况，<strong>注意⚠️：我们考虑的是并发的情况下</strong>，如果说在此条件下出现了一开始缓存就不存在的话，那么可能就是<strong>写场景很多</strong></p>
<h3>总结</h3>
<p>✌️我们已经讨论了所有的情况</p>
<ol>
<li>
<p>在使用<strong>更新缓存+更新数据库</strong>的策略时，可能会出现DB失败，导致数据不一致。解决办法，利用<strong>MQ</strong>确认数据库更新成功（<strong>代码复杂</strong>）</p>
</li>
<li>
<p>在使用<strong>删除缓存+更新数据库</strong>的策略时，在并发<strong>写+读</strong>的情况，可能会出现不一致的情况，利用<strong>延迟双删</strong>来解决</p>
<blockquote>
<p>延时双删：需要评估延迟的时间，如果控制不好那么也是没有作用的。
所谓的延迟双删就是，防止<strong>读请求把旧数据写回DB</strong>，那么在写请求处理完之后，等到差不多的时间延迟再重新删除这个缓存值
延迟的时间：太长那么该时间窗口得到的数据都是脏数据，太短相当于无用功了</p>
</blockquote>
</li>
</ol>
<p>从出现的概率来说，比较推荐使用<strong>更新数据库后删除缓存值</strong>，不知道你记不记得该策略还可能有什么问题？上面我们讨论过一个问题就是，会不会一开始就缓存不存在呢？
这种情况相当于<strong>写操作比较多</strong>。会有什么问题呢？导致的主要问题就是，key会频繁的失效，打到数据库中，如果这个key还是一个热点key😱</p>
<p>所以总结一下，大致可以分为以下情况</p>
<ul>
<li><strong>业务如果是读多写少：使用更新数据库后删除缓存</strong></li>
<li><strong>针对写多读少或者是读写相当多：使用更新数据库后更新缓存</strong></li>
</ul>
<h2>最终一致性</h2>
<blockquote>
<p>最终一致性才是王道！</p>
</blockquote>
<h3>缓存设置过期时间</h3>
<h3>减少缓存删除/更新的失败</h3>
<p>如果因为服务的情况没有正常执行Cache的步骤，那么可能造成长时间的不一致</p>
<p>可以使用消息中间件来保证删除或者更新的成功</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F04.CE3o1ZJx.png&amp;fm=webp&amp;w=800&amp;h=498&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<h3>通过订阅MySQL binLog的方式来处理缓存</h3>
<p>Canal这个开源项目可以很好的帮助我们</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F05.BFjuoCja.png&amp;fm=webp&amp;w=800&amp;h=356&amp;dpl=69dce8926406240008354a25" alt="" /></p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[第三方接口调用]]></title>
            <link>https://agility6.me/call-third-party</link>
            <guid isPermaLink="false">https://agility6.me/call-third-party</guid>
            <pubDate>Tue, 10 Sep 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[前言 在实习的过程中我遇到了多个需要对接第三方平台的工作，那么对接的时候我应该考虑什么或者是我应该注意什么。 因此这一篇文章就尝试总结一下吧 经常需要考虑的点 封装统一的Http请求工具类：这一点很好理解，其实就是为了去更好的复用代码 打印请求接口入参、出参、耗时日志 参数的合法性校验 减少对下游无效的调用 例如，数据格式的校验、业务合法性的校验 接口设置超时超时时间 不同的业务场景设置不同的超时...]]></description>
            <content:encoded><![CDATA[<h2>前言</h2>
<p>在实习的过程中我遇到了多个需要对接第三方平台的工作，那么对接的时候我应该考虑什么或者是我应该注意什么。</p>
<p>因此这一篇文章就尝试总结一下吧</p>
<h2>经常需要考虑的点</h2>
<ol>
<li>
<p>封装统一的Http请求工具类：这一点很好理解，其实就是为了去更好的复用代码</p>
</li>
<li>
<p>打印请求接口入参、出参、耗时日志</p>
</li>
<li>
<p>参数的合法性校验</p>
<p>减少对下游无效的调用</p>
<p>例如，数据格式的校验、业务合法性的校验</p>
</li>
<li>
<p>接口设置超时超时时间</p>
<p>不同的业务场景设置不同的超时时间</p>
</li>
<li>
<p>接口是否需要重试以及重试的次数需要考虑</p>
<p>需要考虑一下下游接口提供的能力，注意事务接口不要重试</p>
<p>查询类接口重试，也是需要考虑的因为要考虑下游接口QPS的支持</p>
</li>
<li>
<p>多次调用改为单次批量调用</p>
</li>
<li>
<p>接口返回的数据考虑是否缓存</p>
<p>如果返回数据理论上在长时间内都不会改变，可以使用redis进行缓存</p>
</li>
</ol>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Think（一些思考💡）]]></title>
            <link>https://agility6.me/think</link>
            <guid isPermaLink="false">https://agility6.me/think</guid>
            <pubDate>Sat, 07 Sep 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[前言 📝这是一个十分主观记录，可能是我对技术的思考或者是批判，可能是我当下的想法这个想法随时都可能发生改变。 回想第一次实习的时候，我收获到什么了？可能最有印象的是对技术的思考，这一点可能是最重要的，在以前自己独立做项目的时候，并不会去过多思考为什么要用这个技术，那时候是以技术的角度出发去做。 通过实习的技术调研评审，我开始思考业务，总结起来其实就是「技术永远是反哺业务的」，所有我认为我应该去思...]]></description>
            <content:encoded><![CDATA[<h2>前言</h2>
<blockquote>
<p>📝这是一个十分主观记录，可能是我对技术的思考或者是批判，可能是我当下的想法这个想法随时都可能发生改变。</p>
</blockquote>
<hr />
<p>回想第一次实习的时候，我收获到什么了？可能最有印象的是对技术的思考，这一点可能是最重要的，在以前自己独立做项目的时候，并不会去过多思考为什么要用这个技术，那时候是以技术的角度出发去做。</p>
<p>通过实习的技术调研评审，我开始思考业务，总结起来其实就是「技术永远是反哺业务的」，所有我认为我应该去思考如何根据业务去选择对应的技术，在体量不大的时候根本没有必要去使用市面上所谓的“高大尚”技术，做到KISS原则，并且保证在实现的时候做一些扩展性的考虑就可以了。</p>
<p>那为什么自己在做项目的时候，明明没有那么大的体量但是又要用那些技术呢？「养兵千日，用兵一时」在工作中可能前期用户体量并不大，但是可能随着产品迭代用户量开始增大，你得「有能力」去重构你的项目，重新选择合适当前的技术。</p>
<hr />
<p>在阅读到唯一索引和普通的索引如何选择，发现了一个点就是在普通索引更新操作的时候，可以将待更新的数据先存入到change buffer，然后当我们需要查询的时候，才进行merge。突然发现这一个点在项目中也有使用到，例如要更新数据库的时候，可以在之前使用一个缓冲区，当缓冲区达到某个条件再去统一更新数据库</p>
<p>其实又有一个地方可以思考的，就是我们引入一个东西，一定要考虑如果它挂了怎么办？数据呢？例如缓冲区就是</p>
<ol>
<li>当还没有进入缓冲区的时候挂了怎么办？</li>
<li>进入的时候缓冲区的时候挂了怎么办（数据进入了一半）？</li>
<li>数据在缓冲区的时候挂了怎么办？</li>
<li>传入DB的时候挂了怎么办？</li>
</ol>
<p>或许这个就是技术的通用性？</p>
<hr />
<p>在MySQL中，事务提交的时候，如果直接进行刷脏页操作，会涉及到随机IO，导致事务的提交过程变慢。为了优化这一流程，MySQL的InnoDB存储会将修改数据记录到Redo Log中，这是一个顺序IO，成功后立刻返回事务提交成功。（异步处理）</p>
<p>💃：再一次验证技术思想是相通的，在学习到这一点，很快就能联想到平时开发的时候也会是使用到这样的异步思想，为了能够快速返回该链路上的结果，使用异步化进行。当然其中要考虑十分多的case情况了。MySQL是使用了Redo Log来解决了各种case情况。</p>
<hr />
<p>如何进行学习？
在学习InnoDB的时候，先从InnoDB想达到什么目的，它平衡了「高可用」和「高性能」。
因此有内存结构和磁盘结构
内存结构中：保证高性能有页的概念、怎么识别数据在哪个页中、设计出描述数据块和数据页缓存的Hash表、如何做到快速查询页呢？设计出页与页之间是双向链表等等
好像所有那些不明所以的知识点就给串联起来了，所以从「我要做什么」出发可能会有不一样的学习体验</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[消息队列中常见问题的思考]]></title>
            <link>https://agility6.me/mq-thinking</link>
            <guid isPermaLink="false">https://agility6.me/mq-thinking</guid>
            <pubDate>Sat, 24 Aug 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[前言 每当引入一个新的技术在项目中，一定是为了解决某个问题从而提升性能，当然不可避免的会增加维护成本以及技术本身需要考虑的问题。那么就来总结一下当项目引入了消息队列之后，需要去关注的一些常见问题 如何处理消费过程中的重复消息 如何确保消息不丢失 如何保证消息的顺序消费 消息积压了应该如何处理 如何处理消费过程中的重复消息 在消息传递过程中，如果传递失败那么发送方会执行重试，而这个重试的过程就可以会...]]></description>
            <content:encoded><![CDATA[<h2>前言</h2>
<p>每当引入一个新的技术在项目中，一定是为了解决某个问题从而提升性能，当然不可避免的会增加维护成本以及技术本身需要考虑的问题。那么就来总结一下当项目引入了消息队列之后，需要去关注的一些常见问题</p>
<ol>
<li>如何处理消费过程中的重复消息</li>
<li>如何确保消息不丢失</li>
<li>如何保证消息的顺序消费</li>
<li>消息积压了应该如何处理</li>
</ol>
<h3>如何处理消费过程中的重复消息</h3>
<p>在消息传递过程中，如果传递失败那么发送方会执行重试，而这个重试的过程就可以会出现重复的消息。</p>
<p>可能会有第一直觉想到说，如果我的消息队列本身消息就是没有重复，那么业务程序不就简单多了吗？</p>
<p>在常见的消息队列中都是遵守<strong>At least once</strong>，也就是至少一次，消息在传递的过程中，至少会被送达一次，也就是说不允许丢消息，但是允许有少量重复消息出现。</p>
<p>说的这里要避免消费过程中的重复消息，本质还是需要<strong>让代码“接受”重复</strong>，也就是要让代码能过消除重复消息对业务的影响。</p>
<p>用幂等性来解决重复消息的问题，幂等通俗来说就是，<strong>任意多次执行产生的影响，和第一次执行的影响相同</strong>，那么如何处理消费过程中的重复消息呢？<strong>At least once + 幂等消费</strong>，那么下面就来谈谈几种常见的幂等方式。</p>
<blockquote>
<p>案例：假设给账户A的余额增加100元，如何达到幂等</p>
</blockquote>
<h4>利用数据库的唯一约束来实现幂等</h4>
<p>可以新建一张<strong>转账流水表</strong>，其中表中有三个字段并且创建唯一约束为转账单ID和账单ID</p>
<ol>
<li>转账单的ID</li>
<li>账户ID</li>
<li>变更余额</li>
</ol>
<p>来看看消费逻辑，同一张转账单和同一个账户只能在表中存在一个，这样就达到了幂等的效果。</p>
<ol>
<li>在转账流水表中增加一条转账记录</li>
<li>根据转账记录，异步操作更新用户余额</li>
</ol>
<h4>为更新的数据设置前置条件</h4>
<p>顺延着上面所说的<strong>唯一约束</strong>的思路，可以在变更数据的时候设置一个前置条件，当满足前置条件的情况下才进行操作。</p>
<p>例如向用户A的账户增加100元，那么增加一个约束，只有当用户A的账户是等于200元的时候才进行增加。在消息队列中可以这样做</p>
<ol>
<li>在发送消息的时候在消息体中带上当前的余额</li>
<li>消费过程中判断数据库中的余额是否与当前准备消费的余额相等</li>
</ol>
<h4>为什么消息队列没有做到Exactly once？</h4>
<ul>
<li>性能问题：回到最原始的问题，为什么要使用消息队列无非就是提升性能，那么如果消息队列要实现Exactly once的特性，就必须在消费端pull数据的时候，判断是否被消费过，无疑会降低性能。</li>
</ul>
<blockquote>
<p>引申思考点：正如使用Redis进行缓存必然需要考虑数据一致性的问题，如果为了达到强一致性而采用锁进行，那么我认为是本末倒置了。</p>
</blockquote>
<ul>
<li>即使消息队列实现了Exactly once，但是在消费端成功消费之后，返回ack失败那么还是会导致重复消息，因此还是需要at least once + 幂等来进行处理。</li>
</ul>
<h3>如何确保消息不丢失</h3>
<p>解决消息不丢失，前提应该是知道消息可能会在那里丢失。</p>
<ul>
<li>生产阶段：消费在Producer创建出来，经过网络传输发送到Broker端</li>
<li>存储阶段：在这个阶段，消息在Broker之端存储</li>
<li>消费阶段：Consumer从Broker上拉取消息，进过网络传输到Consumer上</li>
</ul>
<p><img src="https://agility6.me/assets/mq/01.png" alt="" /></p>
<h4>生产阶段</h4>
<p>消息队列通过最常用的请求确认机制，保证消息的可靠传输，当Broker收到消息的时候会返回确认响应。</p>
<p>在代码层面上，只要正确处理返回值并且捕获对应的异常，就可以保证这个阶段消息不丢失</p>
<h4>存储阶段</h4>
<p>在正常情况下，如果消息能够正常的到达Broker中并且完成持久化那么消息就不会丢失。</p>
<p>Kafka是使用日志来做消息的持久化，日志文件是存储在磁盘上的，如果Broker在消息没有完全写入日志之前崩溃，那么消息可能会丢失。并且，操作系统在写磁盘之前，会先把数据写入到page cache中，然后操作系统自己决定什么时候同步到磁盘中，这个过程中，如果这个过程中宕机了，那么这个消息也可能丢失。</p>
<p>即使Kafka引入了副本机制来提升消息的可靠性，那么如果发生了同步延迟，还没有来得及同步，主副本就挂掉了，那么消息就可能发生丢失。</p>
<blockquote>
<p>kafka是不能保证100%的消息不丢失（极端情况下）,可以引入分布式事务等来保证在kafka Broker没有保存消息成功时，可以重新投递消息。</p>
</blockquote>
<h4>总结</h4>
<blockquote>
<p>如何确保消息不丢失？WAL!</p>
</blockquote>
<h3>如何保证消息的顺序消费</h3>
<blockquote>
<p>kafka为例</p>
</blockquote>
<p>在kafka中它可以保证，在同一个的partition上是顺序消费的，但是跨partition，或者是跨topic的消息就是无序的了。</p>
<blockquote>
<p>引申为什么同一个partition的消息是有序的？当生产者向某个partition发送消息的时候，消息会追加到该partition的日志文件中，并且被分配唯一一个offset，文件的读写是有顺序的，而消费者在消费的过程中，是通过offset来进行的，因此保证了消息是有序的。</p>
</blockquote>
<p>那么如何实现消息的顺序消费</p>
<ul>
<li>
<p>在一个topic中，只创建一个partition，这样就可以保证同一个partition中顺序消费了</p>
</li>
<li>
<p>发送消息的时候指定partition，如果一个topic有多个partition，那么可以将需要保证顺序的消息都发送到同一个partition中</p>
</li>
</ul>
<h4>如何发送到同一个partition</h4>
<ul>
<li>指定partition</li>
</ul>
<pre><code class="language-java">  Producer&lt;String, String&gt; producer = new KafkaProducer&lt;&gt;(getProperites());

  String topic = "hello_world";
  String message = "hi!";
  int partition = 0;

  // 创建包含分区信息的ProducerRecord
  ProducerRecord&lt;String, String&gt; record = new ProducerRecord&lt;&gt;(topic, partition, null, message);

  produce.send(record);
</code></pre>
<h3>消息积压了应该如何处理</h3>
<p>消息积压一定是在使用消息队列需要考虑的问题。</p>
<p>首先，消息积压的直接原因一定是，<strong>系统中某部分的性能出现了问题</strong></p>
<p>如果消费端速度跟不上发送端生产消息的速度，那么就有可能造成积压，在设计系统的时候，应该保证<strong>消费端的消费性能应该高于生产端发送性能</strong>。消费端的性能优化除了优化消费业务逻辑以外，也可以通过水平扩容，增加消费端的并发数来提升总体的消费性能。</p>
<p>那么常见的消息积压应该如何处理？对于系统内发生了消息积压的情况，先解决积压（扩容Consumer的实例数量），再分析原因；那么常见的处理积压可以总结</p>
<ol>
<li>临时扩容，增加消费端</li>
<li>服务降级，关闭非核心业务，减少消息生产</li>
<li>通过日志分析，找到积压问题</li>
</ol>
<blockquote>
<p>引申：消费端是否可以通过批量消费来提升消费性能？是否批量消费总体是需要<strong>结合业务</strong>的，需要注意如果使用了批量处理，需要考虑批量消费一旦某条数据消费失败会导致整批数据重复消费；业务对实时性要求不能太高，批量消费需要Broker积累到一定消费数量才会发送到Consumer</p>
</blockquote>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Mysql Index Issue]]></title>
            <link>https://agility6.me/mysql-index-issue</link>
            <guid isPermaLink="false">https://agility6.me/mysql-index-issue</guid>
            <pubDate>Tue, 20 Aug 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[📝记录一下在实习遇到的一个线上Bug，整体排查解决链路（已脱敏）。 🤔为什么要写这一个总结呢？问题其实很简单，但是其中的排查步骤是值得思考的，如何将这次经验抽象成通用的解决步骤，这个才是关键所在！。 省流版：因为一张表索引没有设置好，导致的后续一系列的问题！ 问题的发现 客户的工单：产品的xxx页面修改操作无法响应，卡死。 定位问题 出现这个问题，第一个需要查看的就是日志和监控系统，但是项目没...]]></description>
            <content:encoded><![CDATA[<ul>
<li>📝记录一下在实习遇到的一个线上Bug，整体排查解决链路（已脱敏）。</li>
<li>🤔为什么要写这一个总结呢？问题其实很简单，但是其中的排查步骤是值得思考的，如何将这次经验<strong>抽象成通用的解决步骤，这个才是关键所在！</strong>。</li>
</ul>
<p><strong>省流版：因为一张表索引没有设置好，导致的后续一系列的问题！</strong></p>
<h2>问题的发现</h2>
<p>客户的工单：产品的xxx页面修改操作无法响应，卡死。</p>
<h2>定位问题</h2>
<ol>
<li>
<p>出现这个问题，第一个需要查看的就是日志和监控系统，但是项目没有部署监控和日志系统</p>
</li>
<li>
<p>紧急部署内部的监控工具</p>
</li>
</ol>
<h2>分析</h2>
<h3>监控日志分析</h3>
<p>通过日志可以发现，在某个时间的时候开始出现锁超时问题其中，<code>xxx_todo</code>表被锁住了。在业务中该表对应着我们其中待办模块。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F01.BjCYjt8N.png&amp;fm=webp&amp;w=800&amp;h=247&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<p>从上述日志得到的信息就是，<strong><code>xxx_todo</code>表被锁了</strong>，该表对应的是待办模块，判断问题的方向为待办相关定时任务</p>
<h3>定时任务日志分析</h3>
<p>通过拉取数据，找到定时任执行记录，可以锁定到「消息机制」待办定时任务执行没有结果。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F02.0j_pcYPo.png&amp;fm=webp&amp;w=800&amp;h=301&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<h3>阻塞期间数据库分析</h3>
<p>阻塞期间数据库分析，实时抓取阻塞sql，确认和待办删除业务相关，并且执行将近两个小时</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F03.PI21IjfO.png&amp;fm=webp&amp;w=800&amp;h=65&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<h3>线程分析</h3>
<p>进行sql执行节点，获取线程快照，发现「消息机制」待办定时清理，正在运行</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F04.CW1Q7QFh.png&amp;fm=webp&amp;w=800&amp;h=426&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<h3>执行计划分析</h3>
<p>既然已经找到了具体的阻塞sql，那么我们可以直接进行expalin进行分析。</p>
<p>这里贴出<strong>脱敏后的SQL</strong>，来解释一下这个sql的含义吧</p>
<ol>
<li>
<p><code>delete from sys_start</code></p>
<p>这个是一个删除操作，目的是从<code>sys_start</code>中删除满足某些条件的记录</p>
</li>
<li>
<p><code>where exists (...)</code></p>
<p>这里使用了 exists 子句，它的作用是：如果 exists 内的查询返回任何记录，外部的删除操作就会执行。也就是说，如果某些条件的记录存在于子查询中，那它们对应的 <code>sys_start</code> 表的记录将被删除。</p>
</li>
<li>
<p>子查询<code>a</code></p>
<p>只选取那些 在 <code>sys_todo</code> 表中没有关联记录 的 <code>fd_id</code>。换句话说，它找到那些在 <code>sys_todo</code> 表中存在，但在 <code>sys_todotarget</code> 表中没有匹配 <code>fd_todoid</code> 的记录。</p>
</li>
<li>
<p>子查询`b</p>
<p>只选取那些 在 <code>sys_todo_info</code> 表中没有关联记录 的 <code>fd_id</code>。换句话说，它找到那些在 <code>sys_todo</code> 表中存在，但在 <code>sys_todo_info</code> 表中没有匹配 <code>fd_todoid</code> 的记录。</p>
</li>
<li>
<p>子查询a和b的连接</p>
<p>这部分将子查询 <code>a</code> 和子查询 <code>b</code> 中找到的 <code>tid</code> 和 <code>did</code> 进行内连接，条件是 <code>a.tid = b.did</code>。因此，它会选出那些同时满足两者条件的 <code>fd_id</code>。</p>
</li>
<li>
<p>总结</p>
<p>这条 SQL 语句的作用是：从 <code>sys_start</code> 表中删除那些 <code>fd_todoid</code> 符合以下条件的记录</p>
<p>这些记录在 <code>sys_todo</code> 表中有对应的 <code>fd_id</code>，但 不在 <code>sys_todotarget</code> 表中。</p>
<p>同时，这些 <code>fd_id</code> 在 <code>sys_todo_info</code> 表中也 没有 对应的记录。</p>
</li>
</ol>
<pre><code class="language-sql">delete from sys_start
where exists (
  select a.tid
  from (
    select s.fd_id as tid
    from sys_todo s
    where not exists (
      select fd_todoid
      from sys_todotarget y
      where s.fd_id = y.fd_todoid
    )
  ) a
  inner join (
    select l.fd_id as did
    from sys_todo l
    where not exists (
      select fd_todoid
      from sys_todo_info m
      where l.fd_id = m.fd_todoid
    )
  ) b
  on a.tid = b.did
  where a.tid = fd_todoid
)
</code></pre>
<p>从执行计划和sql可以得知，扫描<code>sys_start</code>表进行数据筛查进行的是全表扫描，其他表关联字段需要进行所以扫描，可以发现确认<code>sys_todotarget</code>缺失索引</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F05.BsOg99pp.png&amp;fm=webp&amp;w=800&amp;h=301&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<h3>增加索引优化</h3>
<p>更新完成后再次查看执行计划，并且进行测试</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F07.B739hMgC.png&amp;fm=webp&amp;w=800&amp;h=335&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F06.CCftxcU1.png&amp;fm=webp&amp;w=800&amp;h=164&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F08.CYTX_VVD.png&amp;fm=webp&amp;w=800&amp;h=377&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<p>成功🏅</p>
<h2>总结</h2>
<blockquote>
<p>即使一张表未添加索引的性能问题，也可能引起严重后果。排查链路很长，排查难度也很大。</p>
</blockquote>
<p><strong>如果在项目中遇到类似的问题，你会如何着手处理？</strong></p>
<ol>
<li>
<p>问题复现初步分析与监控</p>
<p>收集信息：首先，我会收集性能问题的相关信息，包括具体的症状（如响应时间过长、查询卡顿、系统无响应等）、发生时间、频率以及受影响的范围。</p>
<p>监控工具能提供关键的指标，如CPU使用率、内存占用、数据库查询时间等。</p>
</li>
<li>
<p>日志分析</p>
<p><strong>日志分析是核心</strong>，通过分析日志中的错误、警告等等定位到可能的瓶颈或者是故障点。</p>
<p>其实日志分析我感觉是可难可简单的就是，如果遇到多因素交织情况下，性能问题往往不是由单一因素引起的，而是多个因素（如不优化的SQL、资源瓶颈、锁争用等）共同作用的结果，排查时需要考虑多个角度。<strong>总结起来就是需要多总结经验</strong></p>
</li>
<li>
<p>具体情况具体分析</p>
<p>例如如果排查出是DB的问题，那么其实可以大致分为这几类</p>
<p>查询性能、索引管理、锁争用、数据库的配置等</p>
</li>
</ol>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[HTTPS的「S」]]></title>
            <link>https://agility6.me/https</link>
            <guid isPermaLink="false">https://agility6.me/https</guid>
            <pubDate>Wed, 14 Aug 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[前言 本篇文章就来记录一下HTTP的S究竟是什么？ HTTP大家一定都是十分熟悉了，那么HTTP与HTTPS有什么不同呢？ 多了个S HTTP是明文传输，容易有安全性的问题 HTTPS是会加密传输的，并且需要CA证书 其实HTTPS重点是在这个S上，也就是SSL/TLS这就是HTTPS的核心，所以本篇文章也是从这两个进行展开的。 加密安全协议 SSL其实是TLS的前生它们都是安全加密协议，目前大部...]]></description>
            <content:encoded><![CDATA[<h2>前言</h2>
<p>本篇文章就来记录一下HTTP的S究竟是什么？</p>
<p>HTTP大家一定都是十分熟悉了，那么HTTP与HTTPS有什么不同呢？</p>
<ol>
<li>
<p><s>多了个S</s></p>
</li>
<li>
<p>HTTP是明文传输，容易有安全性的问题</p>
</li>
<li>
<p>HTTPS是会加密传输的，并且需要CA证书</p>
</li>
</ol>
<p>其实HTTPS重点是在这个S上，也就是SSL/TLS这就是HTTPS的核心，所以本篇文章也是从这两个进行展开的。</p>
<h2>加密安全协议</h2>
<p>SSL其实是TLS的前生它们都是安全加密协议，目前大部分浏览器都不支持SSL，而支持TLS。</p>
<p>到这里可以很清楚的知道，HTTPS需要保证安全性，那么就一定需要对数据进行加密，所以接下来先来说一说加密的知识吧。</p>
<h3>对称加密</h3>
<p>通俗来说，对称加密就是双方都是使用相同的加密规则，那么这个就称为对称加密。</p>
<p>那么如果有第三方知道这个加密规则，那么就有风险被破解了。</p>
<h3>非对称加密</h3>
<p>首先，先来讲讲如何得到一个安全的密钥。</p>
<p>首先用户A和用户B都拥有一个公钥和私钥，这时候会合并成<code>公 + 私</code>的进行传输，那么其中有可能被黑客窃取，但是没有关系，当双方获取到了对方的公+私钥，那么会和自己本身的私钥进行结合，那么这个结果就是一个安全的密钥了。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F01.BA9sK8dw.png&amp;fm=webp&amp;w=800&amp;h=543&amp;dpl=69dce8926406240008354a25" alt="Diagram" /></p>
<p>那么非对称加密，就可以使用这个安全的密钥来进行加密了，这个就是非对称加密的核心了。</p>
<p>公开密钥是所有人都知道的密钥，私有密钥仅仅是持有方才有的密钥，一般来说私钥就放在服务器里，数据进过公钥加密就只能被私钥解密，数据经过私钥加密就只能被公钥解密。</p>
<p>这里类比一下客户端和服务器的关系</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F02.vV1ljilN.png&amp;fm=webp&amp;w=800&amp;h=460&amp;dpl=69dce8926406240008354a25" alt="Diagram" /></p>
<h3>证书</h3>
<p>前面已经学习到了两种的加密方式了，但是我们还是不知道和我在沟通的是否是自己想要沟通的对象，因此需要服务端需要申请一张SSL证书，来告诉这个域名是经过验证的，这里证书还规定了私钥和公钥。</p>
<h2>TLS</h2>
<p>综上我们介绍了一些前置的知识，那么现在就来看看如何进行TLS握手呢？这里使用时TLS1.2，首先进行老生常谈的三次握手接下来就是TLS的握手了。</p>
<ol>
<li>客户端说“你好”</li>
</ol>
<ul>
<li>客户端会发送支持的TLS协议版本，如TLS1.2版本</li>
<li>生成随机数，用于会话密钥条件条件之一</li>
<li>当前我支持的加密套件</li>
</ul>
<ol>
<li>服务器说“你好”</li>
</ol>
<ul>
<li>确认TLS是否支持，不支持则关闭加密通信</li>
<li>服务器生成随机数，用于会话加密之一</li>
<li>确认密码套件</li>
<li>服务器的数字证书</li>
</ul>
<ol>
<li>客户端回应</li>
</ol>
<ul>
<li>收到服务器的回应，会确认证书的真实性</li>
<li>如果没有问题则取出证书的公钥，使用加密报文，向服务器发送以下信息，一个随机数（会被服务器公钥加密）、加密通信算法改变通知，表示随后的信息用会话秘钥加密通行</li>
<li>「客户端回应」这一步生成的随机数，是第三个了。会发给服务端，因此客户端和服务端随机数都是一样的。</li>
</ul>
<p>客户端和服务端有了这三次随机数，使用规定的加密算法，生成本次通信的「会话秘钥」</p>
<ol>
<li>服务器的最后回应</li>
</ol>
<ul>
<li>服务器收到也收到三个随机数，使用规定的加密算法，计算本次的「会话秘钥」</li>
</ul>
<p>最后TLS握手完毕，接下来客户端和服务器进入加密通行。</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[About Cache]]></title>
            <link>https://agility6.me/cache</link>
            <guid isPermaLink="false">https://agility6.me/cache</guid>
            <pubDate>Mon, 10 Jun 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[缓存的经典问题 缓存失效 缓存穿透 缓存雪崩 数据不一致 数据并发竞争 Hot Key Big Key 每一个问题都参照四个步骤进行阐述：问题描述、原因分析、业务场景、解决方案 缓存失效 问题描述 当一个系统中存在大量的热点数据，通常情况下就需要上缓存，大致的流程就是 查缓存（有则直接返回） 查DB（缓存中不存在） 将查到的数据回写到缓存中 我们希望数据查询尽可能命中，这样系统负载最小，性能最佳，...]]></description>
            <content:encoded><![CDATA[<h2>缓存的经典问题</h2>
<ol>
<li>缓存失效</li>
<li>缓存穿透</li>
<li>缓存雪崩</li>
<li>数据不一致</li>
<li>数据并发竞争</li>
<li>Hot Key</li>
<li>Big Key</li>
</ol>
<blockquote>
<p>每一个问题都参照四个步骤进行阐述：问题描述、原因分析、业务场景、解决方案</p>
</blockquote>
<h3>缓存失效</h3>
<h4>问题描述</h4>
<p>当一个系统中存在大量的热点数据，通常情况下就需要上缓存，大致的流程就是</p>
<ol>
<li>查缓存（有则直接返回）</li>
<li>查DB（缓存中不存在）</li>
<li>将查到的数据回写到缓存中</li>
</ol>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F26.BOWMKStF.png&amp;fm=webp&amp;w=800&amp;h=625&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<p>我们希望数据查询尽可能命中，这样系统负载最小，性能最佳，但是如果这时候有<strong>大量的Key同时失效</strong>，很多缓存数据访问都会miss，就会穿透到DB中，这样就会导致整体的系统压力急剧上升，这就是缓存失效的问题。</p>
<h4>原因分析</h4>
<p>导致缓存失效的主要原因，就是批量Key一起失效，简言之就是在加入缓存时过期时间都是一致的。一般情况下，缓存时逐步写入的，所以自然就会是逐步淘汰的。</p>
<p>但是，在一些场景下，如果需要将一个批次的热点数据添加到缓存中，这时候如果过期时间没有做处理，就会造成批量的Key同时失效。</p>
<h4>业务场景</h4>
<p>比如说一批次的火车票、飞机票当票售卖的时候，系统会一次性加载到缓存中。</p>
<h4>解决方案</h4>
<p>前面分析了，缓存失效最主要的原因就是大量的Key同时失效，那么解决方案也是从这一个方面出发。</p>
<p>设计缓存的过期时间，<strong>过期时间 = base时间 + 随机时间</strong>。这样数据在以后是慢慢过期，而不是瞬间全部过期。</p>
<blockquote>
<p>base时间如何设计？</p>
<ol>
<li>应用程序的性质</li>
<li>缓存数据的更新频率</li>
<li>缓存数据的大小和内存容量</li>
</ol>
</blockquote>
<h3>缓存穿透</h3>
<h4>问题描述</h4>
<p>“穿透”顾名思义就是穿过Cache和DB，简单来说，当我们查询个别的Key时，缓存中没有命中，自然就会查DB，DB也不存在这个数据，那么这时候就无法回写到缓存中，这样就会导致<strong>查询缓存中不存在的数据时，每次都要查询数据库</strong>。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F27.DZ-FGEcr.png&amp;fm=webp&amp;w=800&amp;h=649&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<blockquote>
<p>在量级不大的流量，缓存穿透发生概率很低，并且缓存穿透很难被发现！</p>
</blockquote>
<h4>原因分析</h4>
<p>根本原因很简单，就是访问了不存在的Key和数据。</p>
<p>在系统设计的时候，更多是考虑正常访问的路径，对特殊访问路径、异常访问路径考虑相对欠缺。</p>
<h4>业务分析</h4>
<p>缓存穿透的业务场景很多，比如通过不存在ID访问商品等。</p>
<p>其实在个例的缓存穿透，对系统几乎没有影响，但如果是大量的缓存穿透攻击，在短时间大批量的查询不存在，有可能导致DB直接宕机。</p>
<h4>解决方案</h4>
<ol>
<li>当在查询这些不存在的数据，第一次查DB，虽然没有查到结果返回NULL，但是依旧去记录这一个Key，只是这个Key对应的value是一个特殊的值。</li>
</ol>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F28.a8DTV8S1.png&amp;fm=webp&amp;w=800&amp;h=642&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<ol>
<li>构建一个BloomFilter缓存过滤器，记录<strong>全量数据</strong>，这样访问数据时，可以直接通过BloomFilter判断这个Key是否存在，如果不存在直接返回即可，根本无需查缓存和DB。</li>
</ol>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F29.BW7s1n18.png&amp;fm=webp&amp;w=800&amp;h=583&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<p><strong>方案一的一些问题</strong>：当遇到高密度批量的访问不存在的Key，即便是Key只设置一个简单的默认值，也会占用大量的资源。</p>
<p><strong>改善的策略：</strong></p>
<ol>
<li>针对这些不存在的Key设置较短的过期时间，尽快过期。</li>
<li>将这些不存在的Key存入到一个公共缓存中，首先查找业务缓存，如果miss则查找公共缓存中的非法Key，如果公共缓存命中则直接返回，如果公共缓存不存在，则会到达DB，DB如果也是为miss，则将Key回写到公共缓存中，如果DB存在正常回写即可。</li>
</ol>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F30.DktPnGrr.png&amp;fm=webp&amp;w=800&amp;h=563&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<h4>布隆过滤器</h4>
<p>布隆过滤器是一种基于<strong>位数组</strong>和<strong>多个哈希函数</strong>，布隆过滤器用于判断一个元素是否存在于一个集合中，布隆过滤器也是存在一定的误判率。</p>
<p><strong>工作原理</strong></p>
<p>布隆过滤器是一种概率型数据结构，可以高效的插入和查询，得到某个值是否存在。采用一个很长的二进制数组，通过一系列的Hash函数来确定该数据是否存在。本质上是一个n位的二进制数组，每个元素只有0和1来表示。</p>
<p>可以从下图得出：<strong>布隆过滤器对于“未出现”的判断是准确的，但无法对出现过做出绝对的正确判断</strong>。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F31.BUZRsMiJ.png&amp;fm=webp&amp;w=800&amp;h=920&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<p><strong>如何减少布隆过滤器的误判？</strong></p>
<ul>
<li>第一个是增加二进制位数，增加位数组的长度。</li>
<li>第二个是增加Hash的次数，其实每一次Hash处理都是在增加数据的特征，特征 越多，出现误判的概率就越小。</li>
</ul>
<pre><code class="language-java">@Autowired
private RedissonClient redissonClient;

private RBloomFilter&lt;Object&gt; bloomFilter;

@PostConstruct
public void init() {

    bloomFilter = redissonClient.getBloomFilter("bloom-filter");
    // 初始化，设置数组长度和误判率
    bloomFilter.tryInit(1000000L, 0.01);
    // 将所有的key添加到bloomFilter中即可

}
</code></pre>
<p>上面的代码中，通过Redisson创建了一个BloomFilter对象，设置了位数组长度位1000000，误判率位1%，然后通过<code>add()</code>方法向布隆过滤器中添加元素，通过<code>contains()</code>方法判断元素是否存在于布隆过滤器中。</p>
<p>另外，<strong>布隆过滤器不支持元素删除操作，一旦删除元素，可能会影响元素的判断结果</strong></p>
<p><strong>衍生问题：在初始化后，对应商品被删除怎么办？</strong></p>
<p>假如布隆过滤器初始化后，对应商品被删除了，该怎么办？</p>
<p>因为布隆过滤器某一位的二进制数据，可能被多个编号的Hash位进行引用，所以不能直接对布隆过滤器某一位进行删除，否则数据就会乱了。</p>
<p><strong>常见的解决方案</strong></p>
<ol>
<li>定时异步重建布隆过滤器，比如说每过4个小时在额外的一台服务器上，异步去执行一个任务调度，来重新生成布隆过滤器，替换掉已有的布隆过滤器。</li>
<li>计数布隆过滤器，在标准的布隆过滤器下，是无法得知当前某一位它是被哪些具体数据进行引用，但是计数布隆过滤器它是在这一位上额外的附加的计数信息，表达出该位被几个数据进行引用。</li>
</ol>
<h3>缓存雪崩</h3>
<h4>问题描述</h4>
<p>系统运行过程中，缓存雪崩是一个非常严 重的问题，缓存雪崩是指部分缓存节点不可用，导致整个缓存体系甚至服务系统不可用的情况。</p>
<p>缓存雪崩安装缓存是否支持rehash分两种情况</p>
<ol>
<li>缓存不支持rehash导致的系统雪崩不可用。</li>
<li>缓存支持rehash导致的缓存雪崩不可用。</li>
</ol>
<h4>原因分析</h4>
<p>在上述两种情况，缓存不进行rehash时产生的雪崩，<strong>一般是由于较多缓存节点不可用</strong>，请求穿透导致DB也过载不可用，最终整个系统雪崩。而缓存支持rehash时产生的雪崩，则大多数跟流量洪峰有关，流量洪峰到达，引发部分缓存节点过载Crash，然后因rehash扩散到其他缓存节点，最终整个缓存体系异常。</p>
<p>第一种情况，缓存节点不支持rehash，较多缓存节点不可用时，大量Cache访问失败，这些请求会进一步访问DB，而且DB可承载的访问量远比缓存小的多，请求量过大，就很容易造成DB过载，大量慢查询，最终阻塞甚至宕机，从而导致服务异常</p>
<p>第二种情况，因为缓存分布式设计时，会选择一致性hash分布式方法，同时在部分节点异常时，采用rehash策略，即把异常节点请求平均分散到其他缓存节点，在一般情况下，一致性hash分布 + rehash策略可以很好的运行，但<strong>在大量的流量洪峰到来时</strong>，如果大流量key比较集中，正好在某1~2个缓存节点，很容易将这些缓存节点的内存节点异常宕机，然后异常下线，这些大流量Key请求有被rehash到其他缓存节点，进而导致其他缓存节点也被过载Crash，缓存异常持续扩散，最终导致整个缓存体系异常。</p>
<h4>业务场景</h4>
<h4>解决方案</h4>
<p>预防雪崩</p>
<ol>
<li>对业务DB的访问增加读写开关，当发现DB请求变慢、阻塞、慢查询超过阈值，关闭读开关，部分或者所有读DB的请求继续快速失败、立即返回，等DB恢复之后再打开读开关。</li>
<li>对缓存增加多个副本，缓存异常或请求miss后，在读取其他缓存副本。</li>
<li>缓存监控</li>
</ol>
<h3>数据不一致</h3>
<h4>问题描述</h4>
<ol>
<li>同一份数据，有可能发生，DB和缓存的不一致。</li>
<li>如果缓存有多个副本，多个缓存副本里面的数据也可能会发生不一致现象。</li>
</ol>
<h4>原因分析</h4>
<p><strong>DB和缓存的不一致</strong></p>
<p>大多数是和<strong>缓存更新异常</strong>或者是<strong>更新的策略</strong>有关。</p>
<p><strong>缓存多个副本不一致</strong></p>
<p>系统采用一致性Hash分布，同时采用rehash自动漂移策略，在节点多次上下线之后，也会产生脏数据。缓存有多个副本时，更新某个副本失败，也会导致这个副本的数据是老数据。</p>
<h4>业务场景</h4>
<h4>解决方案</h4>
<h5>DB和缓存的不一致解决方案</h5>
<p>在通常情况下，最直观的方法就是，在更新DB的数据完成的时候再更新缓存，或者相反，这个方法也称为（<strong>双更新策略</strong>）。</p>
<pre><code class="language-java">public void putValue(key, value) {
    putToRedis(key, vlaue);
    putToDB(key, value); // 操作失败
}
</code></pre>
<p><strong>后删除策略（能解决多数不一致情况）</strong></p>
<p>因为每一次读取时，都先会判断Redis中是否有值，没有则会读取DB，这样是没有问题的，但是需要考虑的问题是</p>
<ul>
<li>先删缓存？</li>
<li>后删缓存？</li>
</ul>
<p><strong>先删缓存</strong></p>
<pre><code class="language-java">public void putValue(key, value) {
    deleteFromRedis(key);
    putToDB(key, value); // 操作失败
}
</code></pre>
<p>如果线程A删除了某个Key的值，这时候有另一个请求B到来，那么它就会穿到DB中，读取到旧的值。无论操作A更新数据库的操作持续多长时间，都会产生不一致的情况。</p>
<p><strong>后删缓存</strong></p>
<p>把删除的动作放在后面，就能够保证每次读取到的值都是最新的</p>
<pre><code class="language-java">public void putValue(key, value) {
    putToDB(key, value);
    deleteFromRedis(key);
}
</code></pre>
<p>这是我们日常中常用的模式，Spring cache就是默认实现了这个模式，分别介绍一下读和写的过程。</p>
<p><strong>数据的读取过程，规则是“先读Cache，再读DB”</strong></p>
<ol>
<li>每次读取数据，都从Cache读。</li>
<li>如果读到了，则直接返回。</li>
<li>如果读不到Cache的数据，则从DB中读。</li>
<li>读取到的数据回写到Cache中。</li>
</ol>
<p><strong>写请求，规则是“先更新DB，再删除Cache”</strong></p>
<ol>
<li>将变更写入到数据库中。</li>
<li>删除缓存中的对应数据。</li>
</ol>
<p><strong>衍生问题：但是我们如果考虑高并发的情况下，这种方案其实也是有问题的</strong></p>
<p>常见的问题就是，在变更数据库的时候是成功的，但是因为类似网络的问题导致删除缓存是失败的。</p>
<p><strong>延迟双删</strong></p>
<p>要能够确保删除动作一定被执行，那就可以解决问题，起码能够缩小数据不一致的时间窗口，常用的方式就是延迟双删，依然是先更新再删除，唯一不同的就是把删除的动作，在不久后再执行一次，比如说3秒后。</p>
<pre><code class="language-java">public void putValue(key, value) {
    putToDB(key, value);
    deleteFromRedis(key);

    ...deleteFromRedis(key, after3sec);
}
</code></pre>
<p>这一种方案需要具体情况具体分析，例如如果第一次删除缓存已经是抛异常了，那么就算延迟几秒钟删除，它依旧是会抛异常的。删除动作是有多种选择的。</p>
<ol>
<li>放在DelayQueue中，会有随着JVM进程的死亡，丢失更新的风险</li>
<li>放在MQ中，可能会增加架构的复杂度</li>
</ol>
<p><strong>闪电缓存</strong></p>
<p>将缓存失效的时间设置非常短，这样也可以避免数据不一致的情况</p>
<blockquote>
<p>根据业务来决定！</p>
</blockquote>
<h5>缓存多个副本不一致</h5>
<p>不采用rehash漂移策略，而采用缓存分层策略，尽量避免脏数据。</p>
<p>缓存系统中包含热点数据、常规数据、冷数据，缓存空间划分为三个层级：第一层存储热点数据，第二层存储常用数据，第三次存储相对冷的数据。每个层级都可以设置不同的缓存容量和缓存时间。</p>
<h3>数据并发竞争</h3>
<h4>问题描述</h4>
<p>数据并发竞争，是指在高并发访问场景，一旦缓存访问没有找到数据，大量请求就会并发查询DB，导致DB压力大增的现象。</p>
<p>主要是由于多个进程/线程中，有大量并发请求获取相同的数据，而这个数据Key因为正好过期、被剔除等各种原因在缓存中不存在，这些进程/线程之间没有任何协调，然后一起去并发查询DB，请求那个相同的Key，最终导致DB压力大增</p>
<p><strong>关键词：批量请求同一个Key，Key不存在</strong></p>
<h4>业务场景</h4>
<ul>
<li>车票系统，如果某个火车车次缓存信息过期，但仍然有大量用户在查询该车次信息</li>
<li>微博正好被缓存淘汰，但这条微博仍然有大量的转发、评论、赞。</li>
</ul>
<p>上述情况存在并发竞争读取的问题。</p>
<h4>解决方案</h4>
<p><strong>方案一：使用全局锁</strong></p>
<p>当缓存请求miss后，先尝试加全局锁，只有加全局锁成功的线程，才可以到DB去加载数据。其他进程/线程在读取缓存数据miss时，如果发现这个Key有全局锁，就进行等待，待之前的线程将DB的数据回写到缓存的后，再从缓存中获取。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F32.CjC8b6U3.png&amp;fm=webp&amp;w=800&amp;h=771&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<pre><code class="language-java">RLock lock = redissonClient.getLock("global_lock"); // 获取全局锁

if (cacheMiss) {
    boolean locked = false;
    try {
        locked = lock.try.tryLock(10, TimeUnit.SECONDS); // 尝试获取全局锁，等待10秒钟
       	if (locked) {
            // 获取到全局锁后，从数据库加载数据，并存储到缓存中
            loadDataFromDatabase();
            storeDataToCache();
        } else {
            // 获取全局锁失败，等待一段时间重试
            Thread.sleep(1000);
            loadDataFromCache();
        }
    } catch (InterruptedException e) {
        // 异常处理
    } final {
        if (locked) {
            // 释放
            lock.unlock();
        }
    }
} else {
    // 如果缓存可获得，直接返回
    loadDataFromCache();
}
</code></pre>
<h3>Hot Key</h3>
<h4>问题描述</h4>
<h4>原因分析</h4>
<p>Hot key引发缓存系统异常，主要是系统接受到突然热门的事件，超大量的请求访问热点事件对应的Key，大量的请求访问请求同一个Key，流量集中打在了一个缓存节点机器。</p>
<p>并发请求非常大，访问数据完全相同的请求称为热点查询。</p>
<h4>业务场景</h4>
<p>突发的事件都有可能造成Hot key的情况，秒杀、双11等。</p>
<h4>解决方案</h4>
<h5>前置缓存</h5>
<p>本地缓存，部署在应用服务中的本地缓存</p>
<p>热点查询是对相同的数据进行不断重复查询的一种场景。<strong>特点是次数多，但需要存储的数据少，因为数据是相同</strong>。针对这类业务场景，可以将热点数据前置缓存在应用程序内来应对热点查询。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F33.DOrb2THO.png&amp;fm=webp&amp;w=800&amp;h=405&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<p><strong>应用内缓存需要设置上限</strong></p>
<p>前置缓存对应的宿主机内存是有限的，还要支持业务应用使用，必须设置缓存容量的上线且设置容量满了逐出策略，LRU，将最少用的缓存在容量满的时候清理掉。</p>
<p>前置缓存需要设置过期时间。</p>
<p><strong>其次是根据业务对待延迟的问题</strong></p>
<p>前置缓存的延迟问题的解决方案要么采用定期（被动）刷新，要么采用主动刷新。</p>
<p>如果要实现感知变化，可以采用Binlog的方式，在变更时主动刷新，需要注意的是前置缓存的主动感知不设置在前置缓存所在的应用中，因为业务代码在该机器运行，通过MQ感知会消费CPU和内存资源，前置缓存的数据量小，很多变更的消息因为不是热点数据而被忽略掉，为了前置缓存的更新，可以将前置缓存的内容异构出来一份用作判断。（<strong>复杂</strong>）</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F34.B-7PRnzn.png&amp;fm=webp&amp;w=800&amp;h=589&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<h5>主动发现</h5>
<p>找出热点Key，首先可以将这些热点Key进行分散处理，例如将一个热点Key中，分散为<code>hotKey#1</code>、<code>hotKey#2</code>、<code>hotKey#3</code>将这些key分散存在多个缓存节点中，然后客户端请求时，随机访问其中某个后缀的hotkey，这样就可以把热key请求打散。</p>
<p>借助外部计数工具来实现热点的发现，可以在一个集中的位置对于请求的数据进行比较，根据配置的阈值判断请求是否会命中数据，是否触发热点Key，对于判断为热点的数据，主动推送到前置缓存中。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F35.B_YmVfrL.png&amp;fm=webp&amp;w=800&amp;h=630&amp;dpl=69dce8926406240008354a25" alt="" /></p>
<h5>降级兜底</h5>
<p>对于超出预期的流量，使用限流策略，限流的阈值设置为压测的40%-50%</p>
<h3>Big Key</h3>
<h4>问题描述</h4>
<p>大Key，是指在缓存访问时，部分Key的Value过大、读写、加载易超时的现象。</p>
<h4>原因分析</h4>
<p>如果这些大Key占总数据的比例很小，导致很容易被频繁剔除，DB反复加载。如果业务中这些大Key很多，而这种Key被大量访问，也会导致较大的Key慢查询。</p>
<p>另外，如果大Key缓存的字段较多，每个字段的变更都会引发这个缓存数据的变更，同时这些Key也会被频繁地读取，读写相互影响，也会导致慢查询现象，大Key一旦被缓存淘汰，DB加载可能需要花费很多时间，这也会导致大Key查询慢的问题。</p>
<h4>业务场景</h4>
<p>例如保存用户最新1万个粉丝的业务，一个用户个人信息缓存，包括基本资料等。</p>
<h4>解决方案</h4>
<h5>将大Key分拆为多个key，尽量减少大Key的存在</h5>
<p>由于大Key一旦穿透到DB，加载耗时很大，所以可以对这些大Key进行特殊照顾，<strong>设置较长的过期时间</strong>，缓存内部淘汰Key时，同等条件下，尽量不淘汰这些大Key。</p>
<p>避免大Key的产生，采取以下策略</p>
<ol>
<li>分解大Key：可以将大Key拆分成多个小Key，每个小Key对应的Value数据不超过Redis的内存限制，将一个包含大量数据的哈希类型，拆分成多个小的哈希类型。每个小的哈希类型只包含一部分数据。</li>
<li>压缩Value数据：可以对的Key的Value数据进行压缩，降低存储空间，从而减少Redis的内存占用，例如，使用gzip等压缩算法对Value数据进行压缩。</li>
<li>限制数据量：可以设定一个阈值，当一个Key的Value数据超过一定的大小时，自动拒绝写入或进行切分，例如，设置Redis的maxmemory参数、限制Redis内存使用量</li>
</ol>
<blockquote>
<p>删除Big Key</p>
<p>当我们侦察出大Key之后，删除大Key，<strong>切勿使用<code>DEL</code>命令</strong>，它的复杂度是O(n)，n是元素的数量。因为Redis是单线程的所以这样就会阻塞线程了。</p>
<p>使用Redis的<code>UNLIK</code>命令 ，后台开启线程删除。</p>
</blockquote>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[层式结构-时间轮]]></title>
            <link>https://agility6.me/timewheel</link>
            <guid isPermaLink="false">https://agility6.me/timewheel</guid>
            <pubDate>Sat, 25 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[后端开发常见层式结构：时间轮、跳表、LSM-Tree 海量并发的定时任务：时间轮 高并发读写的有序结构组织：跳表 空间利用率以及写性能高的磁盘数据组织：LSM-Tree 什么是层式结构（GPT）：层式结构（Layered Structure）在计算机科学和软件工程中通常指的是将系统分成若干层次，每个层次负责不同的功能和任务。这样设计的好处是可以将复杂系统的不同部分进行解耦和模块化，从而提高系统的可...]]></description>
            <content:encoded><![CDATA[<p>后端开发常见层式结构：时间轮、跳表、LSM-Tree</p>
<ol>
<li>海量并发的定时任务：时间轮</li>
<li>高并发读写的有序结构组织：跳表</li>
<li>空间利用率以及写性能高的磁盘数据组织：LSM-Tree</li>
</ol>
<blockquote>
<p>什么是层式结构（GPT）：层式结构（Layered Structure）在计算机科学和软件工程中通常指的是将系统分成若干层次，每个层次负责不同的功能和任务。这样设计的好处是可以将复杂系统的不同部分进行解耦和模块化，从而提高系统的可维护性、可扩展性和可理解性。</p>
</blockquote>
<h2>时间轮</h2>
<h3>单层级时间轮</h3>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2FSnipaste_2024-05-25_20-10-14.Bv_oTvMu.png&amp;fm=webp&amp;w=800&amp;h=444&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p>定时任务是用时间轮进行实现的，那么它是如何去组织数据的呢？</p>
<ul>
<li>一个格子代表一个时刻</li>
<li>一个格子可存储多个任务</li>
<li>按执行顺序组织数据</li>
</ul>
<h3>多层级时间轮</h3>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2FSnipaste_2024-05-25_20-17-25.BMS2rUwX.png&amp;fm=webp&amp;w=800&amp;h=359&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p>按照任务的<strong>轻重缓急</strong>来进行层次划分的，当我们的任务是在秒这个单位下需要执行的，那么只需要放在前60秒即可，那么如果任务是分、时单位下，那么只需要放在对应的层级即可。对比单层级时间轮，多层级时间轮可以<strong>减少比较的次数</strong>，因此可以提升性能。（避免任务的轮询）</p>
<h3>时间轮如何运行</h3>
<p>多层级时间轮是如何进行运作的呢？</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2FSnipaste_2024-05-25_20-27-14.BDPWZFVZ.png&amp;fm=webp&amp;w=800&amp;h=522&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p>当我们的秒针走完了60秒之后，分针相对于应该移动一格，这时候需要将分钟对应时刻的任务，映射到第一层级中。同理如果移动到时针，那么将任务，映射到分针当中。</p>
<h3>怎么设计时间轮？</h3>
<ol>
<li>设计最小时间精度； 相当于是允许的定时任务误差</li>
<li>设计最大的时间范围；超过时间范围会出现错误</li>
<li>设计最大的层级；层级的个数决定了映射的频繁程度（消耗时间）</li>
</ol>
<h3>为什么使用时间轮</h3>
<p>海量<strong>并发</strong>的定时任务，也就是说我们要找到一个适合在多线程环境中使用的数据结构，在并发中必须考虑的就是<strong>加锁</strong>的问题</p>
<pre><code class="language-tex">lock()
操作数据结构
unlock()
</code></pre>
<p>为什么说时间轮在多线程的环境下效率高呢？主要体现<strong>操作数据结构时间足够的短</strong>。需要考虑锁的粒度，锁的粒度就是操作数据结构的时间。</p>
<p>减小锁粒度有以下方法</p>
<ol>
<li>较小的时间复杂度</li>
<li>更细粒度上加锁（例如说在各个节点上加锁）</li>
</ol>
<p><strong>时间轮就是拥有较小的时间复杂度</strong>，同理任务队列也是一个效率很高的，它选用的就是在<strong>更细粒度上加锁</strong></p>
<h3>实现一个单层级的时间轮定时任务</h3>
<ul>
<li>
<p>首先创建<code>TaskElement</code>代表任务</p>
<ul>
<li>任务函数</li>
<li>所在位置</li>
<li>循环次数</li>
<li>键</li>
</ul>
</li>
<li>
<p><code>TimeWheel</code>实现</p>
<p>简单的来说，就是将时间划分为多个槽（slots），每个槽内存放任务队列。</p>
<ul>
<li>初始化：创建指定数量的槽，每个槽用一个双向链表表示，初始化调度器（<code>ScheduledExecutorService</code>），用于定期触发时间轮的<code>tick</code>操作。</li>
<li>执行<code>tick</code>该方法获取当前位置上的任务队列进行执行。</li>
<li><code>execute</code>执行函数，主要根据<code>TaskElement</code>中的<code>cycle</code>属性判断是否应该执行。</li>
</ul>
<p>添加一个任务</p>
<ul>
<li>首先需要先计算，任务的执行位置和循环次数，使用<code>getPosAndCycle</code>进行计算</li>
<li>使用返回的执行位置和循环次数，实例化<code>TaskElement</code>。</li>
</ul>
<p>删除一个任务</p>
<ul>
<li>只需要根据<code>TaskElement</code>中的<code>key</code>即可</li>
</ul>
</li>
</ul>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2FSnipaste_2024-05-26_00-11-55.DbSOdIJM.png&amp;fm=webp&amp;w=800&amp;h=333&amp;dpl=69dce8926406240008354a25" alt="photo" />
<img src="https://agility6.me/.netlify/images?url=_astro%2FSnipaste_2024-05-26_00-16-24.D0gbcwpc.png&amp;fm=webp&amp;w=800&amp;h=417&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p><a href="https://github.com/AnnularLabs/java-timewheel">代码实现</a></p>
<h3>代码解析</h3>
<h4>Duration</h4>
<p>Duration 是 Java 8 引入的时间类，位于 java.time 包中。它表示两个瞬时时间点之间的时间量。Duration 类提供了一些方法，用于创建、操作和获取时间段的信息。</p>
<p>常用方法</p>
<ul>
<li>Duration.ofDays(long days): 创建指定天数的 Duration 实例。</li>
<li>Duration.ofHours(long hours): 创建指定小时数的 Duration 实例。</li>
<li>Duration.ofMinutes(long minutes): 创建指定分钟数的 Duration 实例。</li>
<li>Duration.ofSeconds(long seconds): 创建指定秒数的 Duration 实例。</li>
<li>Duration.ofMillis(long millis): 创建指定毫秒数的 Duration 实例。</li>
<li>Duration.between(Temporal startInclusive, Temporal endExclusive): 通过两个时间点之间的差异创建 Duration。</li>
<li>Duration.parse(CharSequence text): 解析标准ISO-8601格式的 Duration 字符串。</li>
</ul>
<h4>scheduleAtFixedRate</h4>
<p>scheduler.scheduleAtFixedRate(this::tick, interval.toMillis(), interval.toMillis(), TimeUnit.MILLISECONDS); 这一行代码的作用是使用调度器定期执行 tick 方法。具体来说，这是一个基于固定速率调度的定时任务，每隔指定的时间间隔执行一次 tick 方法。</p>
<p>scheduleAtFixedRate 方法有四个参数：</p>
<ul>
<li>command: 要执行的任务，这里是 this::tick。</li>
<li>initialDelay: 第一次执行任务前的延迟时间，这里是 interval.toMillis() 毫秒。</li>
<li>period: 连续执行任务之间的周期时间，这里也是 interval.toMillis() 毫秒。</li>
<li>unit: 时间单位，这里是 TimeUnit.MILLISECONDS。</li>
</ul>
<p>scheduleAtFixedRate(this::tick, interval.toMillis(), interval.toMillis(), TimeUnit.MILLISECONDS) 的工作流程如下：</p>
<ol>
<li>初始延迟: 在 interval.toMillis() 毫秒后开始第一次执行 tick 方法。</li>
<li>固定速率执行: 每隔 interval.toMillis() 毫秒重复执行 tick 方法。</li>
</ol>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Redis的数据结构]]></title>
            <link>https://agility6.me/redis%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84</link>
            <guid isPermaLink="false">https://agility6.me/redis%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84</guid>
            <pubDate>Thu, 11 Apr 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[本篇文章主要介绍Redis数据类型，具体实现的数据结构 前言 🚀 内容来自参考自Redis设计与实现 ⚠️ 本篇文章主要介绍Redis3.0的数据结构，在Redis7.0数据类型与数据结构的关系有所不一致。 介绍逻辑 数据结构的定义 字段的解释 特性 介绍内容 简单动态字符串 链表 字典 跳跃表 整数集合 压缩列表 简单动态字符串 数据结构的定义 字段的解释 free属性的值，记录SDS存在多少...]]></description>
            <content:encoded><![CDATA[<p><strong>本篇文章主要介绍Redis数据类型，具体实现的数据结构</strong></p>
<h2>前言</h2>
<p>🚀 内容来自参考自<strong>Redis设计与实现</strong></p>
<p>⚠️ 本篇文章主要介绍Redis3.0的数据结构，在Redis7.0数据类型与数据结构的关系有所不一致。
<img src="https://agility6.me/.netlify/images?url=_astro%2F1.BJ-kde-3.png&amp;fm=webp&amp;w=800&amp;h=397&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h2>介绍逻辑</h2>
<ol>
<li>数据结构的定义</li>
<li>字段的解释</li>
<li>特性</li>
</ol>
<h2>介绍内容</h2>
<ol>
<li>简单动态字符串</li>
<li>链表</li>
<li>字典</li>
<li>跳跃表</li>
<li>整数集合</li>
<li>压缩列表</li>
</ol>
<h2>简单动态字符串</h2>
<h3>数据结构的定义</h3>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F2.D-11U-aX.png&amp;fm=webp&amp;w=800&amp;h=474&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h3>字段的解释</h3>
<ol>
<li><code>free</code>属性的值，记录SDS存在多少未使用的空间</li>
<li><code>len</code>属性，记录SDS保存多少字节长度的字符串</li>
</ol>
<h3>特性</h3>
<ol>
<li>
<p>常数复杂度获取字符串长度</p>
</li>
<li>
<p>杜绝缓冲区溢出问题，SDS在执行修改增加操作的时候，API会检查是否满足要求，如果不满足会自动扩容</p>
</li>
<li>
<p>减少修改字符串时带来的内存重分配次数，<strong>空间预分配</strong>会额外分配空间、<strong>惰性空间释放</strong>在缩短操作时，利用free属性记录数量，等待使用。</p>
</li>
</ol>
<h2>链表</h2>
<h3>数据结构的定义</h3>
<ul>
<li>由ListNode和List组成
<img src="https://agility6.me/.netlify/images?url=_astro%2F3.DkVQIf11.png&amp;fm=webp&amp;w=800&amp;h=293&amp;dpl=69dce8926406240008354a25" alt="photo" /></li>
</ul>
<h3>字段的解释</h3>
<h4>ListNode</h4>
<ol>
<li>前置节点</li>
<li>后置节点</li>
<li>节点的值</li>
</ol>
<h4>List</h4>
<ol>
<li><code>head</code>表头指针</li>
<li><code>tail</code>表尾指针</li>
<li><code>len</code>链表长度计数器</li>
<li><code>dup</code>函数用于复制链表节点所保存的值</li>
<li><code>free</code>函数用于释放链表节点所保存的值</li>
<li><code>match</code>函数用于对比链表节点所保存的值和另一个输入值是否相等</li>
</ol>
<h3>特性</h3>
<ol>
<li>双端、无环</li>
<li>多态：链表节点使用void*指针来保存节点值，并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数，所以链表可以用于保存各种不同类型的值。</li>
</ol>
<h2>字典</h2>
<h3>数据结构的定义</h3>
<ul>
<li>由哈希表和哈希节点组成，每个哈希节点保存了字典中的一个键值对
<img src="https://agility6.me/.netlify/images?url=_astro%2F4.BbtY6KK7.png&amp;fm=webp&amp;w=800&amp;h=454&amp;dpl=69dce8926406240008354a25" alt="photo" /></li>
</ul>
<h3>字段的解释</h3>
<h4>哈希表</h4>
<ul>
<li>table是一个数组，是<code>dictEntry</code>类型的数组。</li>
<li>size记录属性。</li>
<li>used哈希表已经有的节点。</li>
<li><code>sizemask</code>属性的值总是等于<code>size - 1</code>，这个属性和哈希值一起决定了一个键应该被放到table数组 的哪个索引上面。</li>
</ul>
<h4>哈希表节点</h4>
<ul>
<li>key保存键值对中对键</li>
<li>v属性保存键值中的值：可能是指针、uint64_t整数、或者int64_t整数</li>
<li>next属性是指向另一个哈希表节点的指针，*<em>用于解决冲突</em></li>
</ul>
<h4>字典</h4>
<ul>
<li>type和privdata属性是针对不同类型的键值对，为<strong>创建多态字典设置的</strong>
<ul>
<li>type指向dictType指针，dictType保存了针对特定类型键值对的函数</li>
<li>privdata属性保存了需要传给类型特定函数的可选参数</li>
</ul>
</li>
<li>ht属性包含<strong>两个项数组</strong>，每一个项都是一个哈希表，<strong>⚠️一般情况下只会使用<code>ht[0]</code>，<code>ht[1]</code>是进行rehash的时候使用</strong></li>
<li>rehashidx是与rehash有关的属性，记录当前rehash的进度，-1（没有进行rehash）。</li>
</ul>
<h3>特性</h3>
<ol>
<li>
<p>解决键冲突，使用<strong>链地址法</strong>来解决冲突，简单来说就是利用<strong>哈希表节点中的next属性</strong>将冲突节点放到链表的表头位置</p>
</li>
<li>
<p>rehash（重新散列），当哈希表保存的键值对数量太多或者太少时，程序需要对哈希表的大小进行相应的扩展或者收缩。</p>
</li>
<li>
<p>渐进式rehash，在进行渐进式rehash的时候，字典里面查找一个键的话，程序会先在<code>ht[0]</code>里面进行查找，如果没找到的话，就会继续到<code>ht[1]</code>里面进行查找。</p>
</li>
</ol>
<h2>跳跃表</h2>
<h3>数据结构的定义</h3>
<ul>
<li>跳跃表由跳跃表节点（zskiplistNode）和用于保存跳跃表节点的相关信息组成（zskiplist）
<img src="https://agility6.me/.netlify/images?url=_astro%2F5.CUsPKT5W.png&amp;fm=webp&amp;w=800&amp;h=536&amp;dpl=69dce8926406240008354a25" alt="photo" /></li>
</ul>
<h3>字段的解释</h3>
<h4>zskiplist</h4>
<ul>
<li>header指向跳跃表的表头节点</li>
<li>tail指向跳跃表的表尾节点</li>
<li>level记录目标跳跃表内，层数最大的那个节点的层数（表头节点的层数不计算在内）</li>
<li>length记录跳跃表的长度，也就是跳跃表目前包含节点的数量（表头节点不计算在内）</li>
</ul>
<h4>zskiplistNode</h4>
<ul>
<li>层（level）节点中用L1、L2等字样标记节点的各个层，每一个层都带有两个属性
<ul>
<li>前进指针（<code>level[i].forward</code>）</li>
<li>跨度，记录前进指针所指向节点和当前节点的距离，用于计算排位</li>
</ul>
</li>
<li>后退指针，指向位于当前节点的前一个节点</li>
<li>分值，在跳跃表中，节点按照各自所保存的分值从小到大进行排序</li>
<li>成员对象，节点所保存的成员变量，它指向一个字符串对象（SDS）</li>
</ul>
<h3>特性</h3>
<ol>
<li>跳跃表中的节点按照分值进行排序，当分值相同时，节点按照成员对象的大小进行排序</li>
</ol>
<h2>整数集合</h2>
<h3>数据结构的定义</h3>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F6.CUXxZseT.png&amp;fm=webp&amp;w=800&amp;h=432&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h3>字段的解释</h3>
<ul>
<li>contents整数集合中的每一个元素都是contents数组的一个数据项（item），从小到大排序、没有重复</li>
<li>length记录了整数集合包含的元素数量，也就是contents数组的长度</li>
<li>encoding属性决定集合数组保存的数据类型（例如contents为int8_t类型的数据，但是实际上contents数组真正类型取决于encoding属性的值）</li>
</ul>
<h3>特性</h3>
<ol>
<li>集合中不会出现重复元素</li>
<li>整数集合升级，发生在添加新元素的类型比原来的类型要大
<ul>
<li>整数集合升级，提高整数集合的灵活性（C语言是静态类型语言，通常不会将两种类型的值放在同一个数据结构当中）</li>
<li>整数集合升级，尽可能节约内存</li>
</ul>
</li>
</ol>
<h2>压缩列表</h2>
<h3>数据结构的定义</h3>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F7.DgI-QK0g.png&amp;fm=webp&amp;w=800&amp;h=358&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h3>字段的解释</h3>
<h4>压缩列表的构成</h4>
<ul>
<li>zlbytes记录整个压缩列表占用的内存字节数；在对压缩列表进行内存重分配，或者计算zlend的位置时使用</li>
<li>zltail记录压缩列表表尾节点距离压缩列表的起始地址有多少字节</li>
<li>zllen记录压缩列表包含的节点数量</li>
<li>entryX压缩列表包含的各个节点</li>
<li>zlend特殊值，用于标记压缩列表的末端</li>
</ul>
<h4>压缩列表节点的构成</h4>
<ul>
<li>每一个压缩列表节点可以保存<strong>一个字节数组</strong>或者<strong>一个整数值</strong></li>
<li>previous_entry_length记录前一个节点的长度
<ul>
<li>如果前一节点的长度小于254字节，那么它的长度为1字节</li>
<li>如果前一节点的长度大于254字节，那么它的长度为5字节</li>
<li>因为previous_entry_length属性记录了前一个节点的长度，因此可以通过指针运算，获取前一个节点的起始地址。</li>
</ul>
</li>
<li>encoding记录节点的content属性所保存数据的类型以及长度</li>
<li>content节点的content属性负责保存节点的值，节点值可以是一个字节数组或者整数，值的类型和长度由节点的encoding决定。</li>
</ul>
<h3>特性</h3>
<ol>
<li>连锁更新，previous_entry_length属性都记录了前一个节点的长度，因此可能会发现连锁更新（发生的几率不高）</li>
</ol>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Binary Search Tree]]></title>
            <link>https://agility6.me/binary-search-tree</link>
            <guid isPermaLink="false">https://agility6.me/binary-search-tree</guid>
            <pubDate>Tue, 12 Mar 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[BinarySearchTree定义 二叉搜索树是二叉树的一种。 任意一个节点的值都大于其左子树所有节点的值。 任意一个节点的值都小于其右子树所有节点的值。 它的左右子树也是一颗二叉搜索树。 设计一颗二叉树 树中节点的设计 节点的值 左孩子 右孩子 当前节点的父节点 判断当前节点是否为叶子节点 判断当前节点度是否为2 public class Node { public int element; ...]]></description>
            <content:encoded><![CDATA[<h2>BinarySearchTree定义</h2>
<ul>
<li>
<p>二叉搜索树是二叉树的一种。</p>
</li>
<li>
<p><strong>任意一个节点的值都大于其左子树所有节点的值</strong>。</p>
</li>
<li>
<p><strong>任意一个节点的值都小于其右子树所有节点的值</strong>。</p>
</li>
<li>
<p>它的左右子树也是一颗二叉搜索树。</p>
</li>
</ul>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F1.Dbs-AmXZ.png&amp;fm=webp&amp;w=800&amp;h=542&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h2>设计一颗二叉树</h2>
<h3>树中节点的设计</h3>
<ol>
<li>
<p>节点的值</p>
</li>
<li>
<p>左孩子</p>
</li>
<li>
<p>右孩子</p>
</li>
<li>
<p>当前节点的父节点</p>
</li>
<li>
<p>判断当前节点是否为叶子节点</p>
</li>
<li>
<p>判断当前节点度是否为2</p>
</li>
</ol>
<pre><code class="language-java">public class Node {

    public int element; // 值

    public Node left; // 左孩子

    public Node right; // 右孩子

    public Node parent; // 父节点

    /**
     * 必须传入当前节点的值以及父节点
     * @param element
     * @param parent
     */
    public Node(int element, Node parent) {
        this.element = element;
        this.parent = parent;
    }

    public boolean isLeaf() {
        return this.left == null &amp;&amp; this.right == null;
    }

    public boolean NodeDegreeTwo() {
        return this.left != null &amp;&amp; this.right != null;
    }

    @Override
    public String toString() {
        return "Node{" +
                "element=" + element +
                '}';
    }
}
</code></pre>
<h3>BinarySearchTree Class设计</h3>
<ul>
<li>
<p>定义root根节点</p>
</li>
<li>
<p>定义树的size</p>
</li>
</ul>
<h2>add方法 ➕</h2>
<p>首先，需要明白的是一颗二叉搜索树的规则是人为规定的， 因此在本次设计中遵守，左子树的值是小于父节点的，右子树的值是大于父节点的。</p>
<p>在实现增加方法的时候，需要严格按照规定的进行增加</p>
<ol>
<li>
<p>首先如果root为null那么直接将新节点作为root即可</p>
</li>
<li>
<p>根节点不为空，那么需要找到<strong>一个节点</strong>作为新节点的父节点</p>
</li>
<li>
<p>找到之后只需要判断插入该节点的左子树还是右子树</p>
</li>
</ol>
<p>重点分析根节点不为空的情况</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F2.BKIt6AxJ.png&amp;fm=webp&amp;w=800&amp;h=608&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p>根据上述的示意图，不难发现我们需要两个变量来记录。<strong>待插入新节点的父节点</strong>，<strong>插入的是左子树还是右子树</strong></p>
<p>这是核心代码，当<code>node == null</code>意味着<code>insertParentNode</code>就是<strong>待插入新节点的父节点</strong>，而<code>cmp</code>变量存放的就是用于判断，<strong>插入的是左子树还是右子树</strong></p>
<pre><code class="language-java">Node node = root;
Node insertParentNode = root;
int cmp = 0; // 记录插入左子树还是右子树

while (node != null) {
    insertParentNode = node;
    cmp = cmp(node.element, element);
    if (cmp == 1) { // node.element &gt; element 左边
        node = node.left;
    } else { // node.element &lt;= element 右边
        node = node.right;
    }
}
</code></pre>
<h2>remove方法 ➖</h2>
<p>在实现删除功能的时候，需要保证时刻满足二叉搜索树的性质。</p>
<p>例如：如果我想删除下图的元素，应该怎么做，肯定需要找到一个元素来代替它，代替之后必须满足二叉搜索树的性质，需要用到<strong>前驱后继节点</strong></p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F3.Whk6zF-x.png&amp;fm=webp&amp;w=800&amp;h=550&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h3>前驱后继节点</h3>
<p>前驱后继的定义就是：按照二叉树的中序遍历排列，某个数的前一个节点就为前驱节点，某个数的后一个节点就为后继节点</p>
<p>例如上图，按照中序遍历得到的是<code>13 15 17 20 25 30</code>。那么<code>20</code>的前驱节点就是<code>17</code>，后继节点就是<code>25</code></p>
<p><strong>利用这个特点，只需要找到前驱或者后继来代替待删除节点即可</strong></p>
<h3>实现</h3>
<p>如果实现根据任意一个节点找到后继节点呢？（前驱也是一致的）</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F4.DgUbnQPw.png&amp;fm=webp&amp;w=800&amp;h=579&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<pre><code class="language-java">private Node successor(Node node) {

    Node p = node.right;

    // 存在右子树的情况
    if (p != null) { // node.right.left.left...
        while (p.left != null) {
            p = p.left;
        }
        return p;
    }

    // 不存在右子树的情况向上寻找
    while (node.parent != null &amp;&amp; node != node.parent.left) {
        node = node.parent;
    }

    // 当node == node.parent.left这时候node.parent就是答案节点
    // 当node.parent == null无后继直接返回node.parent也是可以
    return node.parent;
}
</code></pre>
<h3>删除元素 — 分类讨论</h3>
<ol>
<li>
<p>当删除的节点度为0</p>
<p>那么直接将<code>node.parent.left = null</code>或者<code>node.parent.right = null</code></p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F5.2hVRslkZ.png&amp;fm=webp&amp;w=800&amp;h=477&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
</li>
<li>
<p>当删除的节点度为1</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F6.D8jcx06z.png&amp;fm=webp&amp;w=800&amp;h=624&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
</li>
<li>
<p>当删除节点的度为2</p>
<p>其实这一种情况就是删除度为1或者度为0，因为我们是直接获取前驱或者后继节点代替它，之后直接处理代替节点即可。</p>
</li>
</ol>
<p>情况一：</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F7.DIu72bA9.png&amp;fm=webp&amp;w=800&amp;h=743&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p>情况二：</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F8.DsBxnMop.png&amp;fm=webp&amp;w=800&amp;h=691&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h4>边界情况</h4>
<p>因为删除度为2的节点，最终可以被归结为删除度为0或者1，下面是核心代码</p>
<pre><code class="language-java">if (r != null) { // 度为1的情况
    // 下一个节点的父节点指向node.parent
    r.parent = node.parent;
    // node.parent父节点指向下一个节点（判断是左子树还是右子树）
    if (node.parent == null) { // 当node父节点为空的话，根节点
       root = r;
    } else if (node.parent.left == node) { // 是左子树
        node.parent.left = r;
    } else {
        node.parent.right = r;
    }
} else if (node.parent == null) { // 删除的节点是叶子节点，且是root节点
   root = null;
} else {
    // 判断连接左子树还是右子树即可
    if (node == node.parent.left) node.parent.left = null;
    else node.parent.right = null;
}
</code></pre>
<ol>
<li>
<p><code> if (node.parent == null) {} // 当node父节点为空的话，根节点</code></p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F9.CGr-Vt1N.png&amp;fm=webp&amp;w=800&amp;h=594&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
</li>
<li>
<p><code>node.parent == null // 删除的节点是叶子节点，且是root节点</code></p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F10.Dbd6uMo4.png&amp;fm=webp&amp;w=800&amp;h=520&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
</li>
</ol>
<h2>✅ 具体实现可参考该仓库</h2>
<p><a href="https://github.com/Agility6/DataStructure-Java.git">https://github.com/Agility6/DataStructure-Java.git</a></p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[2023总结🏃]]></title>
            <link>https://agility6.me/2023</link>
            <guid isPermaLink="false">https://agility6.me/2023</guid>
            <pubDate>Sun, 31 Dec 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[主旋律：回想2023一整年应该都是备考 ➕ 实习以及匆忙的校园生活 一些 2023过年期间和朋友自驾游去了一趟广西桂林🙅（其实是陪朋友去找他女朋友）参观一些风景区没太多印象了，有意思的其实是和朋友一起在车上又冷又塞车通宵赶回家。 过完年就回学校备考了，和一位玩的很好的朋友约定每天三点一线的备考，每天约定俗成的在宿舍门口汇合，吃早餐…计划着要赶在学生下课之前去吃饭。 期间还在去图书馆的路上选一个棵...]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>主旋律：回想2023一整年应该都是备考 ➕ 实习以及匆忙的校园生活</p>
</blockquote>
<h3>一些</h3>
<p>2023过年期间和朋友<s>自驾游去了一趟广西桂林🙅</s>（其实是陪朋友去找他女朋友）参观一些风景区没太多印象了，有意思的其实是和朋友一起在车上又冷又塞车通宵赶回家。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2FTweelet1.BnnRmW3_.png&amp;fm=webp&amp;w=800&amp;h=637&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p>过完年就回学校备考了，和一位玩的很好的朋友约定每天三点一线的备考，每天约定俗成的在宿舍门口汇合，吃早餐…计划着要赶在学生下课之前去吃饭。</p>
<p>期间还在去图书馆的路上选一个棵树记录它能不能在考试前长出来🌲,很快就迎来了考试。</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2FTweelet2.Bf51T23w.png&amp;fm=webp&amp;w=800&amp;h=717&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p>在第一门💥的情况下，也迎来了两天考试睡眠一共不足4个小时，而周一立马就去实习了毫无喘息…</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2FTweelet3.BQRw7FQ7.png&amp;fm=webp&amp;w=800&amp;h=1584&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h3>实习😈</h3>
<p>在备考期间学校恰好举行了招聘会，于是抽出半天时间想着能不能碰一碰运气，现在回想23届的校招可以说是地狱级难度（希望25届不要！）当时刷牛客的时候不少23届的同学都在抱怨。</p>
<p>然后在招聘会现场随便走了一圈，开发岗位少的可怜。抱着来都来了的心态，我也开始我的面霸模式。好在简历打印的够多，直接上去就问"贵公司招xxx吗？"。</p>
<p>较为幸运的是，通过这种方式获得了一次实习工作💼。</p>
<p><strong>所有都是全新的</strong></p>
<ol>
<li>
<p>在知道自己考砸的情况下，第二天马不停蹄的就要赶去实习了。夹杂着各种情绪开始了第一天的实习，<strong>第一次</strong>不同的身份（打工人）赶公交、一路小跑、下着雨。</p>
</li>
<li>
<p>实习第二大难题租房，<strong>第一次</strong>租房（感谢好同事第一天就花时间带我去🙏）各种找各种电话，幸运的是第一天就发现一个还不错的。随之而来的就是恐惧，完全陌生的环境，甚至连家到公司的路都不记得。</p>
</li>
<li>
<p><strong>第一次</strong>在公司接领导给的任务，各种害怕担心自己不会✋</p>
</li>
<li>
<p>…</p>
</li>
</ol>
<p><a href="https://www.bilibili.com/video/BV1Bz4y177MS/?vd_source=744f8c9aefe6e026afd715c0d198f65f#reply175672958272">随机帮毕业生搬家，竟误入美女宿舍…</a>无意中刷到的视频，狠狠的共情了
<img src="https://agility6.me/.netlify/images?url=_astro%2FTweelet4.fj_PkVFi.png&amp;fm=webp&amp;w=800&amp;h=394&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<p><strong>✌️</strong></p>
<p>花了不少时间调整心态，慢慢习惯周围，好像也没那么糟糕。发现自己住的房子离地铁好像也就几百米，旁边体育馆、市图书馆、公园等等🤣。</p>
<p>工作没有PUA！不加班！家出发骑单车只需要10分钟！因为公司是在研究院中，所以每天早上不需要挤电梯！简直世外桃源！下班路上悠闲的看晚霞！🌇</p>
<p>感谢我的好朋友能够随叫随到的陪我逛逛吃吃🍽！️</p>
<p><strong>实习总结</strong></p>
<p>感谢我实习期间能遇到超级好的leader（同事），在我思考自己的开发方向时能给我很好的指导，并且我确定向转后端方向，能够无条件的信任分发任务给我，这是第一次的实习许多都是我第一次去经历，万事开头难吧，最开始的时候压力是无比的大，没有情绪出口…</p>
<blockquote>
<p>被那些宏大的抽象的条条框框间隔的人们也许只需要一场真实的相遇</p>
</blockquote>
<p><strong>一些照片</strong></p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2FTweelet5.xlR23v9H.png&amp;fm=webp&amp;w=800&amp;h=633&amp;dpl=69dce8926406240008354a25" alt="photo" />
<img src="https://agility6.me/.netlify/images?url=_astro%2FTweelet6.VVV4Zvhm.png&amp;fm=webp&amp;w=800&amp;h=1025&amp;dpl=69dce8926406240008354a25" alt="photo" />
<img src="https://agility6.me/.netlify/images?url=_astro%2FTweelet9.V9r6uRUb.png&amp;fm=webp&amp;w=800&amp;h=633&amp;dpl=69dce8926406240008354a25" alt="photo" />
<img src="https://agility6.me/.netlify/images?url=_astro%2FTweelet8.O1TgZKES.png&amp;fm=webp&amp;w=800&amp;h=633&amp;dpl=69dce8926406240008354a25" alt="photo" />
<img src="https://agility6.me/.netlify/images?url=_astro%2FTweelet7.kjJZHITM.png&amp;fm=webp&amp;w=800&amp;h=1025&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h3>校园</h3>
<blockquote>
<p>先祈祷未来的就业难度降低点吧！</p>
</blockquote>
<p>第一学期的课程爆满！课多作业多实验多😷</p>
<p>在机缘巧合下和我校集训队acmer交流了一番，只能感慨年轻真好😭</p>
<p>上学期即将结束的时候，应差阳错的加入了倪老师的项目组，终于在每天一堆无意义的课程中找到了乐趣。</p>
<p>虽然说重返了校园生活但是更多感觉还是压力，希望能在<strong>2024</strong>🐲能顺利找到实习吧！</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2FTweelet10.D3pQoWVL.png&amp;fm=webp&amp;w=800&amp;h=633&amp;dpl=69dce8926406240008354a25" alt="photo" /></p>
<h3>🤔️</h3>
<p>陪伴了我2023年的年度歌曲！🎵：“So Small slow relaxing ambient”(保留QQ音乐的唯一理由)</p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Trie Tree]]></title>
            <link>https://agility6.me/trie</link>
            <guid isPermaLink="false">https://agility6.me/trie</guid>
            <pubDate>Sun, 15 Oct 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Trie数可以快速的存储和查询字符串集合 用法 假设给定一组字符串abc,ab,bcf,abb,ab。最后需要去查找是否存在多少个ab字符串 使用Trie快速的存储给定的字符串 从根节点开始 遍历每个字符串，判断该节点上是否存在相同子节点 如果存在则继续，不存在则创建 将最后一个字符标记，用于表示当前字符有存在一个字符串 视图 最后查找存在多少个ab字符串，只需要取出b字符对应的个数就行了 代码实...]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>Trie数可以快速的存储和查询字符串集合</p>
</blockquote>
<h3>用法</h3>
<p>假设给定一组字符串<code>abc</code>,<code>ab</code>,<code>bcf</code>,<code>abb</code>,<code>ab</code>。最后需要去查找是否存在多少个<code>ab</code>字符串</p>
<ul>
<li>
<p>使用Trie快速的存储给定的字符串</p>
<ol>
<li>从根节点开始</li>
<li>遍历每个字符串，判断该节点上是否存在相同子节点</li>
<li>如果存在则继续，不存在则创建</li>
<li>将最后一个字符标记，用于表示当前字符有存在一个字符串</li>
</ol>
</li>
<li>
<p>视图</p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2Ftrie%E5%9B%BE%E8%A7%A3.Bgziwdb7.png&amp;fm=webp&amp;w=800&amp;h=482&amp;dpl=69dce8926406240008354a25" alt="trie图解" /></p>
</li>
<li>
<p>最后查找存在多少个<code>ab</code>字符串，只需要取出<code>b</code>字符对应的个数就行了</p>
</li>
</ul>
<h3>代码实现</h3>
<ul>
<li>
<p>重要通过数组去模拟trie</p>
<p>主要要清楚<code>son[][]</code>, <code>cnt[]</code>, <code>index</code>变量的含义</p>
<ul>
<li>
<p>N的大写取决于题目中最大的节点数，也就是最多有多少长度的字符串</p>
</li>
<li>
<p>26则是默认规定都是小写字母那么最多只有26个字母</p>
</li>
<li>
<p><code>son[][]</code>son是存储当前节点的子节点的下标</p>
<p>🌰: <code>son[0][1] = 2;</code> 代表的是下标为0的节点，有子节点这个子节点为“1”（注意这个1取决与你怎么定义的，例如我可以将a表示为1，这里姑且<strong>看作字符<code>a</code></strong>）2则代表这个子节点的下标</p>
<p><code>son[2][2] = 3</code> 仅接着上面的，这个代表的意思就是，下标为2的节点（也就是上面的“1”，看作字符a）a的子节点为“2”（这里姑且看作字符<code>b</code>。<strong>取决于你</strong>），3则代表这个子节点的下标</p>
<p>以此类推…</p>
</li>
</ul>
<pre><code class="language-java">
  int N = 100010;
  int[][] son = new int[N][26];
  int ent[N];
  int index = 0;

</code></pre>
</li>
</ul>
<h4>插入函数</h4>
<p>回忆一下步骤，先判断是否在trie树上，如果不存在则创建节点，存在则<strong>切换为对应的下标</strong>，最后将<code>cnt[]</code>数组对应的最后一个字符自增</p>
<pre><code class="language-java">
  public void insert(String str) {

    int p = 0; // 存储节点的下标
    for (int i = 0; i &lt; str.length(); i++) {

      int u = str.charAt(i) - 'a'; // 将son[][u] u语义化，这里将小写字母语义化为int

      if (son[p][u] == 0) {
        son[p][u] = ++index; // 创建节点，节点的下标通过index生成
      }

      p = son[p][u]; // 当前字符存在，则切换到该节点
    }

    cnt[p]++; // 遍历到最后一个字符说明添加完成，进行标记
  }

</code></pre>
<h4>查询</h4>
<p>查询的逻辑也十分类似，只要遍历到字符串中有字符不存在节点则说明，没有该字符串。当遍历到最后一个字符说明有该字符串，直接获取<code>cnt[]</code>中记录的个数即可</p>
<pre><code class="language-java">
  public int query(String str) {

    int p = 0;

    for (int i = 0; i &lt; str.length(); i++) {

      int u = str.charAt(i) - 'a';

      if (son[p][u] == 0) return 0;

      p = son[p][u];
    }

    return cnt[p];
  }

</code></pre>
<h3>重点</h3>
<ul>
<li>trie树思路其实并不复杂，重点在于使用二维数组去构建</li>
</ul>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[cs61b-sp21-project0-2048]]></title>
            <link>https://agility6.me/cs61b-pro0</link>
            <guid isPermaLink="false">https://agility6.me/cs61b-pro0</guid>
            <pubDate>Tue, 10 Oct 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[project0实现一个2048游戏，大部分逻辑其实已经写好了，只需要把目光放在Model.java中主要的任务实现以下几个函数 emptySpaceExists maxTileExists atLeastOneMoveExists tilt 前三个较为简单这里就不多赘述，重点实现tile方法 Tile method实现 在写tile函数之前务必阅读文档，了解一下函数的用法以及作用 Border的...]]></description>
            <content:encoded><![CDATA[<p>project0实现一个2048游戏，大部分逻辑其实已经写好了，只需要把目光放在<code>Model.java</code>中主要的任务实现以下几个函数</p>
<ul>
<li><code>emptySpaceExists</code></li>
<li><code>maxTileExists</code></li>
<li><code>atLeastOneMoveExists</code></li>
<li><code>tilt</code></li>
</ul>
<blockquote>
<p>前三个较为简单这里就不多赘述，重点实现tile方法</p>
</blockquote>
<h2>Tile method实现</h2>
<p>在写tile函数之前务必阅读文档，了解一下函数的用法以及作用</p>
<ul>
<li>Border的tile</li>
<li>move</li>
<li>setViewingPerspective</li>
</ul>
<p>完成以下检验是否真正的理解需求</p>
<ul>
<li><a href="https://forms.gle/pubhRx4fxYnPTGNX8">Google Form quiz</a></li>
<li><a href="https://forms.gle/AGrhEFbwfMJ7qwaB6">Google Form quiz</a></li>
</ul>
<h3>实现</h3>
<p>根据Tips的提示，我们可以先从只考虑向上移动来进行分析。Border.tile(c, r)的行为是一列一列进行遍历的。</p>
<pre><code class="language-java">  for (int c = 0; c &lt; border.size(); c++) {
    for (int r = 0; r &lt; border.size(); r++) {
      // 具体实现逻辑，针对该列进行操作
    }
  }
</code></pre>
<p>判断是否合并其实就是寻找该列中是否满足某个性质，如果满足则移动到<code>X</code>行。需要维护每一列的数据。因为文档提供了<code>setViewingPerspective</code>，因此只需要专注实现向上移动的逻辑，如果我们的循环是从顶行开始，会方便许多，因为向上移动是最简单的情况，如果某一列上的顶行没有元素，则直接移动到顶行即可。</p>
<pre><code class="language-java">  for (int c = board.size() - 1; c &gt;= 0; c--) {

    int[] x = new int[4];

    for (int r = board.size() - 2; r &gt;= 0; r--) {

      // 具体实现逻辑，针对该列进行操作
    }
  }
</code></pre>
<p>维护了一个数组<code>x</code>用于记录当前行中有什么元素，用于判断是否合并，所以大体的框架就可以写出来了</p>
<pre><code class="language-java">  for (int c = board.size() - 1; c &gt;= 0; c--) {

    int[] x = new int[4];

    // 将顶行元素添加到辅助数组中
    if (board.tile(c, 3) != null) x[3] = board.tile(c, 3).value();

    for (int r = board.size() - 2; r &gt;= 0; r--) {

      if (board.tile(c, r) != null) {

        int currentValue = board.tile(c, r).value();

        Tile t = board.tile(c, r);

        x[r] = currentValue;

        // 获取更新到某行
        // int moveStep = TODO

        // TODO Score实现

        board.move(c, moveStep, t);

        // TODO 更新辅助数组值
        changed = true;
      }
    }
  }
</code></pre>
<p>现在只需要考虑如何获取该移动到多少行，以及怎么更新移动后到数组</p>
<ul>
<li>
<p>合并</p>
<p><strong>当前元素到顶行中，遇到的第一个非0的数，与其相等</strong></p>
</li>
<li>
<p>不合并</p>
<p><strong>当前元素到顶行中，遇到的第一个非0的数，与其不相等</strong></p>
</li>
</ul>
<h4>获取Step函数</h4>
<pre><code class="language-java">    private int getMoveStep(int[] x, int col, int row, int currentValue, int len) {

        // 记录移动到多少行
        int res = 0;

        for (res = row; res &lt; x.length - 1 - len; res++) {
            // 当前位置上的元素不等于上面的元素且上一个元素不等于零则不动
            if (currentValue != x[res + 1] &amp;&amp; x[res + 1] != 0) return res;
            // 当前元素等于上一个元素 当起行+1
            else if (currentValue == x[res + 1]) return res + 1;
        }
        return res;
    }

</code></pre>
<p>我们还需要维护一个<code>len</code>，寻找第一个非0的数，边界应该是顶行元素，注意这里的顶行元素不应该是被合并过的值（如下图</p>
<ul>
<li>
<p>当我们遍历到蓝色的4，这时候我们就不能以最初的顶行为标准了</p>
</li>
<li>
<p>那len是从哪里来的，len的大小应该与合并的次数有关，只有当该列进行了合并，才会修改边界</p>
</li>
</ul>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81len.BIl7CFqt.png&amp;fm=webp&amp;w=800&amp;h=419&amp;dpl=69dce8926406240008354a25" alt="为什么需要len" /></p>
<h4>update函数</h4>
<p>在执行完move函数，循环遍历该列更新辅助数组</p>
<pre><code class="language-java">  private void update(int[] x, int col) {

    for (int r = 0; r &lt; board.size(); r++) {
      if (board.tile(col, r) != null) {
        x[r] = board.tile(col, r).value();
      } else {
        x[r] = 0;
      }
    }
  }

</code></pre>
<h4>计算Score函数</h4>
<ul>
<li>
<p>借助<code>moveStep</code>的值，只需要判断，移动的位置元素的值，是否与它本身元素值相等</p>
</li>
<li>
<p>并且返回的<code>moveStep</code>不能与它本身的位置相等。</p>
</li>
</ul>
<pre><code class="language-java">  if (board.tile(c, moveStep) != null &amp;&amp; moveStep != r) {
    score += 2 * board.tile(c, moveStep).value();
    len++;
  }
</code></pre>
<h3>完成</h3>
<p><a href="https://github.com/Agility6/cs61b-sp21/blob/main/proj0/game2048/Model.java#L109">可参考</a></p>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2F%E5%AE%8C%E6%88%90.CI2AI4ZC.png&amp;fm=webp&amp;w=800&amp;h=435&amp;dpl=69dce8926406240008354a25" alt="done" /></p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[快速探索Fast-DDS]]></title>
            <link>https://agility6.me/fast-dds</link>
            <guid isPermaLink="false">https://agility6.me/fast-dds</guid>
            <pubDate>Thu, 18 May 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[前言 本篇文章主要介绍，我在第一次接触FastDDS所遇到的问题。对于一个陌生的知识，我认为应该去初步了解它是做什么的、并且跑通一个最小DEMO。 参考资料 https://cloud.tencent.com/developer/article/1999079 https://fast-dds.docs.eprosima.com/en/latest/ 初步了解 通过参考资料的介绍，这几个概念是比较...]]></description>
            <content:encoded><![CDATA[<h3>前言</h3>
<p>本篇文章主要介绍，我在第一次接触FastDDS所遇到的问题。对于一个陌生的知识，我认为应该去初步了解它是做什么的、并且跑通一个最小DEMO。</p>
<p>参考资料
<a href="https://cloud.tencent.com/developer/article/1999079">https://cloud.tencent.com/developer/article/1999079</a>
<a href="https://fast-dds.docs.eprosima.com/en/latest/">https://fast-dds.docs.eprosima.com/en/latest/</a></p>
<h3>初步了解</h3>
<p>通过参考资料的介绍，这几个概念是比较关键的</p>
<ul>
<li>
<p>Publisher:它是负责创建和配置其实现的 DataWriters 的 DCPS 实体。 DataWriter 是负责实际发布消息的实体。每个 DataWriter 都有一个分配的 Topic，在该 Topic 下发布消息。</p>
</li>
<li>
<p>Subscriber:它负责接收在其订阅的 Topic下发布的数据。它为一个或多个 DataReader 对象提供服务，这些对象负责将新数据的可用性传达给应用程序。</p>
</li>
<li>
<p>Topic(话题):它是绑定发布和订阅的实体。它在 DDS 域中是唯一的。通过TopicDescription，它允许发布和订阅数据类型的统一。</p>
</li>
<li>
<p>Domain(领域):这是用于链接所有发布者和订阅者的概念，属于一个或多个应用程序，它们在不同主题下交换数据。这些参与域的单个应用程序称为 DomainParticipant。 DDS 域由域 ID 标识。 DomainParticipant 定义域 ID 以指定它所属的 DDS 域。具有不同 ID 的两个 DomainParticipants 不知道彼此在网络中的存在。因此，可以创建多个通信通道。这适用于涉及多个DDS应用程序的场景，它们各自的 DomainParticipants 相互通信，但这些应用程序不得干扰。 DomainParticipant 充当其他 DCPS 实体的容器，充当发布者、订阅者和主题实体的工厂，并在域中提供管理服务。</p>
</li>
</ul>
<p>对于DEMO的实现，重点关注<code>Publisher</code>和<code>Subscriber</code>也就是发送和接收这两步</p>
<h3>DEMO实现</h3>
<ul>
<li>
<p>对于系统的选择，Windows/Mac OC/Linux。如果你是Windows系统，强烈建议你使用WSL，众所周知Windows在环境搭建上十分的劝退。这里我使用WSL</p>
</li>
<li>
<p>安装方式<code>Colcon installation</code>、<code>bin</code>、<code>Source</code>、<code>docker image</code>。这里我使用Source安装</p>
</li>
</ul>
<pre><code class="language-bash">mkdir ~/Fast-DDS
</code></pre>
<ol>
<li>Foonathan memory</li>
</ol>
<pre><code class="language-bash">  cd ~/Fast-DDS
  git clone https://github.com/eProsima/foonathan_memory_vendor.git
  mkdir foonathan_memory_vendor/build
  cd foonathan_memory_vendor/build
  cmake .. -DCMAKE_INSTALL_PREFIX=~/Fast-DDS/install -DBUILD_SHARED_LIBS=ON
  cmake --build . --target install
</code></pre>
<ol>
<li>Fast CDR</li>
</ol>
<pre><code class="language-bash">cd ~/Fast-DDS
git clone https://github.com/eProsima/Fast-CDR.git
mkdir Fast-CDR/build
cd Fast-CDR/build
cmake .. -DCMAKE_INSTALL_PREFIX=~/Fast-DDS/install
cmake --build . --target install
</code></pre>
<ol>
<li>install eProsima Fast DDS</li>
</ol>
<pre><code class="language-bash">cd ~/Fast-DDS
git clone https://github.com/eProsima/Fast-DDS.git
mkdir Fast-DDS/build
cd Fast-DDS/build
cmake ..  -DCMAKE_INSTALL_PREFIX=~/Fast-DDS/install
cmake --build . --target install
</code></pre>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2Finstall.dBt2edqM.png&amp;fm=webp&amp;w=800&amp;h=426&amp;dpl=69dce8926406240008354a25" alt="Diagram" /></p>
<ol>
<li>接下来我们需要使用<code>Fast DDS Gen</code>,这个工具是帮助我们实现<code>idl文件 --&gt; c++代码</code>
<blockquote>
<p>请确保环境中有jdk! Fast DDS-Gen supports Java versions from 11 to 19.</p>
</blockquote>
</li>
</ol>
<pre><code class="language-bash">cd ~
git clone --recursive https://github.com/eProsima/Fast-DDS-Gen.git
cd Fast-DDS-Gen
./gradlew assemble
</code></pre>
<h3>Create DEMO</h3>
<pre><code class="language-bash">cd ~
mkdir Test &amp;&amp; cd Test
</code></pre>
<ol>
<li>创建Test.idl</li>
</ol>
<pre><code class="language-c++">struct Test {
  string msg;
};
</code></pre>
<ol>
<li><code>Fast DDS Gen</code>转化为c++代码。在Test目录下找到Fast-DDS-Gen/scripts/执行fastddsgen -example CMake Text.idl</li>
</ol>
<pre><code class="language-bash">../Fast-DDS-Gen/scripts/fastddsgen -example CMake Text.idl
</code></pre>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2Fgen.Bk0F-qV1.png&amp;fm=webp&amp;w=800&amp;h=105&amp;dpl=69dce8926406240008354a25" alt="Diagram" /></p>
<ol>
<li>添加分别在<code>TestPublisher.cxx</code>和<code>TestSubscriber.cxx</code>。分别代表了要输出什么 和 怎么输出</li>
</ol>
<ul>
<li>
<p>TestPublisher.cxx
<img src="https://agility6.me/.netlify/images?url=_astro%2FPub.sElUD5zw.png&amp;fm=webp&amp;w=800&amp;h=204&amp;dpl=69dce8926406240008354a25" alt="Diagram" /></p>
</li>
<li>
<p>TestSubscriber.cxx
<img src="https://agility6.me/.netlify/images?url=_astro%2FSub.E-Wuw5-4.png&amp;fm=webp&amp;w=800&amp;h=249&amp;dpl=69dce8926406240008354a25" alt="Diagram" /></p>
</li>
</ul>
<ol>
<li>构建</li>
</ol>
<pre><code class="language-bash">mkdir build &amp;&amp; cd build
cmake ..
make
</code></pre>
<ul>
<li>
<p>如果你在<code>cmake..</code>的时候遇到了以下报错。需要安装fastrtps</p>
<pre><code class="language-text">   By not providing "Findfastrtps.cmake" in CMAKE_MODULE_PATH this project has
   asked CMake to find a package configuration file provided by "fastrtps",
   but CMake did not find one.

   Could not find a package configuration file provided by "fastrtps" with any
   of the following names:

   fastrtpsConfig.cmake
   fastrtps-config.cmake

   Add the installation prefix of "fastrtps" to CMAKE_PREFIX_PATH or set
   "fastrtps_DIR" to a directory containing one of the above files.  If
   "fastrtps" provides a separate development package or SDK, be sure it has
   been installed.
</code></pre>
<ul>
<li>安装<code>Fast-RTPS</code></li>
</ul>
<pre><code class="language-bash">cd ~
git clone https://github.com/eProsima/Fast-RTPS
mkdir Fast-RTPS/build &amp;&amp; cd Fast-RTPS/build

cmake -DTHIRDPARTY=ON ..
make
sudo make install
</code></pre>
<ul>
<li>如果再次执行后如果发现缺少<code>foonathan_memory</code>则需要安装</li>
</ul>
<pre><code class="language-bash">cd ~
cd Fast-RTPS/thirdparty
git clone https://github.com/foonathan/memory.git
cd memory
mkdir build &amp;&amp; cd build
cmake ..
make
sudo make install
</code></pre>
<ul>
<li>再次执行
<img src="https://agility6.me/.netlify/images?url=_astro%2Ftest-build.u5qVoYq3.png&amp;fm=webp&amp;w=800&amp;h=650&amp;dpl=69dce8926406240008354a25" alt="build" /></li>
</ul>
</li>
</ul>
<ol>
<li>执行<code>publisher | subscriber</code></li>
</ol>
<pre><code class="language-bash">~/FastDDS-Test/build$ ./Test publisher
</code></pre>
<pre><code class="language-bash">~/FastDDS-Test/build$ ./Test subscriber
</code></pre>
<ul>
<li>
<p>如果你在执行<code>publisher | subscriber</code>碰到了以下错误</p>
<pre><code class="language-text">./Test: error while loading shared libraries: libfoonathan_memory-0.7.3.so: cannot open shared object file: No such file or directory
</code></pre>
<ul>
<li>路径问题，通过<code>sudo find / -name libfoonathan_memory-0.7.3.so</code>找到路线进行添加</li>
</ul>
<pre><code class="language-bash">export LD_LIBRARY_PATH="YOUR_PATH"
</code></pre>
<ul>
<li>更新动态库缓存</li>
</ul>
<pre><code class="language-bash">sudo ldconfig
</code></pre>
</li>
</ul>
<h3>DEMO</h3>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2FPub-Sub.52mOuMpU.png&amp;fm=webp&amp;w=800&amp;h=426&amp;dpl=69dce8926406240008354a25" alt="DOME" /></p>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Huffman Trees Experiment]]></title>
            <link>https://agility6.me/huffman-trees-experiment</link>
            <guid isPermaLink="false">https://agility6.me/huffman-trees-experiment</guid>
            <pubDate>Sun, 14 May 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[实现一个基于哈夫曼编码由字符转换成由二进制的字符串 项目地址 前言 在远程通讯中，要将待传字符转换成由二进制的字符串 设要传送的字符以及对应的比编码如下 A —— 00 B —— 01 C —— 10 D —— 11 此时如果需要表示ABACCDA则对应转化为00010010101100 在设计编码时，应该遵循出现的次数大的字符则采用尽可能短的编码，以保证整体的二进制字符串长度短 在得出二进制字符...]]></description>
            <content:encoded><![CDATA[<blockquote>
<p>实现一个基于哈夫曼编码由字符转换成由二进制的字符串
<a href="https://github.com/AnnularLabs/huffman-coding/tree/main">项目地址</a></p>
</blockquote>
<h3>前言</h3>
<ul>
<li>在远程通讯中，要将待传字符转换成由二进制的字符串</li>
</ul>
<p>设要传送的字符以及对应的比编码如下</p>
<pre><code class="language-text">A —— 00

B —— 01

C —— 10

D —— 11
</code></pre>
<p>此时如果需要表示<code>ABACCDA</code>则对应转化为<code>00010010101100</code></p>
<p>在设计编码时，应该遵循<strong>出现的次数大的字符则采用尽可能短的编码</strong>，以保证整体的二进制字符串长度短</p>
<p>在得出二进制字符串称为<strong>编码</strong>，可以再次通过编码表转化为字符串称为<strong>解码</strong></p>
<h3>重码</h3>
<ul>
<li>当我们在设计编码时，应该确保任一字符的编码都不是另一个字符的编码的<strong>前缀</strong>否则将会出现<strong>重码</strong></li>
</ul>
<p>设要传送的字符以及对应的比编码如下设</p>
<pre><code class="language-text">A —— 0

B —— 00

C —— 1

D —— 01
</code></pre>
<p>如果使用上述编码表，将会得出<code>0000</code>二进制字符串，将会产生歧义，<code>0000</code>通过<strong>解码</strong>可得出以下情况</p>
<ul>
<li>
<p>AAAA</p>
</li>
<li>
<p>ABA</p>
</li>
<li>
<p>BB</p>
</li>
</ul>
<p><strong>因此会造成我们解码的不确定性</strong></p>
<blockquote>
<p>关键：在设计长度不等的编码，必须是任一字符的编码都不是另一个字符的编码的前缀</p>
</blockquote>
<h3>哈夫曼编码</h3>
<ul>
<li>
<p>保证是前缀码</p>
</li>
<li>
<p>保证字符编码总是最短</p>
</li>
</ul>
<h4>Example</h4>
<p><code>const word = 'abcd'</code></p>
<ul>
<li>a：weight = 1</li>
<li>b：weight = 2</li>
<li>c：weight = 3</li>
<li>d：weight = 4</li>
</ul>
<p><img src="https://agility6.me/.netlify/images?url=_astro%2FTweelet.CKYNs349.png&amp;fm=webp&amp;w=800&amp;h=509&amp;dpl=69dce8926406240008354a25" alt="Diagram" /></p>
<h3>实现</h3>
<p>总体实现步骤分为<code>Coding</code>和<code>Decoding</code></p>
<h4>Coding</h4>
<ol>
<li>
<p>设计每个结点的数据类型(典型的树结构)，</p>
<ul>
<li>str：记录存放的字符</li>
<li>weight：该字符的<strong>权重</strong></li>
<li>parent：父节点</li>
<li>leftChild：左孩子</li>
<li>rightChild：右孩子</li>
</ul>
<pre><code class="language-js">class HNode {
  constructor(str = null, weight = 999, parent = -1, leftChild = -1, rightChild = -1) {
    this.str = str
    this.weight = weight,
    this.parent = parent,
    this.leftChild = leftChild,
    this.rightChild = rightChild
  }
}
</code></pre>
</li>
<li>
<p>初始化，因为要保证字符编码总是最短，所以在字母出现的频率上就有要求，这里参考了<a href="https://www3.nd.edu/~busiforc/handouts/cryptography/letterfrequencies.html">blog</a>。有了字母出现的频率就可以进行哈夫曼树的生成。简单复习Huffman的特点</p>
<ul>
<li>初始时有n颗二叉树，要进过<code>n-1</code>次合并最终形成哈夫曼树</li>
<li>经过n-1 次合并产生n-1个新结点，且n-1个新结点都是具有两个孩子的分支结点</li>
<li>哈夫曼树共有<code>2n-1</code>个结点，且分支结点度不均不为1</li>
</ul>
<pre><code class="language-js">// 初始化
function initHuffmanTree(letter) {
  // 去除下标0
  const HuffmanTree = [0]

  // 原字母
  letter.forEach((item, index) =&gt; {
    HuffmanTree.push(new HNode(item, index + 1).toJSON())
  })

  // 初始化哈夫曼树生成结点
  for (let i = 1; i &lt;= letter.length - 1; i++) {
    HuffmanTree.push(new HNode().toJSON())
  }
}

// 生成哈夫曼树
function createHuffmanTree(HuffmanTree, length) {
  for (let i = length + 1; i &lt; 2 * length; i++) {
    // 挑选权重最小两个
    const [minFirst, minSecond] = SelectMin(HuffmanTree, i - 1)
    /**
     * 1. minFirst的权重 + minSecond的权重
     * 2. 改变minFirst和miSecond的parent值
     * 3. 新HNode设置左孩子和右孩子（遵循左小右大）
     */
    HuffmanTree[i].weight = HuffmanTree[minFirst].weight + HuffmanTree[minSecond].weight
    HuffmanTree[minFirst].parent = i
    HuffmanTree[minSecond].parent = i
    HuffmanTree[i].leftChild = minFirst
    HuffmanTree[i].rightChild = minSecond
  }
}
</code></pre>
<ul>
<li>使用<code>toJSON</code>的原因让它序列化，这样在后续加入到redux中不会有问题</li>
<li>createHuffmanTree的关键是在每次循环中找到两个最小的值。<a href="https://github.com/AnnularLabs/huffman-coding/blob/main/src/utils/huffman-coding/selectMin.js">实现连接</a></li>
</ul>
</li>
<li>
<p>将获取的word转化为<code>01</code>(这里的哈夫曼树遵循左边为0、右边为1)，例如输入的是<code>hello</code>，</p>
<ul>
<li>在letterLETTER_FREQUENCIES中找到<code>h</code>的位置，对应的就是在哈夫曼树的下标记为<code>Child</code></li>
<li>获取<code>Child</code>的<code>parent</code></li>
<li>只需要看parent的leftChild和rightChild哪边等于Child</li>
<li>循环即可</li>
</ul>
<pre><code class="language-js">function createWordCode(HuffmanTree, Letter, words) {
  const resultCodeArray = []
  for (let i = 0; i &lt; words.length; i++) {
    let child = LETTER_FREQUENCIES.indexOf(words[i]) + 1
    let parent = HuffmanTree[child].parent

    const HNodeCode = []
    while (parent !== -1) {
      HuffmanTree[parent].leftChild === child ? HNodeCode.push(0) : HNodeCode.push(1)
      child = parent
      parent = HuffmanTree[child].parent
    }

    // print
    let resultCode = ''
    while (HNodeCode.length &gt; 0) {
      resultCode += HNodeCode.pop()
    }
    resultCodeArray.push(resultCode)
  }

  return resultCodeArray
}
</code></pre>
</li>
</ol>
<h3>Decoding</h3>
<ol>
<li>
<p>获取<code>code</code></p>
</li>
<li>
<p>从上到下对HuffmanTree进行遍历，0走<code>leftChild</code>1走<code>rightChild</code></p>
</li>
<li>
<p>如果当前为叶子结点则直接取出<code>HuffmanTree[].str</code></p>
</li>
</ol>
]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[关于Git Commit的信息]]></title>
            <link>https://agility6.me/gitcommitstandard</link>
            <guid isPermaLink="false">https://agility6.me/gitcommitstandard</guid>
            <pubDate>Sun, 09 Apr 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Commit message should be Understandable Enough Unambiguous Info Commit Type Description Emoji fix: This is to commit a resolved bug in the codebase 🐛 feat: This is to commit a new feature to the code...]]></description>
            <content:encoded><![CDATA[<h3>Commit message should be</h3>
<ol>
<li>Understandable</li>
<li>Enough</li>
<li>Unambiguous</li>
</ol>
<h3>Info</h3>
<table>
<thead>
<tr>
<th>Commit Type</th>
<th>Description</th>
<th>Emoji</th>
</tr>
</thead>
<tbody>
<tr>
<td>fix:</td>
<td>This is to commit a resolved bug in the codebase</td>
<td>🐛</td>
</tr>
<tr>
<td>feat:</td>
<td>This is to commit a new feature to the code base</td>
<td>✨</td>
</tr>
<tr>
<td>chore:</td>
<td>This commits changes that are not related to a feature or a bug</td>
<td>♻️</td>
</tr>
<tr>
<td>refactor:</td>
<td>This commits changes refactored code</td>
<td>📦</td>
</tr>
<tr>
<td>docs:</td>
<td>This commits changes the documentation</td>
<td>📚</td>
</tr>
<tr>
<td>style:</td>
<td>This involves style changes in the codebase</td>
<td>💎</td>
</tr>
<tr>
<td>test:</td>
<td>This commits made in the test file including corrections made</td>
<td>🚨</td>
</tr>
<tr>
<td>perf :</td>
<td>This commits to improve the app’s performances</td>
<td>🚀</td>
</tr>
<tr>
<td>ci:</td>
<td>This commits make changes in the CI integration</td>
<td>⚙️</td>
</tr>
<tr>
<td>build:</td>
<td>the build files and blue dependency</td>
<td>🛠</td>
</tr>
<tr>
<td>revert:</td>
<td>This commit signifies reverting to a previous commit</td>
<td>🗑</td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<blockquote>
<p><a href="https://dev.to/tuasegun/a-guide-to-writing-industry-standard-git-commit-message-2ohl">https://dev.to/tuasegun/a-guide-to-writing-industry-standard-git-commit-message-2ohl</a></p>
</blockquote>
]]></content:encoded>
        </item>
    </channel>
</rss>