<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>流沙</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://lichuanyang.top/</id>
  <link href="https://lichuanyang.top/" rel="alternate"/>
  <link href="https://lichuanyang.top/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, 流沙</rights>
  <subtitle>聚沙成塔，让每一步都有选择</subtitle>
  <title>Mobility</title>
  <updated>2026-07-03T07:41:42.435Z</updated>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="AI实践" scheme="https://lichuanyang.top/categories/AI%E5%AE%9E%E8%B7%B5/"/>
    <category term="ai-agent" scheme="https://lichuanyang.top/tags/ai-agent/"/>
    <category term="skill管理" scheme="https://lichuanyang.top/tags/skill%E7%AE%A1%E7%90%86/"/>
    <category term="pks" scheme="https://lichuanyang.top/tags/pks/"/>
    <category term="工作流" scheme="https://lichuanyang.top/tags/%E5%B7%A5%E4%BD%9C%E6%B5%81/"/>
    <category term="多agent协作" scheme="https://lichuanyang.top/tags/%E5%A4%9Aagent%E5%8D%8F%E4%BD%9C/"/>
    <content>
      <![CDATA[<p>之前写过一篇文章：<a href="https://lichuanyang.top/posts/26060/">https://lichuanyang.top/posts/26060/</a> . 教大家构建与Agent无关的工作流，随时能拉起不同的Agent工作，从而能顺畅用上各个Agent提供的免费、试用套餐。这么实践下来，一个明显会变得繁琐的事情就是对于skill的管理。因此，我又写了一个工具，实现在不同Agent、不同项目间管理skills.</p><span id="more"></span><p>鉴于目前Agent的生态还比较凌乱，大家各干各的，skill的存放目录也自己定自己的。</p><p>比如 Cursor 放在 <code>~/.cursor/skills</code>，Claude Code 放在 <code>~/.claude/skills</code>，Trae 放在 <code>~/.trae/skills</code>，OpenCode 放在 <code>~/.config/opencode/skills</code>，还有 Windsurf、Qoder、Hermes 等等，每家都有自己的路径约定。</p><p>社区也在试图定义通用的.agents&#x2F;skill这样目录，有点作用，但不大。</p><p>当然，skill管理的问题，也不全是使用多Agent带来的。因为skill这种东西，天然就就有非常大的适用范围区别。有的skill是适用于公司的项目的，有的是适用于自己的项目的，有的适用范围还更窄一点，只适用于若干范围内的几个项目，还有的skill, 比如汇总新闻的，我想只配在某个agent里。</p><p>你要是问，不对skill做这些精细化的管理，行不行？那确实也没什么大问题，无非就是agent每次检索技能，多花点token. 但做技术的强迫症，还是希望把这些东西管的细一些，在不需要agent调起的地方，就压根别让agent能看到这些skill.</p><p>基于此，设计了pks这个工具。核心思路是在一个集中的地方管理所有的skill, 然后，按照需求，将skill写入agent的skill目录或者项目目录下。</p><p>具体来说，支持以下功能</p><p><strong>全局管理</strong>：所有 skill 集中存放在 <code>~/.local/share/pks/skills/</code> 下，用 <code>pks list</code> 查看，用 <code>pks new</code> 创建新 skill。</p><p><strong>项目级安装</strong>：在项目目录下执行 <code>pks init</code> 初始化后，可以用 <code>pks install</code> 把全局 skill 安装到当前项目的 <code>.skills/</code> 目录。</p><p><strong>Agent 级安装</strong>：用 <code>pks install-to &lt;agent&gt; &lt;skill&gt;</code> 把 skill 直接装到指定 agent 的 skill 目录（比如 <code>~/.cursor/skills/</code>）。</p><p><strong>双向同步</strong>：在项目里改了 skill 文件，用 <code>pks push</code> 把改动推回全局库。</p><p>实际使用上，我大概有这么些使用场景：</p><p>有一些skill是小范围适用的，比如相关的几个有限的工程，这时候，我不会将skill放到agent的配置里，而是会放到项目中，使用pks install命令，可以把本地技能库中的skill, 放到工程目录下。然后可以在AGENTS.md文件中指引agent去skill目录下找技能，对于支持项目内技能的agent, 也可以使用pks link命令，将agent对应的项目内技能目录，比如.opencode&#x2F;skills, 软链到skills目录下；</p><p>有些skill, 比如收集新闻的skill, 我只想让它在部分agent中出现，这样就执行pks install-to命令, 将其装到agent的skill目录下。</p><p>有时候skill文件需要修改，我会先在某个项目下进行修改，然后执行pks push, 将命令写回技能库。</p><p>这样，对于skill, 就有了一个比较妥善合理的处理流程。</p><p>项目地址在： <a href="https://github.com/lcy362/personal-skills-manager">https://github.com/lcy362/personal-skills-manager</a>  ， 欢迎试用。 大家有什么其他关于skill管理的经验，也欢迎提出。</p>]]>
    </content>
    <id>https://lichuanyang.top/posts/34689/</id>
    <link href="https://lichuanyang.top/posts/34689/"/>
    <published>2026-07-01T09:16:24.000Z</published>
    <summary>
      <![CDATA[<p>之前写过一篇文章：<a href="https://lichuanyang.top/posts/26060/">https://lichuanyang.top/posts/26060/</a> . 教大家构建与Agent无关的工作流，随时能拉起不同的Agent工作，从而能顺畅用上各个Agent提供的免费、试用套餐。这么实践下来，一个明显会变得繁琐的事情就是对于skill的管理。因此，我又写了一个工具，实现在不同Agent、不同项目间管理skills.</p>]]>
    </summary>
    <title>从薅 token 到管 skill：我的 pks 工具落地实践</title>
    <updated>2026-07-03T07:41:42.435Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="AI" scheme="https://lichuanyang.top/tags/AI/"/>
    <category term="知乎" scheme="https://lichuanyang.top/tags/%E7%9F%A5%E4%B9%8E/"/>
    <category term="Obsidian" scheme="https://lichuanyang.top/tags/Obsidian/"/>
    <category term="llm-wiki" scheme="https://lichuanyang.top/tags/llm-wiki/"/>
    <category term="知识管理" scheme="https://lichuanyang.top/tags/%E7%9F%A5%E8%AF%86%E7%AE%A1%E7%90%86/"/>
    <category term="微信读书" scheme="https://lichuanyang.top/tags/%E5%BE%AE%E4%BF%A1%E8%AF%BB%E4%B9%A6/"/>
    <category term="Notion" scheme="https://lichuanyang.top/tags/Notion/"/>
    <content>
      <![CDATA[<p>前段时间，偶然看到karpathy大神提出的llm-wiki, 有种相见恨晚的感觉。我一直是很喜欢写些东西的，但是写完之后，问题就是大量内容散落在各个地方，一直没有精力对它们进行有效的管理。而看到了llm-wiki后，我就意识到，之前写的那些东西，要开始发挥作用了。</p><span id="more"></span><h2 id="为什么需要知识中枢"><a href="#为什么需要知识中枢" class="headerlink" title="为什么需要知识中枢"></a>为什么需要知识中枢</h2><p>首先, llm-wiki是什么。</p><p>llm-wiki 是 Andrej Karpathy 在 最近提出的一个概念：把你过往积累的所有文字材料——笔记、博客、读书摘录、工作日志——作为”语料库”，让 LLM 自动从中提取概念、建立页面、编织交叉引用，最终形成一个结构化的、可持续迭代的个人 Wiki。</p><p>它的核心前提很简单：每个人在日常工作和学习中已经产生了大量有结构、有见解的文字，只是它们散落在各处，缺乏关联。llm-wiki 要做的就是用一个 LLM 驱动的流程，把这些散落的珍珠串起来。你负责持续产生和收集内容，LLM 负责组织和管理。</p><p>和传统的手动 Wiki 维护不同——建页面、写摘要、加链接，枯燥且难以坚持——llm-wiki 把组织成本降到了几乎为零。你只需要告诉 LLM 你的知识库结构和维护规则（即一个 AGENTS.md 文件），它就能反复执行摄入、更新、审计等操作。我自己实践下来的感受是，看着 AI 把零散笔记变成结构化的交叉引用网络，有一种”债务清零”的快感。</p><h2 id="数据接入"><a href="#数据接入" class="headerlink" title="数据接入"></a>数据接入</h2><p>我做的第一件事，就是把之前在notion里写的各种笔记，有开发知识、投资知识，还有各种各样零碎的记录，都导出然后放到了obsidian里。</p><p><strong>如何将 Notion 内容导入 Obsidian？</strong></p><p>实际操作并不复杂，核心步骤如下：</p><ol><li><p><strong>导出 Notion 数据</strong>：在 Notion 的”设置与成员 → 设置”中，选择”导出所有工作区内容”，格式选 <strong>Markdown &amp; CSV</strong>。导出后会得到一个 ZIP 包，解压后每个 Notion 页面对应一个 <code>.md</code> 文件，数据库则额外附带 CSV。注意：Notion 免费版每次只能导出一个工作区，如果你有多个工作区，需要分别操作。</p></li><li><p><strong>安装 Obsidian Importer 插件</strong>：在 Obsidian 社区插件市场搜索”Importer”并安装。这个插件支持从 Notion、Bear、Evernote、OneNote 等多种工具一键导入，会自动处理图片附件和内部链接。启用插件后，用 <code>Cmd+P</code> 打开命令面板，搜索”Importer: Open Importer”，选择 Notion 格式，选中刚才解压的文件夹即可。</p></li><li><p><strong>手动导入（备选方案）</strong>：如果不使用 Importer 插件，直接将解压后的文件夹放入 Obsidian vault 目录即可。Obsidian 原生支持 <code>[[wiki-link]]</code> 格式的内部链接，Notion 导出的 Markdown 中的链接通常已经转换为该格式。</p></li><li><p><strong>后续处理</strong>：导入后建议将原始文件放入一个专门的子目录（比如 <code>raw/notion-export/</code>），标记为”不可修改”。这样保留了原始数据的完整性——这是 llm-wiki 方法论中很重要的一环：原始素材永不可改，LLM 在此基础上生成结构化知识。如果你的 Notion 中有数据库，CSV 文件可以作为参考保留；如果某些页面嵌入了 Notion 特有的 Block（如日历、看板），导出后这些会丢失交互性，但文本内容会保留。</p></li></ol><p>整个流程走下来，我几千条分散的笔记就这样汇入了 Obsidian，成为了知识中枢的第一批”原料”。</p><p>然后将karpathy那篇gist喂给AI, 生成出项目的AGENTS.md文档，AI能够自然的写出llm-wiki所需的摄入、审计等操作。</p><p>接着就可以执行了。看着AI不停的生成wiki内容，把我之前的积累分类整理，还是非常舒适的。</p><p>再往后，我又做了几件事，就是把知乎的创作和微信读书的笔记也纳入进来。知乎上我写了上千篇回答，微信读书几年读了上百本书，除了笔记之外，这些也是我知识体系的重要组成部分。正好，差不多那段时间，微信读书发布了官方的skill, 我也就顺手用了起来。</p><h2 id="知乎收藏导入"><a href="#知乎收藏导入" class="headerlink" title="知乎收藏导入"></a>知乎收藏导入</h2><p><strong>如何将知乎创作同步到 Obsidian？</strong></p><p>知乎没有提供官方的数据导出 API，这里我用 Playwright 做了浏览器自动化。</p><p><strong>操作步骤</strong>：</p><ol><li>安装 Playwright：<code>pip install playwright &amp;&amp; playwright install chromium</code></li><li>首次运行脚本，在打开的 Chromium 浏览器中扫码或密码登录知乎</li><li>登录成功后脚本自动遍历个人主页，抓取所有回答、文章和想法</li><li>登录状态持久化保存到本地，后续通过 <code>--reuse</code> 参数静默执行，无需再次登录</li></ol><p><strong>特点</strong>：增量同步，每次只抓取新内容，已有文件不重复处理；按回答&#x2F;文章&#x2F;想法分类存放。</p><h2 id="微信读书笔记同步"><a href="#微信读书笔记同步" class="headerlink" title="微信读书笔记同步"></a>微信读书笔记同步</h2><p><strong>如何将微信读书笔记同步到 Obsidian？</strong></p><p>微信读书开放了 Agent API Gateway，申请 API Key 后即可调用。</p><p><strong>操作步骤</strong>：</p><ol><li>调用 <code>/user/notebooks</code> 接口获取有笔记的书籍列表</li><li>对每本新书，分别拉取划线和想法内容</li><li>按章节分组，输出为规范的 Markdown 文件</li></ol><p><strong>输出格式</strong>：书名和作者作为标题，每章划线以引用块形式列出（附带日期），笔记和想法附在对应原文下方。</p><p><strong>特点</strong>：完全增量同步，脚本维护已同步书籍 ID 的状态文件，每次运行只处理新增的书籍。151 本书的笔记就这样悄无声息地流入了 Obsidian，成为了知识中枢最丰富的一批原料。</p><h2 id="LLM-Wiki-智能检索"><a href="#LLM-Wiki-智能检索" class="headerlink" title="LLM Wiki 智能检索"></a>LLM Wiki 智能检索</h2><p>到此，内容层的准备基本就完成了。然后我又想，既然我大部分的知识和创作都在这儿了，是不是可以开始蒸馏我这个人了？</p><p>这块先搞了一版简单的，就是把拆分出一个和wiki类似的personal流程，也有ingest、lint等流程，区别就是wiki的重点是知识，而personal的重点是我这个人。</p><p><strong>知识库 vs 人格蒸馏：两种不同的 AI 处理逻辑</strong></p><p>这里有必要解释一下两者的区别——它们共享同一套原始素材，但目标和产出完全不同。</p><p><strong>知识库（Wiki）：回答”我知道什么”</strong></p><p>从笔记、博客和读书摘录中提取客观知识，生成概念页面（如”分布式一致性”）、实体页面（如”Raft 算法”）、来源摘要页面（如”《数据密集型应用设计》读书笔记”），并在它们之间建立密集的交叉引用。目标是让知识变得可查询、可复用，像一个外部化的第二大脑。</p><p><strong>人格蒸馏（Personal Model）：回答”我是谁”</strong></p><p>从创作和阅读中反向推导认知模式、表达风格和价值取向。例如，通过分析技术博客，可以归纳出”论点先行、案例驱动”的表达风格；通过分析知乎回答，可以发现”第一性原理还原”和”量化思维”等反复出现的认知特征。产出不是知识条目，而是一个人的认知图谱——擅长什么、怎么思考、看重什么。</p><p><strong>两者的异同</strong></p><table><thead><tr><th>维度</th><th>知识库</th><th>人格蒸馏</th></tr></thead><tbody><tr><td>核心问题</td><td>我知道什么</td><td>我是谁</td></tr><tr><td>输入</td><td>笔记、博客、读书摘录</td><td>一切个人创作和阅读记录</td></tr><tr><td>产出</td><td>概念&#x2F;实体&#x2F;来源页面 + 交叉引用</td><td>领域深度&#x2F;认知特征&#x2F;表达风格&#x2F;价值观</td></tr><tr><td>方向</td><td>向外看：结构化外部知识</td><td>向内看：建模个人认知</td></tr><tr><td>流程</td><td>摄入 → 查询 → Lint → 审计</td><td>摄入 → 查询 → Lint → 审计（同构）</td></tr></tbody></table><p>两者在流程上高度相似，但一个是向外看，结构化和整理你拥有的知识；一个是向内看，蒸馏和建模你作为个体的认知特征。这种”一体两面”的设计，是我觉得整个系统最有意思的地方。</p><p>这块最近还在看网上的女娲等项目，想看看有没有更好的蒸馏人格的方法。</p><h2 id="效果与心得"><a href="#效果与心得" class="headerlink" title="效果与心得"></a>效果与心得</h2><p>以上就是近期关于知识库的一些实录，有想法欢迎交流。</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-Obsidian-适合程序员做知识管理吗？"><a href="#Q-Obsidian-适合程序员做知识管理吗？" class="headerlink" title="Q: Obsidian 适合程序员做知识管理吗？"></a>Q: Obsidian 适合程序员做知识管理吗？</h3><p>非常适合。Obsidian 的核心理念——本地 Markdown 文件、双向链接、图谱可视化——天然契合程序员的使用习惯。Markdown 语法你本来就会，文件存储在本地意味着数据完全可控、可以用 Git 做版本管理，双向链接让你能像管理代码依赖一样管理知识之间的引用关系。加上 llm-wiki 的思路，AI 可以自动帮你从散落的笔记中提取概念、建立页面和交叉引用，把零散文档变成结构化的知识网络。</p><h3 id="Q-LLM-Wiki-需要-GPU-吗？"><a href="#Q-LLM-Wiki-需要-GPU-吗？" class="headerlink" title="Q: LLM Wiki 需要 GPU 吗？"></a>Q: LLM Wiki 需要 GPU 吗？</h3><p>不需要自己部署 GPU。LLM Wiki 的核心理念是<strong>让 LLM 处理你的文本</strong>，而不是让你自己跑模型。你只需要调用大模型的 API（云端推理），用它来读取你的 Markdown 文件、提取概念、生成页面和交叉引用。这个过程走的是云端 API，不需要本地 GPU。实际上整个流程的”硬件”只需要 Obsidian 和一个能调用 LLM API 的工具（如 WorkBuddy 等 Agent）。</p><h3 id="Q-llm-wiki-和传统-Wiki-有什么区别？"><a href="#Q-llm-wiki-和传统-Wiki-有什么区别？" class="headerlink" title="Q: llm-wiki 和传统 Wiki 有什么区别？"></a>Q: llm-wiki 和传统 Wiki 有什么区别？</h3><p>传统 Wiki 需要你手动创建页面、写摘要、添加内部链接，维护成本高且难以坚持。llm-wiki 把组织成本降到了几乎为零——你只需要继续产生和收集文字内容，LLM 自动读取你的 AGENTS.md 规则，反复执行摄入、更新、审计等操作，生成结构化的交叉引用网络。换句话说，传统 Wiki 是”你组织知识”，llm-wiki 是”AI 帮你组织知识”。</p><h3 id="Q-知识蒸馏和人格蒸馏有什么不同？"><a href="#Q-知识蒸馏和人格蒸馏有什么不同？" class="headerlink" title="Q: 知识蒸馏和人格蒸馏有什么不同？"></a>Q: 知识蒸馏和人格蒸馏有什么不同？</h3><p>两者共享同一套原始素材，但目标和产出完全不同。**知识库（Wiki）**回答”我知道什么”——从笔记和读书摘录中提取客观知识，生成概念页面和交叉引用。**人格蒸馏（Personal Model）**回答”我是谁”——从创作和阅读记录中反向推导你的认知模式、表达风格和价值取向。一个向外看（结构化知识），一个向内看（建模个人认知），流程相似但方向相反。</p><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><ol><li><strong>安装 Obsidian</strong>：从 <a href="https://obsidian.md/">obsidian.md</a> 下载客户端，创建本地 Vault（知识库），即为一个本地文件夹。</li><li><strong>配置 LLM Wiki</strong>：在 Vault 根目录创建 <code>AGENTS.md</code>，参照 karpathy 的 llm-wiki 思路编写知识维护规则，包括摄入（ingest）、更新、审计（audit）等流程定义。让 AI 工具读取该文件，自动从原始笔记中提取概念、建立交叉引用。</li><li><strong>导入 Notion 笔记</strong>：在 Notion 设置中导出为 Markdown + CSV 格式，使用 Obsidian Importer 插件一键导入，或将解压的 Markdown 文件夹直接放入 Vault 目录。</li><li><strong>接入微信读书</strong>：申请微信读书 API Key，调用 <code>/user/notebooks</code> 接口获取书籍列表，拉取划线和笔记，按章节分组输出为 Markdown 文件存入 Vault。</li><li><strong>导入知乎与博客</strong>：使用 Playwright 脚本自动抓取知乎回答和文章；将博客 Markdown 源文件复制到 Vault。完成后让 AI 执行一次全量 wiki 摄入，生成完整的知识交叉引用网络。</li></ol><p>原文地址：<a href="https://lichuanyang.top/posts/18804/">https://lichuanyang.top/posts/18804/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/posts/18804/</id>
    <link href="https://lichuanyang.top/posts/18804/"/>
    <published>2026-06-26T08:18:13.000Z</published>
    <summary>
      <![CDATA[<p>前段时间，偶然看到karpathy大神提出的llm-wiki, 有种相见恨晚的感觉。我一直是很喜欢写些东西的，但是写完之后，问题就是大量内容散落在各个地方，一直没有精力对它们进行有效的管理。而看到了llm-wiki后，我就意识到，之前写的那些东西，要开始发挥作用了。</p>]]>
    </summary>
    <title>把笔记、微信读书、知乎装进 Obsidian：我基于llm-wiki知识中枢搭建实录</title>
    <updated>2026-06-27T03:21:42.149Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="AI实践" scheme="https://lichuanyang.top/categories/AI%E5%AE%9E%E8%B7%B5/"/>
    <category term="AI视频" scheme="https://lichuanyang.top/tags/AI%E8%A7%86%E9%A2%91/"/>
    <category term="开源项目" scheme="https://lichuanyang.top/tags/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/"/>
    <category term="免费工具" scheme="https://lichuanyang.top/tags/%E5%85%8D%E8%B4%B9%E5%B7%A5%E5%85%B7/"/>
    <category term="text-to-video" scheme="https://lichuanyang.top/tags/text-to-video/"/>
    <category term="Agnes AI" scheme="https://lichuanyang.top/tags/Agnes-AI/"/>
    <content>
      <![CDATA[<blockquote><p>“解决的办法不是压制 AI，而是让它变成一种更平权的能力，让每个人都知道如何借 AI 创造更多。这也是我们公司很重要的愿景，让世界级的 AI 属于每一个人。”</p></blockquote><p>这是 Agnes AI 创始人 Bruce Yang 接受采访时说的一段话。</p><p>现在很多国内的AI厂商，无论deepseek还是智谱，都在把AI的价格往下压。坦率的讲，像文字、代码的处理价格，确实已经被压到了一个相当低的价格。但视频不一样，现在做AI视频，门槛确实高得离谱——国外的 Runway、Pika 按月订阅几十美元，国内的即梦、可灵免费额度用完就按秒计费，想本地跑开源模型？一张能跑视频的显卡轻松上万。</p><p>客观来讲，视频的生成，现阶段确实成本较高，让工业级的视频生成能力属于每一个人，确实不现实。但普通人也应该有一些途径能更多的去尝试、去创作，感谢Agnes开放自己的视频模型，让大家有这个机会。而这个项目只是想为了这个做一些微不足道的贡献。 <a href="https://github.com/lcy362/agnes-video-generator">Agnes Video Generator</a>（<a href="https://video.lichuanyang.top/">官网</a>）。说白了就是一个免费的AI视频生成器，不是那种”免费试用3次”的套路，是从写文案到出片、配音、上字幕，全程不花一分钱。只需要去 <a href="https://platform.agnes-ai.com/">Agnes AI</a> 注册个免费API Key就行。</p><p>Agnes的视频模型，目前确实称不上完美，但我想用这么一个项目，和Agnes一起成长，为AI平权，贡献上一点微不足道的力量。</p><span id="more"></span><h2 id="多种玩法"><a href="#多种玩法" class="headerlink" title="多种玩法"></a>多种玩法</h2><p>给它一句话描述，它还你一条视频。分几种类型：</p><p><strong>简单视频。</strong> 纯粹对API的封装，用来测试效果的，接口的各种参数，基本都做成了配置。</p><p><strong>创意视频。</strong> 你写一个故事创意，比如”暗黑版青蛙王子”，AI全包：扩展故事→生成角色参考图→拆分场景→写分镜提示→逐段生成视频→配音→上字幕→拼接成片。全程10步自动跑完，你只需要等着看成片。通过预生成尾帧，可以最大限度的保障场景间视频的连贯性。</p><p><strong>文稿视频、数字人口播。</strong> 贴一篇长文章进去，自动按语音时长分段，每段生成画面，或者放一个数字人在那里念稿。用一条完整的TTS旁白和字幕串起来。做知识区内容的可以试试。</p><p>各模式的详细参数和玩法，可以到<a href="https://video.lichuanyang.top/">官网</a>上看，这里就不展开了。</p><h2 id="跑起来很简单"><a href="#跑起来很简单" class="headerlink" title="跑起来很简单"></a>跑起来很简单</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">git <span class="built_in">clone</span> https://github.com/lcy362/agnes-video-generator.git</span><br><span class="line"><span class="built_in">cd</span> agnes-video-generator</span><br><span class="line">./start.sh</span><br></pre></td></tr></table></figure><p>就这三步。<code>start.sh</code> 会自动帮你建虚拟环境、装依赖、启动服务。</p><p>启动后打开 <code>http://localhost:8765</code>，在页面顶部填上你的 Agnes AI API Key，选个模式，写你的创意，点生成，然后耐心等结果就行。</p><p>用 Cursor 或者 Claude 这些AI Agent的话更方便，我专门为Agent做了使用说明，直接让你的Agent读项目里的Agents.md文件，它自己就能把环境搞好、把服务跑起来。</p><h2 id="看看效果"><a href="#看看效果" class="headerlink" title="看看效果"></a>看看效果</h2><p>做了几个demo,可以看看效果：</p><ul><li><a href="https://v.douyin.com/L4F6KdGnD6U/">暗黑版《青蛙王子》无旁白版</a> — 5个场景用关键帧衔接，全自动生成</li><li><a href="https://v.douyin.com/l2FlbF1Jdz0/">同一个故事加了旁白字幕</a> — AI配音 + 自动字幕，可以看看字幕的效果</li><li><a href="https://v.douyin.com/eSGE9KENWVU/">文稿视频</a> — 贴了篇长文进去自动分段，每段配不同画面</li></ul><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>回到开头 Bruce Yang 说的那句话——「让世界级的 AI 属于每一个人」。</p><p>这个项目不是什么宏大的事业，就是想让 AI 视频创作这道门开着。不用订阅、不用好显卡、不用花一分钱，你只需要一个免费的 API Key 和一台能跑 Python 的电脑。</p><p>代码在 <a href="https://github.com/lcy362/agnes-video-generator">GitHub</a>，官网在 <a href="https://video.lichuanyang.top/">video.lichuanyang.top</a>。欢迎提bug。</p><p>原文地址：<a href="https://lichuanyang.top/posts/22470/">https://lichuanyang.top/posts/22470/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/posts/22470/</id>
    <link href="https://lichuanyang.top/posts/22470/"/>
    <published>2026-06-16T05:20:05.000Z</published>
    <summary>受够了AI视频工具按秒计费？我开源了一个100%免费的AI视频生成器，文字、图片、视频、配音全部免费，一键生成带旁白字幕的多场景AI视频。</summary>
    <title>免费AI视频生成器：我如何用零成本做出带旁白字幕的多场景AI视频</title>
    <updated>2026-06-27T03:22:02.934Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="AI实践" scheme="https://lichuanyang.top/categories/AI%E5%AE%9E%E8%B7%B5/"/>
    <category term="agnes" scheme="https://lichuanyang.top/tags/agnes/"/>
    <category term="AI视频" scheme="https://lichuanyang.top/tags/AI%E8%A7%86%E9%A2%91/"/>
    <category term="agnes免费模型" scheme="https://lichuanyang.top/tags/agnes%E5%85%8D%E8%B4%B9%E6%A8%A1%E5%9E%8B/"/>
    <category term="Agnes-Video-V2.0" scheme="https://lichuanyang.top/tags/Agnes-Video-V2-0/"/>
    <category term="开源" scheme="https://lichuanyang.top/tags/%E5%BC%80%E6%BA%90/"/>
    <category term="效率工具" scheme="https://lichuanyang.top/tags/%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<p>前阵子我在薅各种免费AI token，写了一篇关于”哪里免费去哪里薅”的文章。当时提到过，各家平台会不定期放出免费模型。结果没等多久，Agnes AI 就给了我一个惊喜：<strong>视频模型也免费了。</strong></p><p>不是文字，不是图片，是视频。<code>agnes-video-v2.0</code>、<code>agnes-image-2.1-flash</code>、<code>agnes-2.0-flash</code>，三个模型全免费，注册就送API Key，不要信用卡，不要GPU。其中 Agnes-Video-V2.0 是目前免费 agnes video 生成方案里能力最强的一个，支持多种生成模式，质量相当能打。</p><p>这谁忍得住？我直接把开源框架 ViMax 改造成了 Agnes 全家桶方案，顺便把原来一些反人类的用法都优化了一遍。</p><span id="more"></span><h2 id="Agnes免费模型：到底给了什么"><a href="#Agnes免费模型：到底给了什么" class="headerlink" title="Agnes免费模型：到底给了什么"></a>Agnes免费模型：到底给了什么</h2><p>先说 Agnes 这波免费到底给了啥。在 <a href="https://platform.agnes-ai.com/">Agnes AI 平台</a> 注册之后，拿到一个 API Key，就能用三个模型：</p><ul><li><strong>agnes-2.0-flash</strong>（对话模型）：写故事、编剧本、生成prompt，标准的chat接口</li><li><strong>agnes-image-2.1-flash</strong>（图片模型）：text-to-image，生成角色参考图、关键帧</li><li><strong>agnes-video-v2.0</strong>（视频模型）：支持 text-to-video、image-to-video、keyframes 三种模式</li></ul><p>API 走的是 OpenAI 兼容格式，base URL 在 <code>https://apihub.agnes-ai.com/v1</code>，对接起来非常丝滑。视频模型是异步的——提交任务拿task_id，然后轮询结果，跟大多数云端视频生成服务一个套路。</p><p>关键是：<strong>这三个模型串起来，刚好覆盖了”创意 → 故事 → 图片 → 视频”的完整链路。</strong> 不需要混用别家服务，一个 API Key 搞定所有事。</p><h2 id="改造思路：从多供应商到-Agnes-一把梭"><a href="#改造思路：从多供应商到-Agnes-一把梭" class="headerlink" title="改造思路：从多供应商到 Agnes 一把梭"></a>改造思路：从多供应商到 Agnes 一把梭</h2><p>原版 ViMax 是港大开源的 Agentic 视频生成框架，设计得很通用——LLM 用一家的、图片生成用一家的、视频生成又是一家的。好处是灵活，坏处是你要管三套 API、三套认证、三套错误处理。</p><p>我的改造思路很简单：<strong>既然 Agnes 三个模型全免费，那就全部换成 Agnes，一个 API Key、一个 base URL，省事。</strong></p><p>具体改了什么：</p><p><strong>编剧模块</strong>：把原来的 LLM 调用全换成 <code>agnes-2.0-flash</code>。它负责从你的一句话创意出发，生成完整故事、拆分场景、写每场景的视觉prompt，还要给每个场景生成尾帧描述。用的是 OpenAI 兼容的 chat&#x2F;completions 接口，temperature 0.7，够用了。</p><p><strong>图片生成器</strong>：换成 <code>agnes-image-2.1-flash</code>（t2i）和 <code>agnes-image-2.0-flash</code>（i2i）。前者生成角色参考图，后者做场景间的过渡帧图片编辑。</p><p><strong>视频生成器</strong>：换成 <code>agnes-video-v2.0</code>，这是核心。支持三种模式——纯文字生成视频（t2v）、图片引导视频（ti2vid）、关键帧插值（keyframes）。每场景支持5到20秒，帧率24fps。</p><p>改完之后，整个项目只依赖一个 API 服务商。<code>.api_key</code> 文件里写一个 key，完事。</p><h2 id="易用性优化：别让用户想太多"><a href="#易用性优化：别让用户想太多" class="headerlink" title="易用性优化：别让用户想太多"></a>易用性优化：别让用户想太多</h2><p>原版 ViMax 的用法比较”研究项目风”——参数硬编码在代码里，改个创意得改源码。这不太行，所以我在易用性上做了一堆改进。</p><h3 id="YAML-创意配置"><a href="#YAML-创意配置" class="headerlink" title="YAML 创意配置"></a>YAML 创意配置</h3><p>最大的改动是引入了 YAML 创意文件。在 <code>creatives/</code> 目录下，一个创意就是一个 <code>.yaml</code> 文件：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">&quot;frog&quot;</span></span><br><span class="line"><span class="attr">idea:</span> <span class="string">|</span></span><br><span class="line"><span class="string">  暗黑童话版青蛙王子，公主亲了青蛙之后，</span></span><br><span class="line"><span class="string">  青蛙变成了一个更可怕的怪物</span></span><br><span class="line"><span class="string"></span><span class="attr">user_requirement:</span> <span class="string">|</span></span><br><span class="line"><span class="string">  5个场景，每个10秒，哥特暗黑风</span></span><br><span class="line"><span class="string"></span><span class="attr">style:</span> <span class="string">&quot;哥特暗黑童话风格&quot;</span></span><br><span class="line"><span class="attr">chaining_mode:</span> <span class="string">keyframes</span></span><br><span class="line"><span class="attr">video_width:</span> <span class="number">768</span></span><br><span class="line"><span class="attr">video_height:</span> <span class="number">1152</span></span><br></pre></td></tr></table></figure><p>想拍新视频？写个 YAML，跑一条命令，完事。不用再碰 Python 源码了。</p><h3 id="一键启动脚本"><a href="#一键启动脚本" class="headerlink" title="一键启动脚本"></a>一键启动脚本</h3><p><code>start.sh</code> 做了一键封装：自动加载 <code>.api_key</code>、自动激活虚拟环境、自动列出可用创意、自动跑 pipeline。不传参数就列出所有创意，传个名字就直接跑：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">./start.sh          <span class="comment"># 列出所有创意</span></span><br><span class="line">./start.sh frog     <span class="comment"># 跑&quot;青蛙王子&quot;</span></span><br></pre></td></tr></table></figure><h3 id="智能缓存与断点续跑"><a href="#智能缓存与断点续跑" class="headerlink" title="智能缓存与断点续跑"></a>智能缓存与断点续跑</h3><p>这个必须说，因为视频生成是真的慢。</p><p>每个中间结果——故事文本、场景脚本、角色参考图、每场景视频——全部持久化到磁盘。跑到一半断了？重新跑同一个创意，已完成的步骤直接跳过，只生成缺失的部分。</p><p>最细粒度的是视频缓存：5个场景跑了3个断了，重跑只生成剩下2个。不会从头来。</p><h3 id="多模态图片分析"><a href="#多模态图片分析" class="headerlink" title="多模态图片分析"></a>多模态图片分析</h3><p>你可以自己提供角色参考图，也可以给每个场景自定义尾帧图片。系统会通过多模态 LLM 分析这些图片的内容，把视觉信息融入故事和 prompt 生成。</p><p>比如你提供一张自己画的卡通人物，系统会自动识别它的外貌特征，在后续所有场景里保持一致。</p><h2 id="角色一致性：两阶段锁定"><a href="#角色一致性：两阶段锁定" class="headerlink" title="角色一致性：两阶段锁定"></a>角色一致性：两阶段锁定</h2><p>AI视频最大的坑就是角色一致性——第一个场景是黑发妹子，第二个场景突然变成金发了。</p><p>ViMax-Agnes 用了一个两阶段方案：</p><p><strong>第一阶段</strong>：先生成（或你提供）一张角色参考图。编剧模块会从故事里提取角色的详细外貌描述——体型、发型、服装、配色、标志性特征——然后让图片模型生成一张全身参考图。</p><p><strong>第二阶段</strong>：每个场景视频都以这张参考图为起始帧，通过 <code>ti2vid</code> 模式生成。视频模型从同一个视觉锚点出发，自然保持了角色的一致性。</p><p>实测下来，卡通&#x2F;风格化画风的一致性效果最好。写实风格的话，建议直接提供自己的参考图，比 AI 生成的更可控。</p><h2 id="三种场景串联模式"><a href="#三种场景串联模式" class="headerlink" title="三种场景串联模式"></a>三种场景串联模式</h2><p>这是我觉得改造最有意思的部分。原版 ViMax 有场景串联的概念，但我在 Agnes API 上做了三种模式的适配：</p><p><strong><code>none</code>（独立模式）</strong>：每个场景独立生成，共用同一张角色参考图。速度最快，但场景之间没有过渡，硬切。</p><p><strong><code>ti2vid</code>（过渡帧模式）</strong>：顺序生成，每个场景结束后提取尾帧，用 img2img 生成一张”过渡帧”，作为下个场景的起始帧。过渡比较自然，但误差会累积——前面场景的瑕疵会传到后面。</p><p><strong><code>keyframes</code>（关键帧模式）</strong>：这是推荐方案。每个场景同时指定首帧和尾帧，让视频模型在两个关键帧之间做插值。尾帧由 AI 根据场景描述自动生成（你也可以手动指定）。这样每个场景的起点和终点都是确定的，过渡最平滑。</p><p>三种模式在 YAML 里一个字段切换，不用改代码。</p><h2 id="实战效果"><a href="#实战效果" class="headerlink" title="实战效果"></a>实战效果</h2><p>跑几个创意试了一下：</p><ul><li><strong>青蛙王子</strong>：5场景暗黑童话，keyframes模式，全自动生成</li><li><strong>女孩扣篮</strong>：3场景运动主题，角色一致性保持得不错</li><li><strong>海边舞蹈</strong>：4场景MV风格，场景过渡比较丝滑</li><li><strong>温泉机器人</strong>：3场景治愈风，卡通风格一致性最好</li></ul><p>每个创意从提交到出片，主要瓶颈在视频生成——每场景大概需要几分钟等待。但因为有缓存，调试成本其实不高。</p><h2 id="一些-Agnes-Video-V2-0-技术细节"><a href="#一些-Agnes-Video-V2-0-技术细节" class="headerlink" title="一些 Agnes-Video-V2.0 技术细节"></a>一些 Agnes-Video-V2.0 技术细节</h2><p>给想深入玩的朋友补充几个实现细节：</p><p><strong>视频参数</strong>：帧数遵循 <code>8n+1</code> 规则，上限441帧。5秒121帧、10秒241帧、15秒361帧、18秒和20秒都是441帧（帧率分别是24和22fps）。</p><p><strong>图片上传</strong>：视频API需要图片URL而不是base64，所以本地图片会通过图片生成API”上传”——用 <code>agnes-image-2.1-flash</code> 做一次 prompt 为”保持图片不变”的 i2i 转换，拿到一个托管URL。如果上传失败，回退到 inline base64。</p><p><strong>重试机制</strong>：视频提交遇到429（限流）或5xx（服务端错误），会自动指数退避重试，最多5次。轮询没有超时——视频生成就是这么慢，你得等。</p><p><strong>最小依赖</strong>：整个项目只依赖5个Python包：requests、pydantic、PyYAML、moviepy、tenacity。不需要PyTorch，不需要CUDA，纯API编排层。</p><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>Agnes 这波免费模型的诚意还是挺足的。agnes免费模型覆盖了文字、图片、视频全链路，API格式兼容OpenAI，注册门槛极低。对于想玩AI视频生成但不想烧钱的朋友来说，是个不错的入门选择。</p><p>ViMax-Agnes 的改造也让我验证了一件事：<strong>当免费模型的质量够用的时候，”哪里免费去哪里薅”的策略完全可以从文本扩展到视频。</strong> 一个 API Key、一条命令、一个 YAML 文件，就能从一句话生成一个完整的多场景视频。</p><p>项目开源在这里：<a href="https://github.com/lcy362/vimax-agnes">github.com&#x2F;lcy362&#x2F;vimax-agnes</a>，欢迎 star 和提 issue。</p><blockquote><p><strong>2026年6月更新</strong>：本文工具已迭代为 <a href="https://github.com/lcy362/agnes-video-generator">Agnes Video Generator</a>，支持 Web UI 和多语言，功能更完善，推荐使用新版本。</p></blockquote><p>原文地址：<a href="https://lichuanyang.top/posts/65500/">https://lichuanyang.top/posts/65500/</a></p><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><ol><li><strong>环境准备</strong>：注册 <a href="https://platform.agnes-ai.com/">Agnes AI 平台</a> 账号，获取 API Key。确保本地已安装 Python 3.8+ 和 Git。</li><li><strong>克隆项目</strong>：<code>git clone https://github.com/lcy362/vimax-agnes &amp;&amp; cd vimax-agnes</code>，将 API Key 写入 <code>.api_key</code> 文件。</li><li><strong>创建创意配置</strong>：在 <code>creatives/</code> 目录下新建 YAML 文件，定义 <code>name</code>、<code>idea</code>、<code>style</code>、<code>chaining_mode</code> 等参数。</li><li><strong>一键生成视频</strong>：运行 <code>./start.sh &lt;创意名称&gt;</code>，系统自动执行编剧生成、图片生成、视频生成全流程，中间结果自动缓存，支持断点续跑。</li><li><strong>查看效果</strong>：视频输出在 <code>output/</code> 目录下，检查角色一致性、场景过渡流畅度和整体视频质量。如需调整，修改 YAML 配置后重新运行即可。</li></ol>]]>
    </content>
    <id>https://lichuanyang.top/posts/65500/</id>
    <link href="https://lichuanyang.top/posts/65500/"/>
    <published>2026-06-11T14:00:00.000Z</published>
    <summary>Agnes AI 放出了免费视频模型 Agnes-Video-V2.0，无需GPU、无需信用卡。我把开源视频框架 ViMax 改造成了 Agnes 全家桶方案，顺便做了一堆易用性优化。</summary>
    <title>Agnes免费模型真能白嫖视频？我改造了ViMax来试试</title>
    <updated>2026-06-27T03:42:25.693Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="AI实践" scheme="https://lichuanyang.top/categories/AI%E5%AE%9E%E8%B7%B5/"/>
    <category term="效率工具" scheme="https://lichuanyang.top/tags/%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/"/>
    <category term="AI Agent" scheme="https://lichuanyang.top/tags/AI-Agent/"/>
    <category term="LLM" scheme="https://lichuanyang.top/tags/LLM/"/>
    <category term="工作流" scheme="https://lichuanyang.top/tags/%E5%B7%A5%E4%BD%9C%E6%B5%81/"/>
    <category term="CLI" scheme="https://lichuanyang.top/tags/CLI/"/>
    <content>
      <![CDATA[<p>上篇文章最后说了句：<strong>Agent只是工具，文档才是核心。</strong></p><p>这话说了之后，有人问我：道理都懂，但文档具体怎么管？总不能每个项目都复制粘贴一遍吧？</p><p>确实不能。</p><p>我后来写了个工具来解决这事——<a href="https://github.com/lcy362/personal-skills-manager">pks</a>（Personal Skills Manager），408行纯bash，零依赖。它干的事很简单：把你的AI工作流文档打包成skill，按需装到项目里，不绑定任何Agent平台。</p><span id="more"></span><h2 id="什么是skill"><a href="#什么是skill" class="headerlink" title="什么是skill"></a>什么是skill</h2><p>如果你用过OpenCode、Cursor或者其他Agent平台，大概率见过”skill”这个概念。不同平台叫法不一样——有的叫skill，有的叫rules，有的叫custom instructions——但说的都是同一件事：<strong>你给Agent的结构化指令，告诉它怎么处理特定类型的任务。</strong></p><p>比如”调用我们的API网关时，参数必须平铺在body顶层，不要嵌套在params里”，这是一条skill。”commit消息必须用中文，格式是type(scope):描述”，这也是。”数据库表名用snake_case，字段必须有注释”，还是。</p><p>Agent的底层模型提供了通用推理能力——它知道怎么写代码、怎么调API。但你的API网关有什么坑、你们团队的命名习惯是什么、你们项目为什么选了这个架构——这些知识模型不知道，得你告诉它。skill就是你告诉Agent这些东西的方式。</p><p>各平台管理skill的方式各不相同。OpenCode用<code>.opencode/skills/</code>目录，Cursor用<code>.cursorrules</code>，Claude Code读<code>CLAUDE.md</code>。格式不同，机制不同，但本质没区别：都是一份文档，Agent读到就按里面的规则干活。</p><p>skill本身不依赖任何平台。一个用Markdown写好的skill文件夹，丢进任何项目里，任何Agent找到了都会读、都会用。<strong>纯Markdown，谁来都能读。</strong></p><h2 id="问题出在哪"><a href="#问题出在哪" class="headerlink" title="问题出在哪"></a>问题出在哪</h2><p>skill是个好东西，但问题来了：你手上有好几个项目。</p><p>一份团队编码规范，项目A要用，项目B要用，项目C也要用。你怎么管？</p><p>常见的做法是，把这些配进agent本身的配置里，也就是常规的“安装skill”操作，比如给opencode装个微信读书skill, 这样，所有的项目用opencode打开，就都能用上。</p><p>这本来是没啥问题的，除非你看了我上一篇文章.. </p><p>在我们想打造agent无关的工作流的情况下，这么干问题就大了。哪天你看到有个新agent在送免费token, 下来一用，发现一堆skill都得自己迁移，这不就傻了吗。</p><p>所以我开发pks的初衷，就是把这一层抽出来，解套。让对这些不通用skill的管理，独立于所有的agent和项目，单独成一个体系。</p><h2 id="pks怎么解决"><a href="#pks怎么解决" class="headerlink" title="pks怎么解决"></a>pks怎么解决</h2><p>pks最适合管理那些<strong>非通用的、项目或团队特有的</strong>指令：你的编码风格偏好和commit规范、团队的代码规范和CI&#x2F;CD流程、项目的架构决策记录和API设计约束等。</p><p>pks要解决的就是一件事：<strong>在一个地方管理所有skill，按需装到任何项目里。</strong></p><p>然后，所有skill集中存在一个全局仓库里。项目需要哪些，装哪些。</p><p>全局操作就三个核心命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">pks new my-skill       <span class="comment"># 基于模板创建skill骨架</span></span><br><span class="line">pks list               <span class="comment"># 看看你有哪些skill</span></span><br><span class="line">pks delete my-skill    <span class="comment"># 删掉不要的</span></span><br></pre></td></tr></table></figure><p>项目操作也很简单：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> your-project</span><br><span class="line">pks init                        <span class="comment"># 初始化（只执行一次）</span></span><br><span class="line">pks install my-skill            <span class="comment"># 装一个skill进来</span></span><br><span class="line">pks status                      <span class="comment"># 看看装了哪些</span></span><br><span class="line">pks uninstall my-skill          <span class="comment"># 不想要了就卸掉</span></span><br></pre></td></tr></table></figure><p>装好之后，skill会被复制到项目的<code>.skills/</code>目录：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">your-project/</span><br><span class="line">├── .skills/</span><br><span class="line">│   ├── INDEX.md              # 自动生成的索引</span><br><span class="line">│   └── my-skill/</span><br><span class="line">│       └── SKILL.md           # 你的skill内容</span><br><span class="line">└── ...</span><br></pre></td></tr></table></figure><h2 id="Agent怎么发现这些skill"><a href="#Agent怎么发现这些skill" class="headerlink" title="Agent怎么发现这些skill"></a>Agent怎么发现这些skill</h2><p>这其实是个很自然的流程。</p><p>每次install或uninstall，pks会自动重建<code>.skills/INDEX.md</code>。这个索引文件列出所有已安装的skill，附带描述和版本，并且包含一句话：</p><blockquote><p>Agents: read the <code>SKILL.md</code> file in each skill directory below for relevant instructions.</p></blockquote><p>任何Agent进到项目都会看到这个目录，读INDEX.md，然后按需加载具体的SKILL.md。</p><p>最多你就在项目的AGENTS.md里再加一句去skill目录下找技能。按我的经验，对大部分agent来说，都没这个必要，它们都很自然的就找到这些技能了。</p><h2 id="什么时候会用到"><a href="#什么时候会用到" class="headerlink" title="什么时候会用到"></a>什么时候会用到</h2><p>说个最常见的场景：你负责一个项目，新人要加入开发。</p><p>传统做法是什么？甩一份README，口头交代几句”我们commit要这么写”、”那个内部库要那么调”、”数据库字段别随便改”。剩下的全靠悟。</p><p>用Agent辅助也差不多——你把规则塞进CLAUDE.md或AGENTS.md，Agent每次对话都吃一遍，不管这次任务跟这些规则有没有关系。</p><p>换个思路。把这些知识拆成几个skill：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">skills/</span><br><span class="line">├── team-coding-style/     # 团队编码规范、命名约定、commit格式</span><br><span class="line">│   └── SKILL.md</span><br><span class="line">├── internal-api-gateway/  # 内部API网关：参数平铺、鉴权方式、踩坑记录</span><br><span class="line">│   ├── SKILL.md</span><br><span class="line">│   ├── pitfalls.md        # 常见坑点（比如参数不能嵌套在params里）</span><br><span class="line">│   └── fields.md          # 回包字段含义速查</span><br><span class="line">├── project-architecture/  # 架构决策：模块划分、目录结构、为什么这么设计</span><br><span class="line">│   └── SKILL.md</span><br><span class="line">└── db-conventions/        # 数据库规范：命名、索引策略、迁移流程</span><br><span class="line">    └── SKILL.md</span><br></pre></td></tr></table></figure><p>新人入职？<code>pks init</code>，<code>pks install team-coding-style</code>，<code>pks install project-architecture</code>，几分钟装好。Agent读到这些skill，立刻知道这个项目的规矩。换一个人、换一台机器、换一个Agent平台，同样的skill装上去，效果一样。</p><p>而且<strong>只有装了才消耗token</strong>。一个纯前端项目不需要装数据库规范的skill，一个老项目不需要装新人引导的skill。相比把所有规则塞进一个巨大的配置文件让Agent每次对话都吃一遍，按需安装能省下不少无用token——这本身就是在薅自己的token。</p><p>这种<strong>非通用类</strong>的指令才是pks的主场。通用能力Agent自己就有，但你的团队规范、你的项目约定、你踩过的坑——这些东西只对你有用，也只该在需要的时候才出现。</p><h2 id="为什么是纯bash"><a href="#为什么是纯bash" class="headerlink" title="为什么是纯bash"></a>为什么是纯bash</h2><p>pks整个CLI是408行bash脚本。不用Python，不用Node.js，不用任何运行时。git clone下来就能跑。</p><p>为什么这么极端？因为skill管理工具本身不应该引入任何负担。你的工作流已经够复杂了，管理它的工具应该尽可能简单——简单到不会因为某个运行时版本升级而挂掉。</p><p>bash在macOS和Linux上开箱即用，十年前的脚本今天还能跑。这就是零依赖的好处：<strong>你的工作流文档比任何框架都长寿。</strong></p><h2 id="设计上的几个小心思"><a href="#设计上的几个小心思" class="headerlink" title="设计上的几个小心思"></a>设计上的几个小心思</h2><p>pks有几个设计细节值得提一嘴。</p><p><strong>语义化版本</strong>：每个skill的YAML front matter里有version字段。skill迭代了，版本号跟着涨，项目里装了哪个版本一目了然。</p><p><strong>全局管理，按需安装</strong>：所有skill集中在一个仓库里维护，项目里只装需要的。</p><p><strong>模板保护</strong>：以<code>_</code>开头的skill（比如<code>_template</code>）不会出现在列表里，也不能被删除。<code>pks new</code>创建新skill时自动基于模板生成骨架，不用从零写起。</p><p><strong>路径无关</strong>：pks通过符号链接解析和相对路径定位，在任何位置调用都能正确找到skills目录。不管你从哪个路径执行命令，它都知道自己的家在哪。</p><h2 id="回到那句话"><a href="#回到那句话" class="headerlink" title="回到那句话"></a>回到那句话</h2><p>上篇说”Agent只是工具，文档才是核心”。这篇把它往前推了一步：<strong>文档化的工作流，是可以被管理的。</strong></p><p>pks不是什么高深的东西，408行bash而已。但它代表了一个态度：你的工作流是你的资产，不是任何平台的附属品。</p><p>该薅token薅token，该管skill管skill。钱要省，活要干好，这两件事不矛盾。</p><p>原文地址：<a href="https://lichuanyang.top/posts/26061/">https://lichuanyang.top/posts/26061/</a></p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-Skills-管理和-Agent-管理有什么区别？"><a href="#Q-Skills-管理和-Agent-管理有什么区别？" class="headerlink" title="Q: Skills 管理和 Agent 管理有什么区别？"></a>Q: Skills 管理和 Agent 管理有什么区别？</h3><p>Skills 管理是<strong>独立于 Agent 平台的</strong>，你把所有 skill 统一放在一个全局仓库里，项目需要什么就装什么。Agent 管理则是把 skill 配进某个 Agent 的内部配置中（如 OpenCode 的 <code>.opencode/skills/</code> 或 Cursor 的 <code>.cursorrules</code>），绑定到具体平台。前者的好处是换平台时不需要迁移任何 skill，后者虽然集成度高，但一旦切 Agent 就需要重新配置。</p><h3 id="Q-跨平台兼容性怎么保证？"><a href="#Q-跨平台兼容性怎么保证？" class="headerlink" title="Q: 跨平台兼容性怎么保证？"></a>Q: 跨平台兼容性怎么保证？</h3><p>关键是 <strong>纯 Markdown</strong>。pks 安装 skill 后生成的都是 <code>.md</code> 文件，放在项目的 <code>.skills/</code> 目录下，附带一个自动维护的 <code>INDEX.md</code> 索引文件。任何 Agent 扫描项目目录时都会发现这个结构，读 INDEX.md 就知道有哪些 skill，再按需加载具体的 SKILL.md。不需要特定格式，不需要平台 SDK，纯 Markdown 是最大的公约数。</p><h3 id="Q-pks-和直接在-Agent-里配置-skill-有什么不同？"><a href="#Q-pks-和直接在-Agent-里配置-skill-有什么不同？" class="headerlink" title="Q: pks 和直接在 Agent 里配置 skill 有什么不同？"></a>Q: pks 和直接在 Agent 里配置 skill 有什么不同？</h3><p>直接配在 Agent 里方便，但 skill 会”绑定”到那个 Agent。pks 的做法是<strong>从 Agent 中解耦 skill 管理</strong>——skill 存在全局仓库，按需安装到任何项目中。最大的实际好处：按需安装意味着<strong>只有装了的 skill 才消耗 token</strong>。一个纯前端项目不需要装数据库规范的 skill，一个老项目不需要装新人引导的 skill。相比把所有规则塞进一个大配置文件让 Agent 每次对话都吃一遍，pks 能省下大量无用 token。</p><h3 id="Q-为什么用-bash-而不是-Python-或-Node-js？"><a href="#Q-为什么用-bash-而不是-Python-或-Node-js？" class="headerlink" title="Q: 为什么用 bash 而不是 Python 或 Node.js？"></a>Q: 为什么用 bash 而不是 Python 或 Node.js？</h3><h2 id="零依赖。skill-管理工具本身不应该引入任何运行时负担——你的工作流已经够复杂了。408-行-bash-脚本在-macOS-和-Linux-上开箱即用，不需要安装解释器、不需要管理虚拟环境、不会因为某个运行时版本升级而挂掉。bash-十年前的脚本今天还能跑，这保证了你的工作流文档比任何框架都长寿。"><a href="#零依赖。skill-管理工具本身不应该引入任何运行时负担——你的工作流已经够复杂了。408-行-bash-脚本在-macOS-和-Linux-上开箱即用，不需要安装解释器、不需要管理虚拟环境、不会因为某个运行时版本升级而挂掉。bash-十年前的脚本今天还能跑，这保证了你的工作流文档比任何框架都长寿。" class="headerlink" title="零依赖。skill 管理工具本身不应该引入任何运行时负担——你的工作流已经够复杂了。408 行 bash 脚本在 macOS 和 Linux 上开箱即用，不需要安装解释器、不需要管理虚拟环境、不会因为某个运行时版本升级而挂掉。bash 十年前的脚本今天还能跑，这保证了你的工作流文档比任何框架都长寿。"></a>零依赖。skill 管理工具本身不应该引入任何运行时负担——你的工作流已经够复杂了。408 行 bash 脚本在 macOS 和 Linux 上开箱即用，不需要安装解释器、不需要管理虚拟环境、不会因为某个运行时版本升级而挂掉。bash 十年前的脚本今天还能跑，这保证了你的工作流文档比任何框架都长寿。</h2><p>原文地址：<a href="https://lichuanyang.top/posts/26061/">https://lichuanyang.top/posts/26061/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/posts/26061/</id>
    <link href="https://lichuanyang.top/posts/26061/"/>
    <published>2026-06-04T14:00:00.000Z</published>
    <summary>上篇说&quot;文档才是核心&quot;，那文档怎么管？我用400行bash写了个skill管理器，把AI工作流打包成可移植的技能包，按需装到项目里，不绑定任何Agent平台。</summary>
    <title>教你薅token（二）：构建agent无关的skills管理工作流</title>
    <updated>2026-06-27T03:21:42.177Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="AI实践" scheme="https://lichuanyang.top/categories/AI%E5%AE%9E%E8%B7%B5/"/>
    <category term="效率工具" scheme="https://lichuanyang.top/tags/%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/"/>
    <category term="AI Agent" scheme="https://lichuanyang.top/tags/AI-Agent/"/>
    <category term="LLM" scheme="https://lichuanyang.top/tags/LLM/"/>
    <category term="工作流" scheme="https://lichuanyang.top/tags/%E5%B7%A5%E4%BD%9C%E6%B5%81/"/>
    <category term="prompt" scheme="https://lichuanyang.top/tags/prompt/"/>
    <content>
      <![CDATA[<p>目前大家使用AI的最大或者唯一痛点，是什么？可能就是账单了。</p><p>无论是Claude、CodeX、Cursor，还是qoder或是mimo、minimax，基础套餐基本都要奔着20刀以上去，这还得省着用，很多时候用以来会很不爽。要想量大管饱，那就会更贵。</p><p>而同时，很多平台会有试用装，或是常规的免费模型。比如OpenCode里的deepseek flash长期免费。</p><p>我们怎么才能好好的薅一下这些免费模型呢？</p><p>我长期在各种不同的项目用Agent，有代码，有个人博客，有笔记知识库，有小说，还有我和AI完的小游戏。用着用着发现一个事：<strong>Agent的所有操作，本质上是：读你文档 → 拼prompt → 发给模型 → 按结果改文件。</strong></p><p>文档在哪里，智能就在哪里。你自己拼prompt，跟平台帮你拼，其实没差那么多。</p><p>所以我后来干脆不再纠结用哪个Agent平台了。文档维护好，哪里有免费token就去哪薅。</p><span id="more"></span><h2 id="Agent到底干了什么"><a href="#Agent到底干了什么" class="headerlink" title="Agent到底干了什么"></a>Agent到底干了什么</h2><p>不管哪家Agent，流程都差不多：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">用户说需求 → Agent看项目文档 → 读相关文件 → 拼prompt → 问模型 → 处理返回 → 改文件 → 循环上述流程</span><br></pre></td></tr></table></figure><p>比如让它帮你修个bug，它：</p><ol><li>先读你项目里的 <code>AGENTS.md</code>、<code>README.md</code>，知道这项目是啥、目录在哪</li><li>再找到bug涉及的几个文件，读源码</li><li>把”项目上下文 + 相关代码 + bug描述 + 修改要求”拼成一个prompt</li><li>发给大模型</li><li>把返回的结果写成代码修改</li></ol><p>这些步骤，agent硬有差别，能有多大差别？</p><p>别误会，这里不是要全盘否定一些公认的好Agent。好的Agent在上下文管理、流程控制、工具集成上确实做得顺手，不然也不会流行。但，“人的智能”，完全可以补上这些差距。</p><p>比如上下文管理，Agent能定位到”这个bug涉及用户认证模块，需要读这三个文件”——可前提是它已经看过了你的AGENTS.md。那些文件路径、模块职责、约定俗成的写法，本来就是你自己写进去的。</p><p>文档不好的时候，好的agent能有效率更高的方式找到需要的信息。但在文档完备的情况下，agent的差距就微乎其微了。</p><p>总之，好的Agent在某些场景下，确实是方便，但是，不值这个价格。省下的那点维护文档、切窗口的时间，换每个月几十上百刀订阅费，性价比太低了。</p><p>对我个人来说，维护一份完备的agent指南，性价比高多了。何况，这些指南文档，也完全可以让AI自己写。</p><h2 id="文档才是关键"><a href="#文档才是关键" class="headerlink" title="文档才是关键"></a>文档才是关键</h2><p>想明白上面那段之后，你还得接受一个反直觉的事实：<strong>真正值钱的，不是Agent平台，甚至不是模型，而是你自己的工作流。</strong></p><p>大模型谁都能调，Agent平台也大多只是编排层，功能都差不多。但你的项目文档和你对项目流程的管理，是唯一的，里面记着项目结构、设计决策、踩过的坑、你偏好的写法。</p><p>同样的文档换个Agent平台，效果基本没差；可文档写得垃圾的话，放哪个平台都救不回来。</p><p>我在代码项目里是这么维护的：</p><ul><li>项目概述文档：技术栈、目录结构、关键模块说明</li><li>工作流文档：”新增功能怎么走”、”修bug步骤”、”发布流程”</li><li>规则文档：命名规范、注释要求、踩坑记录</li></ul><p>同一个项目目录，用不同的agent打开，效果几乎没区别。</p><p>小说创作项目同理。世界观、角色档案、章节大纲、写作规范，这些文档不变，换不同模型产出的风格都能保持一致。<strong>文档的样子定了，内容质量就定了。</strong></p><p>包括模型这一层，差别也没那么大。deepseek-v4-flash, 足够处理绝大多数问题。</p><p>就算你真的要花钱买时间，该投资的也是文档本身，不是挑平台。</p><h2 id="薅token实战"><a href="#薅token实战" class="headerlink" title="薅token实战"></a>薅token实战</h2><p>工作流搬到文档之后，最大的好处就是：平台随便切。</p><p>哪家有免费token用哪家，用完换下一家。</p><p>比如这些：</p><ul><li>OpenCode：每天有免费DeepSeek Flash</li><li>Qoder：新用户送一些试用credits，还有免费模型额度</li><li>Codex：试用套装，我其实到现在都还没轮到去薅他，总想着遇到一些复杂场景再去用它的免费额度，免得浪费了。但实际上根本遇不到这种场景。</li><li>Hermes：不定期放出免费模型，有时候能意外捡到好模型，比如前阵子有免费的deepseek flash</li><li>trae: 完全免费，只是要排队，有时候真有些任务觉得免费模型搞不动了，临时去trae跑一下，也完全没问题</li></ul><p>除此之外还有不少路子：</p><ul><li>大模型API的新用户赠金：OpenAI、Anthropic、DeepSeek等，新号通常送几美元。直接调API比走Agent平台更便宜</li><li>开源模型的免费托管：HuggingFace、Together AI有免费推理额度，小任务完全够用</li><li>学生优惠：GitHub Student Pack之类的，送一堆平台credits</li><li>一些社区活动：经常有AI平台送额度</li></ul><p>所以这篇要说的结论其实挺简单：<strong>Agent只是工具，文档才是核心。</strong> 把文档维护好，平台随便换，哪里免费去哪薅。一个月省几十刀，换一顿火锅，不香吗？ </p><p>至于那些每个月烧几百刀token的重度用户——我只能说，你们的钱花得值，但我的token没花钱，效果也差不多。</p><p>原文地址：<a href="https://lichuanyang.top/posts/26060/">https://lichuanyang.top/posts/26060/</a></p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-Agent-和-Skill-是什么关系？"><a href="#Q-Agent-和-Skill-是什么关系？" class="headerlink" title="Q: Agent 和 Skill 是什么关系？"></a>Q: Agent 和 Skill 是什么关系？</h3><p>Agent 是”干活的人”，Skill 是”教它怎么干活的说明书”。Agent 的底层模型提供了通用推理能力，但它不知道你的项目结构、编码规范、API 设计约束——这些知识需要通过 Skill（结构化指令文档）告诉它。两者的关系可以这样理解：Agent &#x3D; 通用推理引擎 + 按需加载 Skill。把 Skill 维护好，换不同的 Agent 平台也能保持一致的输出质量。</p><h3 id="Q-Token-优化最有效的方法是什么？"><a href="#Q-Token-优化最有效的方法是什么？" class="headerlink" title="Q: Token 优化最有效的方法是什么？"></a>Q: Token 优化最有效的方法是什么？</h3><p>不是换更便宜的模型，而是<strong>写好文档</strong>。一份完备的 AGENTS.md 或 SKILL.md，能让 Agent 精准定位到需要读取的文件，避免每次做任务都全量扫描项目。相比之下，在不完备的文档上折腾模型选择或 prompt 精简，省下的 token 远远小于文档带来的提升。先花时间把文档写清楚，再去薅免费 token，这是效益最高的路径。</p><h3 id="Q-文档维护和-Agent-订阅哪个更值？"><a href="#Q-文档维护和-Agent-订阅哪个更值？" class="headerlink" title="Q: 文档维护和 Agent 订阅哪个更值？"></a>Q: 文档维护和 Agent 订阅哪个更值？</h3><p>文档维护更值。Agent 订阅每月花几十上百美元，本质上是为平台帮你”找文件、拼 prompt”的便利性付费。但如果你的项目文档已经足够完备——技术栈、目录结构、设计决策、编码规范都写清楚了——那么换个免费 Agent 效果几乎没差。文档是你自己的资产，可以跨平台复用；Agent 订阅是消耗品，换了平台就得重新来。</p><h3 id="Q-免费模型真的够用吗？"><a href="#Q-免费模型真的够用吗？" class="headerlink" title="Q: 免费模型真的够用吗？"></a>Q: 免费模型真的够用吗？</h3><p>对绝大多数日常任务够用。作者的经验是 DeepSeek Flash 级别的免费模型就能处理大部分代码修改、文档生成、知识整理等任务。遇到确实搞不动的复杂场景，可以临时切到有免费试用额度的平台（如 Trae、Codex 试用套装）。真正的瓶颈通常不是模型能力，而是你给它的上下文（文档）够不够好。</p><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><ol><li><strong>理解 Agent 与 Skill 的关系</strong>：Agent 是执行引擎（负责调模型、读文件、改代码），Skill 是结构化指令文档（告诉 Agent 你的项目结构、编码规范、操作流程）。文档质量直接决定 Agent 的输出质量，好的文档可以让不同 Agent 平台的产出高度一致。</li><li><strong>编写项目文档</strong>：在项目根目录创建 <code>AGENTS.md</code>，至少包含：项目概述（技术栈、目录结构、关键模块）、工作流说明（新增功能怎么做、修 bug 步骤、发布流程）、规则文档（命名规范、注释要求、踩坑记录）。</li><li><strong>让 AI 辅助写文档</strong>：将 karpathy 的 llm-wiki gist 或已有的项目规范喂给 AI，让它自动生成 AGENTS.md 和各模块的 SKILL 文档，人工审核后放入项目目录。</li><li><strong>切换平台验证</strong>：用不同的免费 Agent 平台（OpenCode、Qoder、Hermes 等）打开同一个项目，执行相同任务（如修 bug、加功能），对比输出质量。如果文档完备，差异应该很小。</li></ol><p>原文地址：<a href="https://lichuanyang.top/posts/26060/">https://lichuanyang.top/posts/26060/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/posts/26060/</id>
    <link href="https://lichuanyang.top/posts/26060/"/>
    <published>2026-06-03T14:00:00.000Z</published>
    <summary>我折腾了一阵子AI Agent，发现它本质上就是把文档和prompt拼来拼去。与其花钱订阅，不如自己维护好项目文档，然后哪里免费token用哪里。</summary>
    <title>教你薅token：构建agent无关的AI工作流</title>
    <updated>2026-06-27T03:21:42.170Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="AI实践" scheme="https://lichuanyang.top/categories/AI%E5%AE%9E%E8%B7%B5/"/>
    <category term="效率工具" scheme="https://lichuanyang.top/tags/%E6%95%88%E7%8E%87%E5%B7%A5%E5%85%B7/"/>
    <category term="AI" scheme="https://lichuanyang.top/tags/AI/"/>
    <category term="AI Agent" scheme="https://lichuanyang.top/tags/AI-Agent/"/>
    <category term="hexo" scheme="https://lichuanyang.top/tags/hexo/"/>
    <category term="blog" scheme="https://lichuanyang.top/tags/blog/"/>
    <category term="automation" scheme="https://lichuanyang.top/tags/automation/"/>
    <category term="LLM" scheme="https://lichuanyang.top/tags/LLM/"/>
    <content>
      <![CDATA[<h2 id="背景：一个繁琐到让人崩溃的任务"><a href="#背景：一个繁琐到让人崩溃的任务" class="headerlink" title="背景：一个繁琐到让人崩溃的任务"></a>背景：一个繁琐到让人崩溃的任务</h2><p>最近想给自己的 Hexo 博客换个主题。用了好几年的 Next 主题，虽然经典，但想换个更现代的风格。</p><p>看起来很简单对吧？但实际操作过的朋友都知道，Hexo 主题迁移是一个极其繁琐的过程：</p><ol><li><strong>配置文件长达 1000+ 行</strong>，需要逐项对比新旧主题的配置格式</li><li><strong>功能映射复杂</strong>：Next 的 <code>leancloud_visitors</code> 对应 Butterfly 的 <code>busuanzi</code>，Next 的 <code>reading_progress</code> 对应 Butterfly 的 <code>preloader</code></li><li><strong>两个站点要同步</strong>：中英文站都要改，而且配置略有不同</li><li><strong>Font Awesome 版本兼容</strong>：Butterfly 默认配的 FA 7.1.0 根本不存在于 cdnjs</li><li><strong>YAML 格式敏感</strong>：缩进、换行符都可能导致配置失效</li></ol><p>如果手动操作，估计要花一整天，而且很容易出错。</p><h2 id="解决方案：让-AI-Agent-来干"><a href="#解决方案：让-AI-Agent-来干" class="headerlink" title="解决方案：让 AI Agent 来干"></a>解决方案：让 AI Agent 来干</h2><p>这次我尝试了一个不同的方式：<strong>让 AI Agent（Hermes）全程接管这个任务</strong>。</p><h3 id="AI-Agent-的工作流程"><a href="#AI-Agent-的工作流程" class="headerlink" title="AI Agent 的工作流程"></a>AI Agent 的工作流程</h3><p>整个过程，AI Agent 自主完成了以下工作：</p><ol><li><strong>环境分析</strong>：检查当前 Hexo 版本（7.0.0）、Node.js 版本（v24.14.0）、两个站点的配置差异</li><li><strong>版本调研</strong>：查询 GitHub API，获取最新 Hexo 版本（8.1.2）和 breaking changes</li><li><strong>依赖升级</strong>：批量更新 hexo 及所有插件到最新版本</li><li><strong>主题对比</strong>：分析 Next 和 Butterfly 的功能覆盖度，生成对比表格</li><li><strong>配置迁移</strong>：将 Next 的 1000+ 行配置逐项映射到 Butterfly 格式</li><li><strong>问题排查</strong>：<ul><li>发现 FA 7.1.0 在 cdnjs 上 404，降级到 6.7.2</li><li>发现知乎图标需要 <code>fab</code> 前缀</li><li>发现英文站菜单路径错误</li><li>发现 GA、百度统计、站长验证等关键配置丢失</li></ul></li><li><strong>测试验证</strong>：构建测试、本地预览服务器、检查 HTML 输出</li><li><strong>代码提交</strong>：自动生成 commit message 并 push</li></ol><h3 id="关键技术点"><a href="#关键技术点" class="headerlink" title="关键技术点"></a>关键技术点</h3><h4 id="1-Font-Awesome-版本兼容性问题"><a href="#1-Font-Awesome-版本兼容性问题" class="headerlink" title="1. Font Awesome 版本兼容性问题"></a>1. Font Awesome 版本兼容性问题</h4><p>这是最隐蔽的一个 bug。Butterfly 主题的 <code>plugins.yml</code> 配置了 FA 7.1.0：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">fontawesome:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">&#x27;@fortawesome/fontawesome-free&#x27;</span></span><br><span class="line">  <span class="attr">version:</span> <span class="number">7.1</span><span class="number">.0</span></span><br></pre></td></tr></table></figure><p>但 cdnjs 上根本没有这个版本！导致所有品牌图标（GitHub、StackOverflow、知乎）都不加载。</p><p>AI Agent 通过以下步骤定位问题：</p><ul><li>检查生成的 HTML，发现加载的是 FA 7.1.0</li><li>请求 cdnjs API，确认 7.1.0 返回 404</li><li>测试 FA 6.7.2，确认包含所有需要的图标</li><li>在配置中覆盖 CDN 地址</li></ul><h4 id="2-YAML-配置迁移的陷阱"><a href="#2-YAML-配置迁移的陷阱" class="headerlink" title="2. YAML 配置迁移的陷阱"></a>2. YAML 配置迁移的陷阱</h4><p>Hexo 的配置文件是 YAML 格式，对缩进和换行符非常敏感。AI Agent 在迁移过程中遇到了几个问题：</p><ul><li><strong>CRLF vs LF</strong>：Butterfly 默认配置是 CRLF 换行，用 sed 替换时匹配失败</li><li><strong>重复键</strong>：patch 工具导致 <code>scroll_percent</code> 出现两次</li><li><strong>值丢失</strong>：YAML 缩进不对导致配置值没有正确写入</li></ul><p>最终用 Python 直接操作文件内容才解决了这些问题。</p><h4 id="3-多站点同步"><a href="#3-多站点同步" class="headerlink" title="3. 多站点同步"></a>3. 多站点同步</h4><p>中英文站的配置大部分相同，但有几处差异：</p><ul><li>英文站的 <code>language</code> 需要改为 <code>en</code></li><li>英文站的菜单需要指向中文站</li><li>英文站的 Valine placeholder 需要用英文</li></ul><p>AI Agent 自动识别这些差异并分别处理。</p><h2 id="效果对比"><a href="#效果对比" class="headerlink" title="效果对比"></a>效果对比</h2><table><thead><tr><th>指标</th><th>手动操作</th><th>AI Agent</th></tr></thead><tbody><tr><td>耗时</td><td>4-8 小时</td><td>30 分钟</td></tr><tr><td>出错率</td><td>高（容易遗漏配置）</td><td>低（系统化检查）</td></tr><tr><td>问题排查</td><td>需要逐个 Google</td><td>自动定位根因</td></tr><tr><td>代码提交</td><td>手动写 commit message</td><td>自动生成</td></tr></tbody></table><h2 id="AI-Agent-的优势"><a href="#AI-Agent-的优势" class="headerlink" title="AI Agent 的优势"></a>AI Agent 的优势</h2><p>这次实践让我深刻体会到 <strong>AI Agent 在这类繁琐、重复性工作中</strong> 的巨大优势：</p><h3 id="1-系统化思维"><a href="#1-系统化思维" class="headerlink" title="1. 系统化思维"></a>1. 系统化思维</h3><p>AI Agent 不会像人一样”想到哪改到哪”。它会：</p><ul><li>先分析当前状态</li><li>制定完整计划</li><li>逐步执行并验证</li><li>发现问题及时修复</li></ul><h3 id="2-跨领域知识"><a href="#2-跨领域知识" class="headerlink" title="2. 跨领域知识"></a>2. 跨领域知识</h3><p>主题迁移涉及多个技术领域：</p><ul><li>Hexo 配置</li><li>Font Awesome 版本管理</li><li>YAML 格式处理</li><li>Git 工作流</li><li>前端资源加载</li></ul><p>AI Agent 能够在这些领域之间自由切换，而人类开发者通常只精通其中一两个。</p><h3 id="3-持久注意力"><a href="#3-持久注意力" class="headerlink" title="3. 持久注意力"></a>3. 持久注意力</h3><p>人类在处理 1000+ 行配置文件时，注意力会逐渐下降，容易遗漏。AI Agent 不会疲劳，每个配置项都会被检查到。</p><h3 id="4-自动化验证"><a href="#4-自动化验证" class="headerlink" title="4. 自动化验证"></a>4. 自动化验证</h3><p>AI Agent 不仅会修改配置，还会：</p><ul><li>运行构建测试</li><li>启动本地服务器验证</li><li>检查 HTML 输出</li><li>确认关键配置生效</li></ul><h2 id="适用场景"><a href="#适用场景" class="headerlink" title="适用场景"></a>适用场景</h2><p>基于这次经验，我认为 <strong>AI Agent 特别适合以下类型的工作</strong>：</p><ol><li><strong>配置迁移</strong>：不同系统之间的配置格式转换</li><li><strong>依赖升级</strong>：批量更新多个包并处理兼容性问题</li><li><strong>代码重构</strong>：大规模的代码格式调整</li><li><strong>文档整理</strong>：从多个来源整合信息</li><li><strong>环境搭建</strong>：新项目的初始化配置</li></ol><h2 id="局限性"><a href="#局限性" class="headerlink" title="局限性"></a>局限性</h2><p>当然，AI Agent 也有局限：</p><ol><li><strong>创意性工作</strong>：UI 设计、文案撰写等仍需人类主导</li><li><strong>业务决策</strong>：是否升级、选择哪个主题等决策需要人类判断</li><li><strong>复杂调试</strong>：某些运行时问题需要人工介入</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这次用 AI Agent 完成 Hexo 主题迁移的实践，让我看到了 <strong>LLM（大语言模型）在软件工程领域的巨大潜力</strong>。</p><p>AI Agent 不是来取代开发者的，而是来增强我们的能力。它把我们从繁琐、重复的工作中解放出来，让我们能够专注于更有价值的事情。</p><p>如果你也有类似的”体力活”，不妨试试让 AI Agent 来帮忙。你会发现，<strong>AI 辅助开发</strong> 不是未来，而是现在。</p><h2 id="相关技术栈"><a href="#相关技术栈" class="headerlink" title="相关技术栈"></a>相关技术栈</h2><ul><li><strong>AI Agent</strong>：Hermes Agent</li><li><strong>LLM</strong>：DeepSeek V4 Pro &#x2F; MiMo v2.5</li><li><strong>静态站点生成器</strong>：Hexo 8.1.2</li><li><strong>主题</strong>：Butterfly 5.5.4</li><li><strong>图标库</strong>：Font Awesome 6.7.2</li><li><strong>评论系统</strong>：Valine (LeanCloud)</li><li><strong>统计分析</strong>：Google Analytics &#x2F; 百度统计</li></ul><hr><p><em>本文由 AI Agent 辅助撰写，记录了一次真实的主题迁移实践。</em></p><p>原文地址：<a href="https://lichuanyang.top/posts/48979/">https://lichuanyang.top/posts/48979/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/posts/48979/</id>
    <link href="https://lichuanyang.top/posts/48979/"/>
    <published>2026-05-28T14:00:00.000Z</published>
    <summary>用 AI Agent 全自动完成 Hexo 主题从 Next 到 Butterfly 的迁移，1000+ 行配置文件的自动映射与验证。</summary>
    <title>用 AI Agent 完成 Hexo 主题迁移：从 Next 到 Butterfly 的全自动化实践</title>
    <updated>2026-06-27T02:31:54.494Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="Vercel" scheme="https://lichuanyang.top/tags/Vercel/"/>
    <category term="博客" scheme="https://lichuanyang.top/tags/%E5%8D%9A%E5%AE%A2/"/>
    <category term="运维" scheme="https://lichuanyang.top/tags/%E8%BF%90%E7%BB%B4/"/>
    <category term="独立博客" scheme="https://lichuanyang.top/tags/%E7%8B%AC%E7%AB%8B%E5%8D%9A%E5%AE%A2/"/>
    <content>
      <![CDATA[<p>前几天写了篇新文章，照常往 Vercel 部署，结果 pipeline 直接报错了。一开始以为是构建问题，折腾了一会儿才发现不对——是 Vercel 账号出了状况。用 163 邮箱注册的账号怎么都登不上去了。后来才搞清楚，Vercel 把整个 163.com 邮箱根域给封禁了。</p><span id="more"></span><h2 id="先说说-Vercel-是什么"><a href="#先说说-Vercel-是什么" class="headerlink" title="先说说 Vercel 是什么"></a>先说说 Vercel 是什么</h2><p>如果你是搞独立博客的，大概率听过或者正在用 Vercel。简单说，它是一个前端部署平台，支持自动从 GitHub 仓库拉代码、构建、部署，给你的网站分配一个域名，还自带 CDN 加速。很多人的博客就是 GitHub Pages + Vercel 的组合——代码放在 GitHub，用 Vercel 来部署和托管。</p><p>我自己的博客也是这么搞的。GitHub Pages 本来也能直接部署静态页面，但 Vercel 在自定义域名、HTTPS、CDN 这些方面用起来更方便，所以就选了它。</p><h2 id="163-邮箱被封禁的来龙去脉"><a href="#163-邮箱被封禁的来龙去脉" class="headerlink" title="163 邮箱被封禁的来龙去脉"></a>163 邮箱被封禁的来龙去脉</h2><p>大概在 2026 年 5 月 4 号左右，大量使用 163 邮箱注册 Vercel 的用户发现，自己的账号突然无法登录了。我在网上搜了一下，发现不只是我一个人遇到这个问题。NodeSeek、V2EX 这些论坛上都有人在讨论。</p><p>根据网上的信息，Vercel 在 2026 年 4 月中旬遭遇了一次比较严重的安全事件。黑客团伙 ShinyHunters 通过一个第三方 AI 工具入侵了 Vercel 的内部系统，窃取了员工数据、企业后台权限和一些部署凭证，还索要了 200 万美元的赎金。</p><p>这次封禁 163 邮箱，大概率是安全事件后的应对措施之一。可能是因为 163 邮箱在此次事件中被大量用于恶意注册或攻击行为，也可能只是 Vercel 对某些邮箱域名做了更严格的风控。具体原因 Vercel 没有公开说明，但结果就是：整个 163.com 邮箱域名被一刀切了。</p><p>影响范围不小。很多国内开发者和独立博客作者都是用 163 邮箱注册的 Vercel，这次封禁直接导致这些人的博客全部无法访问。</p><p>不过如果你现在 Vercel 账号还处于登录状态（比如工作电脑的浏览器还保持着登录），其实是可以抢救一下的：进账号设置，添加一个新邮箱（Outlook、Gmail 都行），然后把 163 邮箱去掉。这样账号就能继续正常使用，不需要重新部署。网上有人分享了这个方法，能省不少事。但如果你跟我一样，发现的时候已经完全登不上去了，那就只能走下面的重建流程了。</p><h2 id="回忆我的博客部署架构"><a href="#回忆我的博客部署架构" class="headerlink" title="回忆我的博客部署架构"></a>回忆我的博客部署架构</h2><p>账号被封了，博客打不开了，第一件事就是得搞清楚自己的博客到底是怎么部署的。说实话，这个博客配置的时间已经比较久了，当时配完之后就没怎么动过，具体流程已经记不太清了。</p><p>花了不少功夫才回忆起来，大概的链路是这样的：</p><ol><li>博客源码放在 GitHub 仓库里</li><li>Vercel 连接 GitHub 仓库，负责构建和部署</li><li>域名解析这块，分了两层：阿里云的 DNS 指向 Cloudflare，Cloudflare 再指向 Vercel</li></ol><p>为什么要搞这么复杂？主要是为了用 Cloudflare 的 CDN 和安全防护能力。阿里云做域名注册商，Cloudflare 做中间层的 DNS 管理和 CDN，Vercel 做最终的静态页面托管。当年配的时候也是参考了网上的一些教程，虽然链路长了点，但稳定运行了很久，也就没再动过。</p><h2 id="恢复过程：新账号-重新部署"><a href="#恢复过程：新账号-重新部署" class="headerlink" title="恢复过程：新账号 + 重新部署"></a>恢复过程：新账号 + 重新部署</h2><p>搞清楚部署架构之后，恢复操作其实并不复杂。</p><p><strong>第一步：注册新的 Vercel 账号</strong></p><p>这次学聪明了，不用 163 邮箱了，用 Outlook 重新注册了一个 Vercel 账号。其实 Gmail 也行，但考虑到这次是 Vercel 封禁了特定邮箱域名，用国外主流的邮箱服务会更稳妥一些。</p><p><strong>第二步：重新部署 GitHub 项目</strong></p><p>登录新账号后，连接 GitHub，把博客仓库导入 Vercel。Vercel 自动识别了项目类型，构建和部署都是自动完成的，跟之前的操作基本一样。</p><p><strong>第三步：重新配置域名</strong></p><p>这一步是关键。因为之前域名解析是阿里云 → Cloudflare → Vercel 的链路，重新部署后需要把域名绑定到新的 Vercel 项目上。</p><p>让我惊喜的是，现在 Vercel 在添加自定义域名的时候，已经支持自动修改 Cloudflare 的 DNS 配置了。以前我记得需要手动去 Cloudflare 后台添加 CNAME 记录，现在 Vercel 直接通过 API 帮你搞定，点几下就完事了。</p><p>整个恢复过程，从注册新账号到博客重新可以访问，大概花了不到半小时。</p><h2 id="几点经验"><a href="#几点经验" class="headerlink" title="几点经验"></a>几点经验</h2><ol><li><p><strong>不要用国内邮箱注册海外服务</strong>。163、QQ 邮箱这类国内邮箱，在海外服务的风控体系里天然就是高风险对象。这次是 Vercel，下次可能是别的服务。建议主力账号用 Gmail 或 Outlook。</p></li><li><p><strong>记录好自己的部署架构</strong>。这次最浪费时间的环节就是回忆部署链路。当时配置完觉得都记住了，结果过了这么久早就忘得差不多了。建议把部署架构和关键配置写在笔记里，哪怕只是几句话。</p></li><li><p><strong>Vercel 现在对 Cloudflare 的集成做得不错</strong>。以前手动配 DNS 的时候容易出错，现在自动化程度高了很多，重新部署的成本降低了不少。</p></li><li><p><strong>有条件的话，做好备份和多平台准备</strong>。这次运气好，恢复起来比较快。但如果 GitHub 仓库也出了问题，或者 Vercel 彻底封了项目而不只是账号，恢复就会麻烦很多。重要的博客内容，建议在本地或者别的平台也有一份。</p></li></ol><h2 id="写在最后"><a href="#写在最后" class="headerlink" title="写在最后"></a>写在最后</h2><p>这次 Vercel 封禁 163 邮箱的事，算是给所有用国内邮箱注册海外服务的人提了个醒。平台侧的安全措施我们控制不了，但自己的账号安全和部署架构，还是可以提前做好准备的。</p><p>如果你也遇到了 Vercel 163 邮箱被封、Vercel 账号无法登录的问题，希望这篇文章能帮到你。恢复的核心思路就是：换一个邮箱重新注册，把项目重新部署一遍，域名重新绑定一下。操作不复杂，但前提是你得记得自己的部署链路。</p><p>原文地址: <a href="https://lichuanyang.top/posts/39648/">https://lichuanyang.top/posts/39648/</a></p><hr><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><h3 id="Step-1-发现问题"><a href="#Step-1-发现问题" class="headerlink" title="Step 1: 发现问题"></a>Step 1: 发现问题</h3><p>当博客无法访问或 Vercel 部署报错时，首先确认是否是账号被封禁。尝试登录 Vercel，如果使用 163 邮箱注册的账号无法登录，很可能已被封禁。</p><h3 id="Step-2-排查原因"><a href="#Step-2-排查原因" class="headerlink" title="Step 2: 排查原因"></a>Step 2: 排查原因</h3><p>搜索相关论坛（NodeSeek、V2EX 等），确认是否是 Vercel 大面积封禁 163 邮箱。了解封禁的背景和影响范围，做到心中有数。</p><h3 id="Step-3-迁移邮箱验证"><a href="#Step-3-迁移邮箱验证" class="headerlink" title="Step 3: 迁移邮箱验证"></a>Step 3: 迁移邮箱验证</h3><p>如果还能登录账号，立即在账号设置中添加 Gmail 或 Outlook 邮箱，并移除 163 邮箱。如果已无法登录，用新邮箱重新注册 Vercel 账号。建议使用 Gmail 或 Outlook 等海外主流邮箱。</p><h3 id="Step-4-重新部署"><a href="#Step-4-重新部署" class="headerlink" title="Step 4: 重新部署"></a>Step 4: 重新部署</h3><p>登录新账号后，连接 GitHub 导入博客仓库。Vercel 会自动识别项目类型并完成构建和部署，操作流程与首次部署基本一致。</p><h3 id="Step-5-验证恢复"><a href="#Step-5-验证恢复" class="headerlink" title="Step 5: 验证恢复"></a>Step 5: 验证恢复</h3><p>重新绑定自定义域名。Vercel 现已支持自动修改 Cloudflare DNS 配置，操作非常简便。确认博客所有页面可正常访问，检查 HTTPS、CDN 等功能是否正常运作。</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/39648/</id>
    <link href="https://lichuanyang.top/posts/39648/"/>
    <published>2026-05-11T11:30:00.000Z</published>
    <summary>Vercel 封禁 163 邮箱导致博客部署失败的亲身经历，以及从发现问题到迁移恢复的完整解决方案。</summary>
    <title>Vercel封禁163邮箱后，我是怎么恢复博客的</title>
    <updated>2026-06-27T03:43:25.702Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="AI实践" scheme="https://lichuanyang.top/categories/AI%E5%AE%9E%E8%B7%B5/"/>
    <category term="LLM" scheme="https://lichuanyang.top/tags/LLM/"/>
    <category term="知识管理" scheme="https://lichuanyang.top/tags/%E7%9F%A5%E8%AF%86%E7%AE%A1%E7%90%86/"/>
    <category term="安全规范" scheme="https://lichuanyang.top/tags/%E5%AE%89%E5%85%A8%E8%A7%84%E8%8C%83/"/>
    <category term="wiki" scheme="https://lichuanyang.top/tags/wiki/"/>
    <category term="实践" scheme="https://lichuanyang.top/tags/%E5%AE%9E%E8%B7%B5/"/>
    <content>
      <![CDATA[<p>最近在整理团队的安全开发规范时，遇到了一个老问题：安全规范文档越积越多，但真正用的时候却找不到。每次新人入职，都要翻遍各种文档才能拼凑出完整的安全规范；每次出安全故障，事后总结的经验也散落在各处，下次遇到类似问题还得重新排查。</p><p>我试过用Confluence、Notion、甚至Git仓库的README来管理，但效果都不理想。直到看到了Karpathy提的llm-wiki思路，才觉得这可能是个突破口。</p><span id="more"></span><h2 id="Karpathy的llm-wiki是什么？"><a href="#Karpathy的llm-wiki是什么？" class="headerlink" title="Karpathy的llm-wiki是什么？"></a>Karpathy的llm-wiki是什么？</h2><p>Karpathy在<a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f">Gist</a>里提了个想法：让LLM维护一个Wiki中间层。原话是”the wiki is a persistent, compounding artifact”——这个Wiki层就像个编译器，把原始文档编译成结构化数据，这样LLM每次回答问题时就不用从头在原始文档里”找”信息，直接查Wiki就行。</p><p>我觉得这个思路特别妙。传统文档管理是”存”和”找”，而llm-wiki是”编译”和”查询”。原始文档可能很乱、有重复、甚至有矛盾，但Wiki层是干净、结构化、有交叉引用的。</p><p>举个例子：我们团队有十几份安全开发规范文档，从输入验证到渗透测试都有，还有近十份安全故障复盘报告。这些文档格式不一，有的在Word里，有的在Markdown里，有的甚至只是Slack聊天记录。如果让LLM直接处理这些原始材料，效率会很低。但如果先”编译”成Wiki，效果就完全不一样了。</p><h2 id="我的实现过程"><a href="#我的实现过程" class="headerlink" title="我的实现过程"></a>我的实现过程</h2><h3 id="第一步：生成CLAUDE-md"><a href="#第一步：生成CLAUDE-md" class="headerlink" title="第一步：生成CLAUDE.md"></a>第一步：生成CLAUDE.md</h3><p>我首先把Karpathy的Gist链接丢给Claude，让它基于这个思路生成一份CLAUDE.md文件。这个文件相当于给LLM的”操作手册”，告诉它如何维护Wiki。</p><p>生成的CLAUDE.md包含了几个关键部分：</p><ul><li>三层架构说明（raw → wiki → output）</li><li>Wiki页面规范（frontmatter格式、交叉引用规则）</li><li>摄入工作流（如何处理新素材）</li><li>查询工作流（如何回答用户问题）</li><li>Lint检查（如何保证Wiki健康）</li></ul><p>实际用下来，我发现模型生成的结果基本可用，没有做太多微调。只是另外让AI生成了output层，用于存放最终的规范文档。这样三层架构就完整了：raw放原始素材，wiki放结构化Wiki，output放最终输出。</p><h3 id="第二步：收集和整理原始素材"><a href="#第二步：收集和整理原始素材" class="headerlink" title="第二步：收集和整理原始素材"></a>第二步：收集和整理原始素材</h3><p>这是最耗时的一步。我把散落在各处的安全开发规范文档和故障案例都收集起来，统一放到raw&#x2F;sources&#x2F;目录下。为了保持原貌，我没有修改这些文件的内容，也没有统一格式——PDF、Word、PPT、Markdown等各种格式直接放到raw层就行，模型都可以处理。</p><p>具体来说，我收集了：</p><ul><li>十几份安全开发规范文档：涵盖输入验证、SQL注入防护、XSS防护、CSRF防护、敏感数据保护、日志安全等</li><li>近十份安全故障复盘报告：包括SQL注入漏洞、越权访问、敏感信息泄露、会话劫持等真实案例</li><li>几份安全架构方案文档：涉及零信任架构、安全编码指南、渗透测试流程等</li></ul><p>这些素材质量参差不齐。有的写得很规范，有清晰的标题和列表；有的就是随手记的笔记，甚至还有截图。但没关系，llm-wiki的妙处就在于它能处理这种”脏数据”。</p><h3 id="第三步：让LLM生成Wiki层"><a href="#第三步：让LLM生成Wiki层" class="headerlink" title="第三步：让LLM生成Wiki层"></a>第三步：让LLM生成Wiki层</h3><p>有了CLAUDE.md和原始素材，就可以让LLM开始工作了。我这里用的是Claude，主要是因为它的上下文窗口足够大，能一次性处理较多内容。</p><p>具体操作是这样的：</p><ol><li>把CLAUDE.md作为系统提示词</li><li>告诉LLM：”请按照CLAUDE.md中的规范，处理raw&#x2F;sources&#x2F;目录下的所有素材，生成wiki层”</li><li>LLM会依次读取每个原始文件，提取关键信息，生成对应的Wiki页面</li></ol><p>这个过程不是一次就能完成的。LLM生成的初版Wiki会有一些问题：</p><ul><li>交叉引用不够充分</li><li>有些概念没有被提取成独立页面</li><li>页面之间的逻辑关系不够清晰</li></ul><p>所以需要多轮迭代。我会检查生成的Wiki，指出问题，让LLM修正。比如：<br>“这个故障案例里提到了’连接池配置不当’，但为什么没有对应的concept页面？请创建一个[[connection-pool-best-practices]]页面，并在故障案例里引用它。”</p><p>经过5-6轮迭代，Wiki层才逐渐成型。</p><h3 id="第四步：生成最终规范文档"><a href="#第四步：生成最终规范文档" class="headerlink" title="第四步：生成最终规范文档"></a>第四步：生成最终规范文档</h3><p>Wiki层准备好后，就可以生成最终的规范文档了。我让LLM基于Wiki层，生成一份面向开发者的、可直接阅读的规范文档。</p><p>这里有个设计决策：最终文档是放在output&#x2F;目录下，而不是直接修改原始素材。这样保持了三层架构的清晰性：</p><ul><li>raw&#x2F;：原始素材，不可修改</li><li>wiki&#x2F;：结构化Wiki，LLM维护</li><li>output&#x2F;：最终输出，人类阅读</li></ul><p>生成的规范文档有几个特点：</p><ol><li>每个规范条目都有”为什么”——不只是说”要这样做”，还解释”为什么要这样做”</li><li>相关的故障案例会作为”反面教材”链接在旁边</li><li>有交叉引用，比如”数据库连接池配置”会链接到”连接池最佳实践”这个concept页面</li></ol><h2 id="实际效果"><a href="#实际效果" class="headerlink" title="实际效果"></a>实际效果</h2><h3 id="阅读体验"><a href="#阅读体验" class="headerlink" title="阅读体验"></a>阅读体验</h3><p>最终的规范文档可以直接用Obsidian打开阅读。因为是Markdown格式，支持：</p><ul><li>代码块高亮</li><li>内部链接跳转（点击就能跳转到相关概念或故障案例）</li><li>标签筛选（比如筛选所有#安全相关的规范）</li></ul><p>更重要的是，每条安全规范都关联着真实的故障案例。比如在”SQL注入防护”这条规范旁边，就链接着[[sql-injection-vulnerability-incident]]这个故障案例，详细描述了漏洞是怎么产生的、造成了什么影响、怎么修复的。这样开发者不仅能知道”要怎么做”，还能看到”不这么做会出什么问题”。</p><p>比起以前散落在各处的文档，阅读体验好了不止一个量级。</p><h3 id="LLM问答"><a href="#LLM问答" class="headerlink" title="LLM问答"></a>LLM问答</h3><p>更强大的是LLM问答能力。因为有了Wiki层，LLM回答问题时特别准。比如我问：<br>“数据库批量更新时要注意什么？”</p><p>LLM会：</p><ol><li>先在Wiki里找到[[batch-operation-security]]这个concept页面</li><li>结合[[sql-injection-batch-incident]]这个故障案例</li><li>给出具体的建议：必须使用参数化查询、禁止字符串拼接SQL、批量操作要有事务控制、超大批次要分页处理等</li><li>还会解释为什么：因为批量操作一旦出问题，影响范围是单条操作的N倍</li></ol><p>再比如问：”用户上传文件怎么处理才安全？”</p><p>LLM会：</p><ol><li>找到[[file-upload-security]]这个concept页面</li><li>结合[[malicious-file-upload-incident]]这个故障案例</li><li>给出具体建议：文件类型白名单校验、文件内容检测、重命名存储、限制文件大小、隔离存储等</li></ol><p>这比让LLM在十几份原始文档里”找”答案要靠谱得多。</p><h3 id="持续更新"><a href="#持续更新" class="headerlink" title="持续更新"></a>持续更新</h3><p>最让我满意的是更新机制。当有新的规范文档或故障报告时，我只需要：</p><ol><li>把新文件放到raw&#x2F;sources&#x2F;目录下</li><li>告诉LLM：”有新素材需要摄入”</li><li>LLM会自动更新Wiki层和规范文档</li></ol><p>比如上周我们出了个新的安全故障：因为用户输入未过滤导致XSS漏洞。我把故障报告放到raw目录后，LLM自动：</p><ol><li>创建了[[xss-user-input-incident]]这个故障案例页面</li><li>更新了[[input-validation-security]]这个concept页面，补充了XSS相关的注意事项</li><li>在规范文档的”输入验证规范”章节增加了相关条目</li></ol><h2 id="遇到的问题和解决方案"><a href="#遇到的问题和解决方案" class="headerlink" title="遇到的问题和解决方案"></a>遇到的问题和解决方案</h2><h3 id="问题1：LLM生成的Wiki质量不稳定"><a href="#问题1：LLM生成的Wiki质量不稳定" class="headerlink" title="问题1：LLM生成的Wiki质量不稳定"></a>问题1：LLM生成的Wiki质量不稳定</h3><p>刚开始时，LLM生成的Wiki质量波动很大。有时能很好地提取关键信息，有时会漏掉重要内容。</p><p><strong>解决方案</strong>：我细化了CLAUDE.md里的Wiki页面规范。比如要求每个页面必须有：</p><ul><li>Summary行：一句话描述核心内容</li><li>Tags行：用#标签标记主题</li><li>明确的页面类型（concept&#x2F;entity&#x2F;source&#x2F;comparison）</li></ul><hr><p>对于安全故障案例，还要求包含：漏洞类型、攻击向量、影响范围、修复方案、预防措施。这样LLM就有更清晰的指导，生成质量稳定了很多。</p><h3 id="问题2：交叉引用经常断裂"><a href="#问题2：交叉引用经常断裂" class="headerlink" title="问题2：交叉引用经常断裂"></a>问题2：交叉引用经常断裂</h3><p>Wiki的核心价值在于交叉引用，但LLM经常创建单向链接，或者链接到不存在的页面。</p><p><strong>解决方案</strong>：在CLAUDE.md里强化了交叉引用规则：</p><ol><li>必须双向链接：如果A页面引用了[[B]]，那么B页面也必须引用[[A]]</li><li>frontmatter里的related字段必须和正文里的[[wiki-link]]一致</li><li>只能引用已存在的页面，需要先创建再引用</li></ol><p>我还加了Lint检查，定期检查断裂链接和孤儿页面。</p><h3 id="问题3：处理非结构化素材效率低"><a href="#问题3：处理非结构化素材效率低" class="headerlink" title="问题3：处理非结构化素材效率低"></a>问题3：处理非结构化素材效率低</h3><p>有些原始素材质量很差，比如截图、手写笔记、甚至是Slack聊天记录的导出。LLM处理这些素材时效率很低。</p><p><strong>解决方案</strong>：对于这类素材，我会先手动整理成结构化的Markdown，再交给LLM处理。虽然增加了前期工作量，但保证了最终质量。</p><h2 id="一些经验教训"><a href="#一些经验教训" class="headerlink" title="一些经验教训"></a>一些经验教训</h2><ol><li><p><strong>不要追求一步到位</strong>。llm-wiki是个迭代过程。第一版Wiki肯定不完美，但可以通过持续迭代改进。</p></li><li><p><strong>CLAUDE.md要不断优化</strong>。随着使用，你会发现CLAUDE.md里缺失的规则。比如我后来加了”安全故障案例必须包含：漏洞类型、攻击向量、影响范围、修复方案、预防措施”这样的结构化要求。</p></li><li><p><strong>保持三层架构的纯净</strong>。raw&#x2F;就是raw&#x2F;，不要修改；wiki&#x2F;由LLM维护；output&#x2F;面向人类。混在一起会越来越乱。</p></li><li><p><strong>定期做健康检查</strong>。我会每周跑一次Lint，检查断裂链接、孤儿页面、单向链接等问题。发现问题及时修复。</p></li><li><p><strong>接受不完美</strong>。LLM生成的Wiki不会100%准确，会有遗漏和错误。但相比完全靠人工维护，已经好太多了。</p></li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>用llm-wiki管理安全开发规范，本质上是把”文档管理”变成了”知识工程”。原始文档是”原材料”，Wiki是”半成品”，规范文档是”成品”。LLM在这个过程中充当了”编译器”和”维护者”的角色。</p><p>这个方法最大的价值是<strong>可持续性</strong>。传统的文档管理，时间一长就会荒废，因为维护成本太高。而llm-wiki把维护成本转移给了LLM，人类只需要：</p><ol><li>提供原始素材（这个避免不了）</li><li>检查和修正LLM的工作（比从头写轻松多了）</li><li>使用最终产出（享受成果）</li></ol><p>如果你也在为安全开发规范的管理头疼，不妨试试这个思路。不用完全照搬我的实现，关键是理解”Wiki中间层”这个核心理念。至于具体用什么工具、怎么组织目录，都可以根据实际情况调整。</p><p>毕竟，工具是为人服务的，不是吗？</p><p>原文地址: <a href="https://lichuanyang.top/posts/88001/">https://lichuanyang.top/posts/88001/</a></p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-LLM-生成的代码或规范文档安全吗？会不会引入新的安全风险？"><a href="#Q-LLM-生成的代码或规范文档安全吗？会不会引入新的安全风险？" class="headerlink" title="Q: LLM 生成的代码或规范文档安全吗？会不会引入新的安全风险？"></a>Q: LLM 生成的代码或规范文档安全吗？会不会引入新的安全风险？</h3><p>llm-wiki 方案中，LLM 的角色是”编译器”而非”独立决策者”——它将原始的安全规范文档<strong>提取、结构化、交叉引用</strong>，最终生成的规范文档来源于团队已有的安全知识，并不是 LLM 凭空创造的。真正需要关注的风险点是：1) 原始素材本身是否正确（LLM 不会替你验证这一点）；2) LLM 可能会<strong>遗漏</strong>某些安全条款（因此需要人工审核）。LLM 本身不会主动引入安全漏洞，但<strong>过度信赖未经审核的 LLM 输出</strong>才是真正的风险。</p><h3 id="Q-用-LLM-管理规范后，如何审核和保证质量？"><a href="#Q-用-LLM-管理规范后，如何审核和保证质量？" class="headerlink" title="Q: 用 LLM 管理规范后，如何审核和保证质量？"></a>Q: 用 LLM 管理规范后，如何审核和保证质量？</h3><p>文中总结了三层质量保障机制：1) <strong>CLAUDE.md 规范约束</strong>：通过结构化的模板要求（如安全故障案例必须包含漏洞类型、攻击向量、影响范围、修复方案、预防措施），让 LLM 有章可循；2) <strong>多轮迭代</strong>：初版 Wiki 不会完美，作者用了 5-6 轮迭代让 LLM 修正交叉引用、补充遗漏概念；3) <strong>定期 Lint 检查</strong>：每周检查断裂链接、孤儿页面、单向链接等结构问题。最可靠的是<strong>人工最终校验</strong>——LLM 降低维护成本，但不替代人工把关。</p><h3 id="Q-llm-wiki-方案和传统-lint-工具（SonarQube、ESLint-等）有何不同？"><a href="#Q-llm-wiki-方案和传统-lint-工具（SonarQube、ESLint-等）有何不同？" class="headerlink" title="Q: llm-wiki 方案和传统 lint 工具（SonarQube、ESLint 等）有何不同？"></a>Q: llm-wiki 方案和传统 lint 工具（SonarQube、ESLint 等）有何不同？</h3><p>两者是<strong>互补关系</strong>，解决不同层次的问题。传统 lint 工具是<strong>规则引擎</strong>——检查代码是否违反预定义规则（如 SQL 注入、XSS），工作在代码层面，规则固定。llm-wiki 是<strong>知识管理工具</strong>——解决规范文档散落、新人学习成本高、故障经验无法沉淀复用的问题，工作在知识层面。理想状态下，llm-wiki 产出的规范可以作为 lint 规则的来源，而 lint 工具则自动化地检查代码是否遵守这些规范。</p><h3 id="Q-三层架构（raw-wiki-output）为什么重要？混在一起会怎样？"><a href="#Q-三层架构（raw-wiki-output）为什么重要？混在一起会怎样？" class="headerlink" title="Q: 三层架构（raw&#x2F;wiki&#x2F;output）为什么重要？混在一起会怎样？"></a>Q: 三层架构（raw&#x2F;wiki&#x2F;output）为什么重要？混在一起会怎样？</h3><h2 id="三层架构的核心是保持知识流向的单向性：raw-是原始素材（不可修改，保持原貌），wiki-是-LLM-维护的结构化中间层（可迭代优化），output-是面向人类的最终产出（直接可读）。混在一起的后果：1-原始素材和加工产物无法区分，出问题时难以溯源；2-修改-output-后-wiki-层过时，LLM-下次查询会给出矛盾信息；3-维护成本回归传统模式——每次更新都要同时改多个地方。作者强调”保持三层架构的纯净”是方案可持续的关键。"><a href="#三层架构的核心是保持知识流向的单向性：raw-是原始素材（不可修改，保持原貌），wiki-是-LLM-维护的结构化中间层（可迭代优化），output-是面向人类的最终产出（直接可读）。混在一起的后果：1-原始素材和加工产物无法区分，出问题时难以溯源；2-修改-output-后-wiki-层过时，LLM-下次查询会给出矛盾信息；3-维护成本回归传统模式——每次更新都要同时改多个地方。作者强调”保持三层架构的纯净”是方案可持续的关键。" class="headerlink" title="三层架构的核心是保持知识流向的单向性：raw 是原始素材（不可修改，保持原貌），wiki 是 LLM 维护的结构化中间层（可迭代优化），output 是面向人类的最终产出（直接可读）。混在一起的后果：1) 原始素材和加工产物无法区分，出问题时难以溯源；2) 修改 output 后 wiki 层过时，LLM 下次查询会给出矛盾信息；3) 维护成本回归传统模式——每次更新都要同时改多个地方。作者强调”保持三层架构的纯净”是方案可持续的关键。"></a>三层架构的核心是<strong>保持知识流向的单向性</strong>：raw 是原始素材（不可修改，保持原貌），wiki 是 LLM 维护的结构化中间层（可迭代优化），output 是面向人类的最终产出（直接可读）。混在一起的后果：1) 原始素材和加工产物无法区分，出问题时难以溯源；2) 修改 output 后 wiki 层过时，LLM 下次查询会给出矛盾信息；3) 维护成本回归传统模式——每次更新都要同时改多个地方。作者强调”保持三层架构的纯净”是方案可持续的关键。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/64/</id>
    <link href="https://lichuanyang.top/posts/64/"/>
    <published>2026-05-11T07:38:16.000Z</published>
    <summary>用 LLM 管理团队安全开发规范的实践，基于 Karpathy 的 llm-wiki 思路解决规范文档散落的问题。</summary>
    <title>用LLM管理安全开发规范：一次llm-wiki实践</title>
    <updated>2026-06-27T03:42:25.683Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <category term="vaadin" scheme="https://lichuanyang.top/tags/vaadin/"/>
    <category term="springboot" scheme="https://lichuanyang.top/tags/springboot/"/>
    <category term="前端开发" scheme="https://lichuanyang.top/tags/%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
    <content>
      <![CDATA[<h2 id="后端工程师的前端困境"><a href="#后端工程师的前端困境" class="headerlink" title="后端工程师的前端困境"></a>后端工程师的前端困境</h2><p>后端工程师开发前端的痛点，通常来说莫过于太过繁琐，经常要为了一些很小的事查半天。Vaadin很好的解决了这个痛点，为后端工程师提供易上手、方便使用的前端代码编写解决方案，今天我们就来了解一下。</p><span id="more"></span><h2 id="Vaadin-是什么"><a href="#Vaadin-是什么" class="headerlink" title="Vaadin 是什么"></a>Vaadin 是什么</h2><p>大家好，今天跟大家介绍一个对后端工程师特别有价值的工具——Vaadin。</p><p>说起来，上手前端基本的html, css开发，确实并不难，但是如果只会这些基本的东西，开发起来会很繁琐。如果想要使用前端生态中的各种轮子，虽说便利度提升了，但学习成本也会同步上升。所以，如果不是职业的全栈工程师，只是作为一个后端，想临时写点前端代码，比如自己想做点小项目，通常来说都会有个很痛苦的过程。</p><p>Vaadin很好的解决了这个痛点。通过vaadin包装好的常用前端组件，我们几乎可以零学习成本的编写出功能完备、不太难看的页面。对于后端背景的程序员来说，无疑会大幅度降低自己做些小项目的成本。</p><h2 id="核心概念"><a href="#核心概念" class="headerlink" title="核心概念"></a>核心概念</h2><p>Vaadin提供的功能，就是可以直接用java代码来写页面。Vaadin提供了多种输入框、表单等等封装好的前端样式，而且与springboot做了深度的融合，使用起来非常方便。</p><p>Vaadin的实际原理并不复杂，主要是基于服务端渲染，即在后端生成最终的html代码，交给浏览器。服务端渲染，这个并不罕见，与客户端渲染的优势和劣势，我们在这里不多讲。当然，对于vaadin来说，使用服务端渲染，似乎也没什么好说的，毕竟是写的后端代码，直接在后端做渲染，是个再正常不过的实现路径。Vaadin的引擎对前后端之间的交互做了封装，所以对使用者来说，前后端之间的交互是无感的，在页面层，我们也可以正常的调用后端service.</p><h2 id="快速上手"><a href="#快速上手" class="headerlink" title="快速上手"></a>快速上手</h2><p>下面是我写的一段代码示例，可以更直观的感受Vaadin的作用：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line">@Route(value = &quot;path&quot;, layout = MainView.class)</span><br><span class="line">@PageTitle(&quot;路径规划&quot;)</span><br><span class="line">public class PathView extends VerticalLayout &#123;</span><br><span class="line"></span><br><span class="line">    @Autowired</span><br><span class="line">    private PathService pathService;</span><br><span class="line"></span><br><span class="line">    public PathView() &#123;</span><br><span class="line">        TextField start = new TextField();</span><br><span class="line">        TextField end = new TextField();</span><br><span class="line">        HorizontalLayout path = new HorizontalLayout();</span><br><span class="line">        path.add(start, end);</span><br><span class="line">        Button pathCalculate = new Button(&quot;calculate path&quot;);</span><br><span class="line"></span><br><span class="line">        VerticalLayout result = new VerticalLayout();</span><br><span class="line">        TextField transferNum = new TextField();</span><br><span class="line">        TextField distance = new TextField();</span><br><span class="line">        Text stations = new Text(&quot;&quot;);</span><br><span class="line">        result.add(</span><br><span class="line">                new H3(&quot;换乘数: &quot;),</span><br><span class="line">                transferNum,</span><br><span class="line">                new H3(&quot;总距离 :&quot;),</span><br><span class="line">                distance,</span><br><span class="line">                new H3(&quot;途径站点详情:&quot;),</span><br><span class="line">                stations</span><br><span class="line">        );</span><br><span class="line"></span><br><span class="line">        pathCalculate.addClickListener(click -&gt; &#123;</span><br><span class="line">            PathInfoVO pathResult = pathService.getPath(start.getValue(), end.getValue());</span><br><span class="line">            System.out.println(pathResult);</span><br><span class="line">            stations.setText(StringUtils.join(pathResult.getDetail(), &quot;,&quot;));</span><br><span class="line">            transferNum.setValue(String.valueOf(pathResult.getTransferNum()));</span><br><span class="line">            distance.setValue(String.valueOf(pathResult.getDistance()));</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        add(</span><br><span class="line">                new Text(&quot;hello world&quot;),</span><br><span class="line">                path,</span><br><span class="line">                pathCalculate,</span><br><span class="line">                result</span><br><span class="line">        );</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码中，我们完全使用java代码对页面中的各个组件进行了编排，包括button的click函数，也是使用java开发者习惯的方式定义，并且能够直接调用其他后端service, 可以说是几乎零学习成本了。</p><p>详细代码和运行效果，可以到 <a href="https://github.com/lcy362/mobo-subway">项目地址</a>中查看。</p><p>如果你对Vaadin感兴趣，或者有任何问题或想法，欢迎在评论区交流。一起探索如何更好地利用Vaadin，提升我们的开发效率吧！</p><p>原文地址： <a href="https://lichuanyang.top/posts/43947/">https://lichuanyang.top/posts/43947/</a></p><hr><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><h3 id="Step-1-环境准备"><a href="#Step-1-环境准备" class="headerlink" title="Step 1: 环境准备"></a>Step 1: 环境准备</h3><p>确保已安装 JDK 和 Maven&#x2F;Gradle，推荐使用 Spring Boot 作为基础框架。在 IDE 中安装 Vaadin 插件以获得更好的开发体验。</p><h3 id="Step-2-创建第一个Vaadin项目"><a href="#Step-2-创建第一个Vaadin项目" class="headerlink" title="Step 2: 创建第一个Vaadin项目"></a>Step 2: 创建第一个Vaadin项目</h3><p>使用 Spring Initializr 或 Vaadin 官方脚手架创建项目，添加 Vaadin 依赖。项目启动后访问默认端口即可看到初始页面。</p><h3 id="Step-3-编写UI组件"><a href="#Step-3-编写UI组件" class="headerlink" title="Step 3: 编写UI组件"></a>Step 3: 编写UI组件</h3><p>使用 Vaadin 提供的组件类（如 <code>TextField</code>、<code>Button</code>、<code>VerticalLayout</code> 等）直接以 Java 代码构建页面布局。通过 <code>@Route</code> 注解定义页面路由，用 <code>@PageTitle</code> 设置页面标题。</p><h3 id="Step-4-数据绑定"><a href="#Step-4-数据绑定" class="headerlink" title="Step 4: 数据绑定"></a>Step 4: 数据绑定</h3><p>通过 <code>@Autowired</code> 注入业务 Service，在按钮的点击事件中调用后端逻辑，将返回结果设置到对应的 UI 组件中，实现前后端数据交互。</p><h3 id="Step-5-部署"><a href="#Step-5-部署" class="headerlink" title="Step 5: 部署"></a>Step 5: 部署</h3><p>使用 <code>mvn package</code> 或 <code>gradle build</code> 打包为可执行 JAR 文件，部署到服务器即可运行。</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/43947/</id>
    <link href="https://lichuanyang.top/posts/43947/"/>
    <published>2024-03-06T11:36:22.000Z</published>
    <summary>介绍 Vaadin 框架如何让后端工程师无需深入前端生态就能高效开发 Web 界面，解决后端开发前端的痛点。</summary>
    <title>Vaadin框架教程：Java工程师的前端开发秘籍</title>
    <updated>2026-06-27T03:43:25.703Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="hexo" scheme="https://lichuanyang.top/tags/hexo/"/>
    <category term="个人博客" scheme="https://lichuanyang.top/tags/%E4%B8%AA%E4%BA%BA%E5%8D%9A%E5%AE%A2/"/>
    <category term="i18n" scheme="https://lichuanyang.top/tags/i18n/"/>
    <category term="多语言" scheme="https://lichuanyang.top/tags/%E5%A4%9A%E8%AF%AD%E8%A8%80/"/>
    <content>
      <![CDATA[<h2 id="多语言博客的常见方案"><a href="#多语言博客的常见方案" class="headerlink" title="多语言博客的常见方案"></a>多语言博客的常见方案</h2><p>对于hexo的多语言方案，官方并没有提供很完备的支持，但是网络上有很多人尝试过不同的解决方案。今天，我们对这些方案简单做个总结，看看我们需要的多语言方案是什么样子的。</p><span id="more"></span><p>其实，hexo的多语言方案，说白了就是两个思路，一个是在文章维度切换语言，借助hexo原生的能力和hexo-generator-i18n等插件，用一套框架维护两个语言的内容；另一个则是独立维护不同语言的样式、内容，在站点维度切换语言，只是在域名层面合并到同一个域名。</p><p>具体要选择什么方案，取决于大家想要一个什么样的多语言网站。一开始我想象中的多语言网站的是第一种，即统一的首页，在首页和每篇文章上都支持切换语言，对每篇文章，可以通过切换语言，直达对应的其他语言译文上。</p><p>这个思路，借助于hexo-generator-i18n插件，可以实现，但要做比较多的定制开发。所以，我又重新思量了一下思路，我要的多语言网站应该是什么样。</p><h2 id="方案对比"><a href="#方案对比" class="headerlink" title="方案对比"></a>方案对比</h2><p>重新考虑之后，逐渐觉得第二种思路才是更合理的。我们要做一个多语言网站的目的是什么？对我来说，其实就是要获取一些英文流量。作为一个未备案网站，已经很久无法获取百度的搜索流量了，google在中国占的份额又太低，所以单凭中文内容，很难从搜索中获取较高流量。所以我希望通过提供一些英文内容，获取英文流量。这个目标下，完全不需要对所有的文章都维护多语言版本。何况，不同文章的语言受众也的确有很大区别，所以，语言的切换可以直接在站点层面进行。</p><p>具体操作方式上，一种是直接维护多个站点，两个站点内容、样式完全独立，只是部署在同一个域名下。比如中文站点根路径是lichuanyang.top，英文站点根路径是lichuanyang.top&#x2F;en . 两个站点上各做一个跳转链接，向对方跳转。</p><p>我采用的是另一种方式，相比上述方式，成本小一些，不需要实际维护两个站点。原理大致如下：</p><p>在本地，同样维护两个站点。但是英文站点不会去做实际的部署，只是用作生成静态网页的工具。英文版的内容生成之后，直接复制到中文网站的对应目录下，只对中文网站做部署即可。</p><h2 id="Hexo-双站实现细节"><a href="#Hexo-双站实现细节" class="headerlink" title="Hexo 双站实现细节"></a>Hexo 双站实现细节</h2><p>具体操作步骤如下：</p><ul><li><p>将博客目录整体复制一份，作为英文博客目录. 例如，我的博客根目录叫blog.source, 复制出一个blog.source.en. 这步完成后，如果博客源码也是以git维护的，可以直接在原目录的外层直接新建一个git项目，在外层做管理就行了。 示例如下：</p>  <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">blog(git根目录)/blog.source</span><br><span class="line">              /blog.source.en</span><br></pre></td></tr></table></figure></li><li><p>将en目录下的文章全部删除；站点描述等文本调整为英文内容；英文语言设置为en；英文站点根 (_config.yml中的root配置)设置成&#x2F;en</p></li><li><p>在两个站点下增加跳转链接。我是借助菜单功能，直接在菜单里加一个其他语言项。 next主题中配置如下：</p>  <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">中文站 ：    English: /en || fa fa-language</span><br><span class="line">英文站：     中文: https://lichuanyang.top || fa fa-language</span><br><span class="line"></span><br></pre></td></tr></table></figure></li><li><p>之后配置基本就完成了，可以在en目录下写英文文章，流程与之前写中文文章时完全一致</p></li></ul><h2 id="部署与-CI-CD"><a href="#部署与-CI-CD" class="headerlink" title="部署与 CI&#x2F;CD"></a>部署与 CI&#x2F;CD</h2><ul><li>最后就是生成和发布环节了，这一步要注意，每次生成时，我们需要先生成中文站点，再生成英文站点并将英文内容复制到中文目录下，避免生成中文内容时将英文内容覆盖掉。具体操作示例如下：</li></ul> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hexo clean &amp;&amp; hexo g &amp;&amp;cd ../blog.source.en &amp;&amp; hexo clean &amp;&amp; hexo g &amp;&amp; cd ../blog.source &amp;&amp;cp -r ../blog.source.en/public/. public/en/ &amp;&amp; hexo s</span><br></pre></td></tr></table></figure><p> 命令最后一段，使用hexo s就是本地启动，使用hexo d就是发布出去，和正常使用时一样。</p><p> 这样，我们就有了一个好用的多语言站点了。之后，写中文内容就在中文目录下进行，写英文内容就在英文目录下进行, 最后执行一下上边的命令就可以了。</p><h2 id="原文地址-https-lichuanyang-top-posts-40400"><a href="#原文地址-https-lichuanyang-top-posts-40400" class="headerlink" title="原文地址: https://lichuanyang.top/posts/40400/"></a>原文地址: <a href="https://lichuanyang.top/posts/40400/">https://lichuanyang.top/posts/40400/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/40400/</id>
    <link href="https://lichuanyang.top/posts/40400/"/>
    <published>2024-01-22T10:12:31.000Z</published>
    <summary>总结 Hexo 多语言方案的两种思路：文章维度切换语言和独立站点维护，对比各方案的优劣和最佳实践。</summary>
    <title>hexo多语言方案总结及最佳实践</title>
    <updated>2026-06-27T02:15:49.476Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="知乎" scheme="https://lichuanyang.top/tags/%E7%9F%A5%E4%B9%8E/"/>
    <category term="油猴" scheme="https://lichuanyang.top/tags/%E6%B2%B9%E7%8C%B4/"/>
    <category term="Tampermonkey" scheme="https://lichuanyang.top/tags/Tampermonkey/"/>
    <content>
      <![CDATA[<p>知乎作为一个高质量的文字社区，评论区的讨论往往非常有深度。然而，知乎默认的评论区时间格式是个让人头疼的小问题——它只显示”3 小时前””昨天””5 天前”这种相对时间。这在日常浏览中没问题，但如果你想追踪一场讨论的时间线、对比不同评论的先后顺序，这种模糊的时间展示就完全不够用了。</p><span id="more"></span><h2 id="痛点：相对时间的局限"><a href="#痛点：相对时间的局限" class="headerlink" title="痛点：相对时间的局限"></a>痛点：相对时间的局限</h2><p>相对时间有几个明显的缺陷：</p><ol><li><strong>信息丢失</strong>：你永远不知道”3 小时前”到底是几点几分，无法在脑海中形成精确的时间锚点</li><li><strong>跨天混乱</strong>：到了第二天，”昨天”就变成了”前天”，时间参考系一直在变</li><li><strong>讨论追踪困难</strong>：一场技术讨论中，A 回复 B 的某个论点，但 B 的评论和 A 的回复可能都显示”2 天前”，你根本看不出谁先谁后</li></ol><h2 id="脚本的来历"><a href="#脚本的来历" class="headerlink" title="脚本的来历"></a>脚本的来历</h2><p>之前在<a href="https://meta.appinn.net/t/topic/47711/18">小众软件</a>上看到一个关于知乎评论区时间展示混乱的吐槽帖，有位大佬当场写了一个油猴脚本来解决这个问题。</p><p>我用了好几个月，体验非常好。直到前段时间知乎做了一轮前端更新，脚本开始出现展示混乱的问题。于是我花了一个下午研究了一下知乎的新 DOM 结构，修复了脚本的逻辑，顺便把它发布到了 GreasyFork 上——这样后续知乎再有调整，可以直接从 GreasyFork 推送更新，不用手动安装新版本了。</p><h2 id="安装和使用"><a href="#安装和使用" class="headerlink" title="安装和使用"></a>安装和使用</h2><p>脚本已发布在 GreasyFork：</p><p><strong><a href="https://greasyfork.org/zh-CN/scripts/482871-%E7%9F%A5%E4%B9%8E%E5%A2%9E%E5%BC%BA-%E8%AF%84%E8%AE%BA%E6%97%B6%E9%97%B4%E7%B2%BE%E7%A1%AE%E5%88%B0%E7%A7%92">安装地址</a></strong></p><p>安装前提是你需要在浏览器中安装 Tampermonkey（油猴）扩展：</p><ul><li><strong>Chrome</strong>：<a href="https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo">Chrome Web Store</a></li><li><strong>Edge</strong>：Edge 扩展商店直接搜索 Tampermonkey</li><li><strong>Firefox</strong>：<a href="https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/">Firefox Add-ons</a></li></ul><p>安装 Tampermonkey 后，点击上面的 GreasyFork 链接，再点「安装此脚本」即可。脚本会自动在知乎评论区生效，无需额外配置。</p><h2 id="技术实现简述"><a href="#技术实现简述" class="headerlink" title="技术实现简述"></a>技术实现简述</h2><p>脚本的核心逻辑其实不复杂，关键步骤有三步：</p><p><strong>1. 找到时间元素</strong></p><p>知乎评论区的时间信息挂在一个 <code>span</code> 元素上，旧版使用的是 <code>title</code> 属性存储完整时间戳。知乎更新后改用了其他属性，所以需要适配新的 DOM 结构。</p><p><strong>2. 提取并格式化</strong></p><p>从 DOM 元素中读取原始的 ISO 时间字符串（如 <code>2024-01-15T14:32:08.000Z</code>），然后用 JavaScript 格式化为 <code>2024-01-15 14:32:08</code> 这样的可读格式，替换掉原来的相对时间文字。</p><p><strong>3. 处理动态加载</strong></p><p>知乎评论区是懒加载的——滚动到底部才会加载更多评论。因此脚本需要监听 DOM 变化（<code>MutationObserver</code>），当新的评论被插入页面时，自动对新评论的时间做同样的精确化处理。</p><h2 id="后续计划"><a href="#后续计划" class="headerlink" title="后续计划"></a>后续计划</h2><p>我打算把这个脚本的功能继续扩展，比如：</p><ul><li><strong>一键跳转到被引用的评论</strong>：知乎的引用格式是”回复 xxx”，但无法直接跳转</li><li><strong>高亮 OP（题主）的评论</strong>：在长讨论中快速定位题主的发言</li><li><strong>评论导出功能</strong>：把有深度的讨论导出为 Markdown</li></ul><p>大家如果有其他知乎体验上的痛点，欢迎留言提需求。如果对油猴脚本开发感兴趣，后续我也会考虑出一期油猴脚本的入门教程。</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/44912/</id>
    <link href="https://lichuanyang.top/posts/44912/"/>
    <published>2023-12-22T09:24:18.000Z</published>
    <summary>油猴脚本：将知乎评论区时间精确到秒，解决知乎更新后时间显示混乱的问题。</summary>
    <title>知乎增强工具-评论时间精确到秒</title>
    <updated>2026-06-27T00:26:22.560Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="云原生" scheme="https://lichuanyang.top/categories/%E4%BA%91%E5%8E%9F%E7%94%9F/"/>
    <category term="kubernetes" scheme="https://lichuanyang.top/tags/kubernetes/"/>
    <content>
      <![CDATA[<p>这篇教程不会关注 kubernetes 的部署、架构、实现方式等内部原理，而是完全从一个使用 kubernetes 的开发人员的视角去介绍kubernetes是什么，从而帮助了解如何更好的去利用 kubernetes 的特性。</p><span id="more"></span><h2 id="基本介绍"><a href="#基本介绍" class="headerlink" title="基本介绍"></a>基本介绍</h2><p>对于kubernetes是什么，一个比较官方的定义，Kubernetes是一个开源容器编排平台，管理大规模分布式容器化软件应用，简称为k8s.</p><p>通俗一些来讲，k8s 的核心理念是以应用为中心，向下屏蔽基础设施的差异，向上通过容器镜像实现应用的标准化，帮助开发人员在仅需关注应用本身，无需考虑部署、容灾、伸缩等运维细节的情况下，开发出大规模、可靠的分布式应用。</p><h2 id="优势"><a href="#优势" class="headerlink" title="优势"></a>优势</h2><p>如上所述，k8s的核心优势就是降低运维成本，让应用开发人员能够专注于应用本身。具体来讲，包括如下几点：</p><ul><li>便捷的部署流程和大规模的复制分发能力：应用提供标准化的容器镜像，之后的部署发布流程都可以做到自动化；应用也无需再做更多的事情就可以实现跨平台、跨区域的部署。</li><li>服务自愈能力：自动摘除不健康的节点并做恢复。</li><li>敏捷的自动伸缩：根据设定的系统负载，做快速的自动伸缩，实现系统处理能力和成本之间的平衡。</li></ul><h2 id="主要概念及实现方式介绍"><a href="#主要概念及实现方式介绍" class="headerlink" title="主要概念及实现方式介绍"></a>主要概念及实现方式介绍</h2><p>这篇kubernetes教程的剩余部分会从应用开发人员视角，介绍需要了解的k8s概念，主要包括以下几类：</p><ul><li>Pod, service, deployment等基本概念</li><li>spec+controller这套基本实现原理</li><li>k8s提供给应用的扩展点</li></ul><h2 id="spec-controller"><a href="#spec-controller" class="headerlink" title="spec+controller"></a>spec+controller</h2><p>这是k8s内部大部分逻辑的实现方式，了解这个逻辑，可以帮助开发人员更好的理解某些特定情况下k8s的行为。</p><p>如果翻看k8s的yaml配置，就会发现k8s的大部分组件中都有一个或若干个spec字段，它的含义很好理解，就是对于某个属性的期望值，比如一个服务节点数量的期望值。</p><p>controller的作用就是持续监测期望值和实际指标是否一致，不一致时就进行相应的操作进行处理，比如发现节点数不够了，就会启动对应数量的新节点。</p><p>这样，无论是上线、扩缩容、还是故障恢复，我们会发现这些逻辑都变得非常清晰，都是触发一下对容器数期望值或实际值的调整，然后让controller去增删节点就可以了。</p><p>这套机制是k8s里广泛使用的一个底层设计原则，作用是解开监控和操作逻辑之间的耦合。举个例子，我们在很多情况下都需要触发启动新的节点，包括要做扩容时、部分节点挂了时、要上线时，等等。这些完全没关联的场景如何去触发相同的操作，代码要如何保持整洁呢？k8s的做法就是用一个期望值的概念做中间层，将操作的触发时机和操作本身拆分成独立的逻辑。</p><p>了解了这个逻辑，我们可以更容易的理解k8s是怎么工作的。 比如基于HPA做自动扩容，就是根据当前的CPU使用情况，和期望的CPU使用量，决定是增加还是减少节点。</p><h2 id="Deployment"><a href="#Deployment" class="headerlink" title="Deployment"></a>Deployment</h2><p>Deployment 用于指示 k8s 如何创建和更新应用程序，我们通常理解的一个“服务”、“应用”大体上就对应在deployment这一层上。 像镜像、内存cpu使用限制、节点数、环境变量等各种配置，都是在deployment上。</p><h2 id="Pod"><a href="#Pod" class="headerlink" title="Pod"></a>Pod</h2><p>pod比较好理解，就是我们通常理解的一个节点。pod是 k8s 中最小可管理单元。</p><h2 id="Service"><a href="#Service" class="headerlink" title="Service"></a>Service</h2><p>Service是一个需要着重了解的概念，它是一个抽象层，它选择具备某些特征的 Pod（容器组）并为它们定义一个访问方式。 通常的应用场景下，deployment和service是一一对应的，但实际上deployment和service之间并没有必然的联系。</p><p>一个请求恰好可以打到某个deployment关联的所有pod上，看起来是一件非常正常的事情。而事实上，这件事情的发生不是由于这些pod通过一个deployment产生，而是因为deployment上配置了一个标签，通过这个deployment生成的pod都获得了这个标签; 同时，另外存在一个service, 设置通过这个标签选择pod, 请求进入的是通过service选择出的一批pod.</p><h2 id="探针-probe-和钩子-hook"><a href="#探针-probe-和钩子-hook" class="headerlink" title="探针(probe)和钩子(hook)"></a>探针(probe)和钩子(hook)</h2><p>k8s提供了一些用于应用扩展的接口，其中就包括probe和hook, 可以帮助应用更好的利用k8s的能力。</p><p>k8s提供了 liveness和 readiness 探针，顾名思义，liveness probe的作用是判断应用是否存活，readiness probe的作用是判断应用是否已经启动完毕。</p><p>应用可以用http接口等不同形式提供探针，k8s则负责在探测到不同状态时做不同的操作。比如说探测到liveness probe返回状态异常，则可以认为此节点已经丢失，对此节点做删除处理。</p><p>hook则支持应用在一些特定阶段插入一些行为，比如preStop hook, 运行应用关闭之前先做一些操作，可以用来做graceful shutdown.</p><h2 id="实践思考"><a href="#实践思考" class="headerlink" title="实践思考"></a>实践思考</h2><p>通过对本篇kubernetes教程上文列出的概念的了解，相信大家可以从应用开发人员的视角完全理解kubernetes是什么, 也可以大致想象出应用方使用k8s的基本思路： 构建标准镜像，暴露出必要的监控数据，剩下的事情都交给k8s来做。</p><p>在准确的数据下，k8s可以帮我们做好系统的自愈、扩缩容等操作，而我们要做的是，结合应用场景，尽量给出更加准确的监控数据。</p><p>举一些例子，比如说应用提供的liveness探针，如何能尽量确保准确的展示系统的健康状态，某个接口可以访问，某个端口可以调通，能否等价的说明这个应用是否健康。</p><p>比如说k8s以cpu、内存等指标来判断系统负载时，这些机器指标能否真实的展现出应用的真实负载，是否有可能连接池、IO等其他瓶颈会在cpu、内存等瓶颈到来之前就达到。</p><p>这些都是需要应用方结合应用的实际情况精心设计的。</p><p>此外，k8s的设计理念决定了在k8s环境下，”重启”会是一件非常稀松平常的事，像硬件故障、小概率的死循环、不健康的gc, 等不同类型的问题，都可以在k8s的自主重启下实现自愈； 自动的扩缩容扩充中，也会经常性的有节点启停。 所以说，应用需要让自己的启动流程顺畅一些，避免大量耗时或大量资源消耗。</p><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><ol><li><strong>安装 kubectl</strong>：从官方下载 <code>kubectl</code> 二进制文件，配置 <code>~/.kube/config</code> 连接到 K8s 集群，运行 <code>kubectl get nodes</code> 验证连接。</li><li><strong>创建第一个 Deployment</strong>：编写 Deployment YAML，指定容器镜像、端口（<code>containerPort</code>）、CPU&#x2F;内存限制，运行 <code>kubectl apply -f deployment.yaml</code>。</li><li><strong>暴露 Service</strong>：编写 Service YAML，通过 <code>selector</code> 标签关联 Deployment 的 Pod，指定 <code>type: LoadBalancer</code> 或 <code>NodePort</code>，运行 <code>kubectl apply -f service.yaml</code>。</li><li><strong>水平伸缩</strong>：运行 <code>kubectl autoscale deployment &lt;name&gt; --cpu-percent=80 --min=2 --max=10</code> 创建 HPA，通过压测观察 Pod 自动扩缩容。</li><li><strong>配置探针与钩子</strong>：在 Deployment 中添加 <code>livenessProbe</code>、<code>readinessProbe</code> 和 <code>lifecycle.preStop</code> 配置，确保应用在 K8s 环境下具备自愈和优雅关闭能力。</li></ol>]]>
    </content>
    <id>https://lichuanyang.top/posts/55227/</id>
    <link href="https://lichuanyang.top/posts/55227/"/>
    <published>2023-01-28T11:06:17.000Z</published>
    <summary>从开发人员视角介绍 Kubernetes 是什么，不关注部署和架构，专注于如何利用 K8s 特性提升开发效率。</summary>
    <title>kubernetes是什么-实用向教程</title>
    <updated>2026-06-27T02:31:54.492Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="知乎" scheme="https://lichuanyang.top/tags/%E7%9F%A5%E4%B9%8E/"/>
    <category term="油猴" scheme="https://lichuanyang.top/tags/%E6%B2%B9%E7%8C%B4/"/>
    <category term="Tampermonkey" scheme="https://lichuanyang.top/tags/Tampermonkey/"/>
    <content>
      <![CDATA[<p>知乎是个什么样的网站，大家都比较了解。今天我们换个角度——分享一个让”上班用知乎”这件事变得更安全、更高效的小工具。</p><span id="more"></span><h2 id="摸鱼经济学：为什么是知乎？"><a href="#摸鱼经济学：为什么是知乎？" class="headerlink" title="摸鱼经济学：为什么是知乎？"></a>摸鱼经济学：为什么是知乎？</h2><p>之前我翻了一下自己知乎账号的后台数据，发现一个很有意思的现象：<strong>PC 端的流量居然占了 40% 以上</strong>。在移动互联网时代，这个比例高得不太正常。</p><p>后来从一些其他渠道也了解到，知乎周末和节假日的流量明显不如工作日。这说明什么呢？</p><p><strong>上班刷知乎的你，不是一个人。</strong></p><p>对相当一部分人来说，知乎主要的使用场景就是——上班摸鱼。而且知乎这个网站确实天然适合摸鱼：</p><ul><li><strong>以文字内容为主</strong>，不像短视频那样有声音、耗流量</li><li><strong>有大量专业性内容</strong>，被老板路过看到屏幕时，可以说”我在查技术资料”</li><li><strong>文章篇幅适中</strong>，一条时间线看几分钟，心理负担小</li></ul><h2 id="知乎摸鱼的三个痛点"><a href="#知乎摸鱼的三个痛点" class="headerlink" title="知乎摸鱼的三个痛点"></a>知乎摸鱼的三个痛点</h2><p>但知乎 PC 端有些设计，对摸鱼党不太友好：</p><p><strong>1. 图片太大</strong></p><p>知乎文章的配图经常会占满大半个屏幕。不管文章本身多正经，一张巨大的图片出现在屏幕上，旁边同事一瞥就能看到——这时候你说”我在看技术文章”就显得很没说服力。</p><p><strong>2. 标题浮窗抢眼球</strong></p><p>浏览知乎问题时，页面上方会有一个非常显眼的标题浮窗。它不仅占了一行空间，而且白底黑字特别突出，路人一眼就知道你在看知乎。</p><p><strong>3. Logo 高调</strong></p><p>知乎的 logo 统一放在页面左上角，任何时候截图或别人瞄一眼屏幕，品牌辨识度拉满。</p><h2 id="脚本解决方案"><a href="#脚本解决方案" class="headerlink" title="脚本解决方案"></a>脚本解决方案</h2><p>我写了一个油猴脚本，针对性解决了这三个问题：</p><ul><li><strong>限制图片尺寸</strong>：最大宽度设为 300px，所有文章配图变成小图，不抢眼</li><li><strong>隐藏标题浮窗</strong>：问题页面的标题浮窗直接移除</li><li><strong>去掉知乎 Logo</strong>：页面左上角的知乎 logo 一并隐藏</li></ul><p>这样一来，知乎页面看起来就像一个普通的文字阅读器——干净、低调，非常适合在办公室环境中使用。</p><h2 id="安装和使用"><a href="#安装和使用" class="headerlink" title="安装和使用"></a>安装和使用</h2><p>脚本已发布在 GreasyFork：</p><p><strong><a href="https://greasyfork.org/zh-CN/scripts/483047-%E6%91%B8%E9%B1%BC%E5%8A%A9%E6%89%8B-%E7%9F%A5%E4%B9%8E">安装地址</a></strong></p><p>安装前提是浏览器中已安装 Tampermonkey（油猴）扩展。安装脚本后自动生效，无需配置。</p><h2 id="技术实现"><a href="#技术实现" class="headerlink" title="技术实现"></a>技术实现</h2><p>脚本的实现其实非常简单，核心就是通过 <code>GM_addStyle</code> 注入自定义 CSS：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 限制图片最大宽度</span></span><br><span class="line"><span class="title function_">GM_addStyle</span>(<span class="string">&#x27;.RichContent img &#123; max-width: 300px !important; &#125;&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 隐藏标题浮窗</span></span><br><span class="line"><span class="title function_">GM_addStyle</span>(<span class="string">&#x27;.QuestionHeader &#123; display: none !important; &#125;&#x27;</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 去除 Logo</span></span><br><span class="line"><span class="title function_">GM_addStyle</span>(<span class="string">&#x27;.AppHeader-logo &#123; display: none !important; &#125;&#x27;</span>);</span><br></pre></td></tr></table></figure><p>本质上就是几行 CSS——但效果出奇的好。油猴脚本的妙处也正在于此：很多时候你不用写复杂的逻辑，只靠注入 CSS 就能显著改变网页的视觉体验。</p><h2 id="和”知乎增强工具”的区别"><a href="#和”知乎增强工具”的区别" class="headerlink" title="和”知乎增强工具”的区别"></a>和”知乎增强工具”的区别</h2><p>我之前还写过一个<a href="/posts/44912/">知乎增强工具</a>，那个脚本侧重于<strong>信息增强</strong>——把评论区时间精确到秒。而这个脚本侧重于<strong>视觉优化</strong>——让知乎在办公场景下看起来更像一个正经的阅读工具。</p><p>两个脚本可以同时安装，互不冲突。</p><p>大家如果对知乎使用有什么其他痛点，欢迎留言提。也欢迎去看看我另一个<a href="/posts/44912/">知乎增强工具</a>，评论区时间精确到秒，很适合追踪技术讨论的时间线。</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/23393/</id>
    <link href="https://lichuanyang.top/posts/23393/"/>
    <published>2022-12-28T10:29:01.000Z</published>
    <summary>分享一个优化知乎 PC 端使用体验的油猴脚本，让上班摸鱼更高效。</summary>
    <title>怎么更科学的用知乎摸鱼</title>
    <updated>2026-06-27T00:26:38.323Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="个人成长" scheme="https://lichuanyang.top/tags/%E4%B8%AA%E4%BA%BA%E6%88%90%E9%95%BF/"/>
    <category term="读书感悟" scheme="https://lichuanyang.top/tags/%E8%AF%BB%E4%B9%A6%E6%84%9F%E6%82%9F/"/>
    <category term="系统之美" scheme="https://lichuanyang.top/tags/%E7%B3%BB%E7%BB%9F%E4%B9%8B%E7%BE%8E/"/>
    <category term="逻辑思维" scheme="https://lichuanyang.top/tags/%E9%80%BB%E8%BE%91%E6%80%9D%E7%BB%B4/"/>
    <content>
      <![CDATA[<p>这本书的作用主要是教大家面对现实中的复杂问题，如何更有效的去思考。书本身偏重系统的基本概念，这些概念其实都比较好理解。另外，作者举了非常多的例子。初读这本书时，会感觉理论和实际例子有那么些割裂。 但是后来逐渐感觉到，这个话题确实是没那么容易讲的通俗易懂的，需要读者自己付出更多的思考，才能有实质性的收获。而作者举的这些例子，就是非常好的引导读者去思考的引子。通过跟着作者一起思考这些例子，我们就可以帮助自己更好的理解书中的概念和逻辑。</p><span id="more"></span><p>所以，这篇笔记中，我主要其实就是比较机械化的把书中的一些核心概念提炼出来，同时会尝试举一些身边的例子。 而若想更好的理解书中每一部分，还是推荐去读一遍书，并且跟着作者的思路一起去好好思考那些例子的逻辑。</p><h2 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h2><p>系统是一组相互连接的事物，在一定时间内，以特定的行为模式相互影响。</p><p>任何一个系统都包括三种构成要件：要素、连接、功能或目标。</p><p>存量是所有系统的基础，比如浴缸中的水、人口数量、书店中的书、树木的体积、银行里的钱，等等。但是，存量不一定非得是物质的，你的自信、在朋友圈中的良好口碑，或者对世界的美好希冀等，都可以是存量。</p><p>存量会随着时间的变化而不断改变，使其发生变化的就是“流量”</p><p>绘制存量-流量图，是理解系统的基础方式。</p><p>当某一个存量的变化影响到与其相关的流入量或流出量时，反馈回路就形成了。</p><p>调节回路与增强回路是两种很常见的反馈回路，比较好理解，就是字面意思。调节回路趋向于将系统存量维持稳定，增强回路则是会不断放大、增强原有的发展态势。</p><h2 id="常见的系统"><a href="#常见的系统" class="headerlink" title="常见的系统"></a>常见的系统</h2><p>接下来通过一些常见的系统模型，来帮助大家更好的理解系统是什么。</p><h2 id="单存量系统"><a href="#单存量系统" class="headerlink" title="单存量系统"></a>单存量系统</h2><ul><li><p>系统1.1:  一个存量、两个相互制衡的调节回路的系统， 比如温度调节器，通过一个升温调节回路、一个降温调节回路，控制存量（温度）稳定。</p></li><li><p>系统1.2：一个存量、一个增强回路以及一个调节回路的系统, 比如人口，新出生是增强回路，死亡是调节回路。</p></li><li><p>系统1.3：含有时间延迟的系统，比如库存，相比简单的温度调节器，主要区别在于调节回路上会存在延时。</p></li></ul><h2 id="双存量系统"><a href="#双存量系统" class="headerlink" title="双存量系统"></a>双存量系统</h2><ul><li><p>系统2.1：一个可再生性存量受到另外一个不可再生性存量约束的系统， 比如开采石油等不可再生资源，就会存在资本和资源两个存量，其中存在一个增强回路，即开采资源过程中会获得利润，获得的利润能够增加资本，从而扩大产能，加大开采量。同时，由于石油不可再生，开采难度会越来越大，也就形成了一个调节回路。</p></li><li><p>系统2.2：有两个可再生性存量的系统，比如渔业，和石油比较类似，只是鱼是可再生资源，会导致系统的发展形态会和上一类系统有所区别。</p></li></ul><h2 id="系统的三大特征"><a href="#系统的三大特征" class="headerlink" title="系统的三大特征"></a>系统的三大特征</h2><ul><li><p>适应力。 系统之所以会有适应力，是因为系统内部结构存在很多相互影响的反馈回路，正是这些回路相互支撑，即使在系统遭受巨大的扰动时，仍然能够以多种不同的方式使系统恢复至原有状态。比如人体就是一个适应力非常强的系统。</p></li><li><p>自组织。 系统所具备的使其自身结构更为复杂化的能力，被称为“自组织”。自组织对于系统来说，意味着系统可以自己去“进化”，从而完成更大的目标，或实现更复杂的功能。像我之前的老板，经常跟我说我们的目标是去建立一个具有“自组织”能力的团队。只有这样，团队才能跟着业务发展一起进步。</p></li><li><p>层次性。 在新结构不断产生、复杂性逐渐增加的过程中，自组织系统经常生成一定的层级或层次性。一个大的系统中包含很多子系统，一些子系统又可以分解成更多、更小的子系统。如果各个子系统基本上能够维系自身，发挥一定的功能，并服务于一个更大系统的需求，而更大的系统负责调节并强化各个子系统的运作，那么就可以产生并保持相对稳定的、有适应力和效率的结构。</p></li></ul><h2 id="系统的障碍"><a href="#系统的障碍" class="headerlink" title="系统的障碍"></a>系统的障碍</h2><p>我们认为自己所知道的关于这个世界的任何东西都只是一个模型。我们的模型通常是与现实世界高度一致的。这就是我们为什么会成为这个星球上最为成功的一个物种的原因。 我们的模型仍远远达不到能完整地描绘世界的程度。这就是我们为什么经常会犯错误、会感到出乎意料的原因。</p><p>所以，我们要看看从系统的角度可以发现哪些问题，帮助我们尽量的少犯错。作者主要整理了以下6点：</p><ul><li>系统结构是行为的根源，而系统行为体现为随时间而发生的一系列事件；而人们容易被表象所迷惑</li><li>在非线性的世界里，不要用线性的思维模式</li><li>需要恰当地划定系统边界</li><li>要能看清各种限制因素</li><li>系统中无所不在的时间延迟</li><li>人们只有有限理性, 导致决策往往并非整体最优</li></ul><h2 id="系统的陷阱与对策"><a href="#系统的陷阱与对策" class="headerlink" title="系统的陷阱与对策"></a>系统的陷阱与对策</h2><p>这一章则是一些更具体的问题，以及针对这些问题有哪些可能的对策。</p><h2 id="政策阻力；治标不治本"><a href="#政策阻力；治标不治本" class="headerlink" title="政策阻力；治标不治本"></a>政策阻力；治标不治本</h2><p>有一些长期持续的行为模式，可能并不符合人们的预期，往往被视为一个问题。尽管人们发明了各种技术、采取了多项政策措施，试图去“修复”它们，但系统好像很顽固，每年都产生相同的行为。这是一种常见的系统陷阱，人们习惯称之为“治标不治本”或“政策阻力”， 比如毒品泛滥、失业等。</p><p>“政策阻力”来自于系统中各个参与者的有限理性，每一个参与者都有自己的目标。当各个子系统的目标不同或不一致时，就会产生变革的阻力。应对“政策阻力”最有效的方式是，设法将各个子系统的目标协调一致，通常是设立一个更大的总体目标，让所有参与者突破各自的有限理性。</p><h2 id="公地悲剧"><a href="#公地悲剧" class="headerlink" title="公地悲剧"></a>公地悲剧</h2><p>对于人们共同分享的、有限的资源，很容易出现开发（或消耗）逐步升级或增长的态势。“公地悲剧”之所以产生，一个重要原因是资源的消耗与资源的使用者数量增长之间的反馈缺失了，或者时间延迟太长。</p><p>防止“公地悲剧”有以下三种方式：一是教育，帮助人们更清晰的看到后果；二是将资源私有化；三是采取一些配额制之类的管制措施。</p><h2 id="目标侵蚀"><a href="#目标侵蚀" class="headerlink" title="目标侵蚀"></a>目标侵蚀</h2><p>绩效标准受过去绩效的影响，尤其是当人们对过去的绩效评价偏负面，也就是过于关注坏消息时，将启动一个恶性循环，使得目标和系统的绩效水平不断下滑。通俗的说法，就是温水煮青蛙，形势在不知不觉中慢慢变差。</p><p>对策是保持一个绝对的绩效标准。更好的状况是，将绩效标准设定为过去的最佳水平，从而不断提高自己的目标，并以此激励自己。</p><h2 id="竞争升级"><a href="#竞争升级" class="headerlink" title="竞争升级"></a>竞争升级</h2><p>“以眼还眼，以牙还牙”。每一个参与者期望的系统状态都是相对于其他参与者而言的，并试图超越对方，领先一步，连并驾齐驱都不行；而且每个参与者都有高估对方的敌意、夸大对方实力的倾向。竞争升级的系统结构是一个增强回路，它是以指数级方式发展起来的，一旦超过某个限度，其使竞争激化的速度会超出绝大多数人的想象。</p><p>对策一种依托于一方主动让步；更优雅的方式则是期望双方达成协定。</p><h2 id="富者愈富"><a href="#富者愈富" class="headerlink" title="富者愈富"></a>富者愈富</h2><p>利用积累起来的财富、权力、特殊渠道或内部信息，可以创造出更多的财富、权力、渠道以及信息。这些都是另外一个被称为“富者愈富”的基模的例子。</p><p>对策：多元化，即允许在竞争中落败的一方可以退出，开启另外一场新的博弈；反垄断法，即严格限制赢家所占有的最大份额比例；修正竞赛规则，限制最强的一些参与者的优势，或对处于劣势的参与者给予一些特别关照，增强他们的竞争力（例如施舍、馈赠、税赋调节、转移支付等）；对获胜者给予多样化的奖励，避免他们在下一轮竞争中争夺同一有限的资源，或产生偏差。</p><h2 id="转嫁负担"><a href="#转嫁负担" class="headerlink" title="转嫁负担"></a>转嫁负担</h2><p>当面对一个系统性问题时，如果采用的解决方案根本无助于解决潜在的根本问题，只是缓解（或掩饰）了问题的症状时，就会产生转嫁负担、依赖性和上瘾的状况。</p><p>应对这一陷阱最好的办法是提前预防，防止跌入陷阱。一定要意识到只是缓解症状或掩饰信号的政策或做法，都不能真正地解决问题。</p><h2 id="规避规则"><a href="#规避规则" class="headerlink" title="规避规则"></a>规避规则</h2><p>任何规则都可能会有漏洞或例外情况，因而也存在规避规则的机会。也就是说，虽然一些行为在表面上遵守或未违背规则，但实质上却不符合规则的本意，甚至扭曲了系统。</p><p>对策是设计或重新设计规则，从规避规则的行为中获得创造性反馈，使其发挥积极的作用，实现规则的本来目的。</p><h2 id="目标错位"><a href="#目标错位" class="headerlink" title="目标错位"></a>目标错位</h2><p>系统行为对于反馈回路的目标特别敏感。如果目标定义不准确或不完整，即使系统忠实地执行了所有运作规则，其产出的结果却不一定是人们真正想要的。</p><p>对策就是恰当地设定目标及指标，以反映系统真正的福利。</p><h2 id="系统的变革方式，能够干预系统的杠杆点"><a href="#系统的变革方式，能够干预系统的杠杆点" class="headerlink" title="系统的变革方式，能够干预系统的杠杆点"></a>系统的变革方式，能够干预系统的杠杆点</h2><p>这一部分则是一些我们能够去干预系统的方式，按照有效性由低到高去排列。这一章里的很多理论，相信大家或多或少在其他的地方也见到过一些。很多优秀的人，即便没读过这本书，很多时候其实也是这么思考的。而系统之美这本书，能够帮助我们更加体系化的了解为什么要这么做。</p><p>12：数字，通过各种流量的数值来调节系统。像王者荣耀处理平衡性的策划，基本上很大一部分精力就在做这事，哪个英雄机制过强了，就狠狠的削一把数值，哪个英雄不大行，就加数值，让他站撸也有很高的强度。这种方式实际上是效力很低的一种方式，它无法改变系统基本的结构。但是大多数人90%的注意力都会集中在参数上。</p><p>11：缓冲器。通过提高缓冲器的容量，我们通常可以使系统稳定下来。但是，如果缓冲器过大，系统也将变得缺乏弹性，它对于变化的反应速度将过于缓慢。同时，要建立、扩大或维护某些缓冲器的容量，也需花费巨大的时间和资金，例如建设水库或仓库等。为此，一些企业发明了“零库存”的“及时生产”模式。在这些企业看来，与耗费巨资维持固定的库存相比，偶尔的波动或缺货造成的损失并不是很大。</p><p>10.存量—流量结构：实体系统及其交叉节点。这一点主要关乎于系统的整体设计。恰当的杠杆点，需要从一开始就被设计好。一旦实体的结构建立起来了，要想找到杠杆点，就需要理解系统的限制和瓶颈，在尽可能发挥它们的最大效率的同时，避免出现较大的波动或扩张，超出其承受能力。而如果系统已经运行起来，想再去调整其中的关键节点，就会变的很困难。软件工程中著名的“防腐层”概念，可以一定程度上优化这个问题，即通过一些预设的中间层，减少调整关键节点的成本。</p><p>9.时间延迟：系统对变化做出反应的速度。时间延迟是一个高杠杆点，但事实上，时间延迟通常不是很容易改变的。很多事物的发展有其内在规律，该花多长时间就得花多少时间。你不可能一夜之间积累起一大笔资本，孩子也不可能在一夜之间长大，拔苗助长也无法加快庄稼的生长。但如果有办法改变时间延迟，往往可以取得显著效果。比如近期疫情防控中，为什么要经常进行大规模的核酸检测，就是通过这种方式降低病毒从被传播到被发现这一段的时间延迟。</p><p>8.调节回路：试图修正外界影响的反馈力量。这一点很好理解，我们可以通过加调节回路的方式，加强对系统的控制。kubernetes里的controller可以认为就是调节回路的实现，通过不同的controller, 就可以将不同组件的值，也就是存量，控制在期望值上。</p><p>7.增强回路：驱动收益增长的反馈力量。和调节回路类似，只是目标不同。</p><p>6.信息流：谁能获得信息的结构。相当于一个新的回路，让人们在之前得不到信息的地方可以获得反馈。比如说资本主义罪恶的加班排名，就是将同事的加班信息告诉你，让你自然产生压力，从而促进加班。</p><p>5.系统规则：激励、惩罚和限制条件。这一部分，实际上就是将系统的范围、边界和自由度定义清楚。</p><p>4.自组织：系统结构增加、变化或进化的力量。接文章开头的话题，要建设一个自组织的团队，是要做很多事情的，比如提升团队中每个成员的能力，让大家都能有决策能力；加强团队内信息的透明度；等等。而这样的团队一旦能建成，不光产出会有质的提升，团队中成员的工作成就感和幸福度也会大幅度提升。</p><p>3.目标：系统的目的或功能。系统中的某个参与者可以清晰地设定、阐述、重复、支持并坚持新的目标，从而引导了系统的变革。这就是为什么这些年OKR这么流行的原因。通过明确合理的OKR, 指导大家更合理的决策和行动。</p><p>2.社会范式：决定系统之所以为系统的心智模式。一些社会公认的观念，一些潜在的基本假设以及关于社会现实本质的普遍看法，构成了社会的范式（paradigm），或者是一整套世界观，它们是人们普遍相信的、关于世界是如何运作的一系列基本假设、规则或信念。这些信念都是隐含的，因为在一个社会中，几乎每一个人都已经知道它们，因而无须特别申明。这些范式自然会对系统运行产生极其重大的影响，当然，要改变这些范式，也会比改变其他东西更困难。</p><p>1.超越范式。与改变范式相比，在更高的层次上，还有另外一个杠杆点，那就是使自己摆脱任何范式的控制。这一点其实有那么一点玄学的意思了，我们在这里就不多展开。核心点其实就是我们要有意识的去跳出当前系统。</p><h2 id="系统的生存法则，与系统共舞"><a href="#系统的生存法则，与系统共舞" class="headerlink" title="系统的生存法则，与系统共舞"></a>系统的生存法则，与系统共舞</h2><p>在工业社会长大的人，若热衷于系统思考，很可能会犯一个严重的错误。他们可能会假定，通过系统分析，可以认清系统中的相互联系以及复杂纠葛，借助计算机的威力，最后找到预测和控制系统的钥匙。不幸的是，这是错误的观念，其根源在于工业时代根深蒂固的心智模式，即相信存在一把预测和控制的钥匙。但实际上，要做到这一点是完全不现实的，我们需要认识到并愿意放弃控制的错觉，需要换一种截然不同的方式。这样，我们仍然可以有很大的作为空间。这种方式就是与系统共舞。我们无法控制系统，但是可以在系统中生存的更好。</p><p>接下来就是和系统共舞的一些法则，这一部分相当于一些小tips, 也都比较好理解，我们就简单列举一下：</p><ul><li>跟上系统的节拍：要观察系统是如何运作的，才能跟上它。我们需要多关注事实和数据，才不至于被别人的理论带着走。</li><li>把你的心智模式展现在阳光下，画出系统结构图，强迫自己把自己内心隐藏的各种假设投射出来，并精准地表述它们。</li><li>相信、尊重并分享信息</li><li>谨慎地使用语言，并用系统的概念去丰富语言</li><li>关注重要的，而不只是容易衡量的</li><li>为反馈系统制定带有反馈功能的政策。对于动态的、自我调节的反馈系统，不能用静止的、刚性的政策来进行管制，好的政策必须能够根据系统状态的变化及时地灵活调整。</li><li>追求整体利益</li><li>聆听系统的智慧</li><li>界定系统的职责</li><li>保持谦逊，做一名学习者</li><li>庆祝复杂性</li><li>扩展时间的范围</li><li>打破各种清规戒律</li><li>扩大关切的范围</li><li>不要降低“善”的标准</li></ul><h2 id="原文地址-https-lichuanyang-top-posts-53791"><a href="#原文地址-https-lichuanyang-top-posts-53791" class="headerlink" title="原文地址: https://lichuanyang.top/posts/53791/"></a>原文地址: <a href="https://lichuanyang.top/posts/53791/">https://lichuanyang.top/posts/53791/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/53791/</id>
    <link href="https://lichuanyang.top/posts/53791/"/>
    <published>2022-05-16T10:34:01.000Z</published>
    <summary>《系统之美》读书笔记，学习面对复杂问题的系统性思考方法，建立正确的思维框架。</summary>
    <title>读书笔记《系统之美》,如何面对现实中的复杂问题</title>
    <updated>2026-06-27T02:15:49.476Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="架构设计" scheme="https://lichuanyang.top/categories/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="分布式系统" scheme="https://lichuanyang.top/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    <category term="分布式系统设计" scheme="https://lichuanyang.top/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/"/>
    <content>
      <![CDATA[<p>之前翻译过一篇关于分布式系统的文章 <a href="https://lichuanyang.top/posts/3914/">https://lichuanyang.top/posts/3914/</a> ，在各个平台都取得了不错的反响。因此，最近又重新整理了一下相关的知识，结合一些这一年多里新的理解，重新整理了下这篇文章。</p><span id="more"></span><p>首先我们需要明确本文要讨论的分布式系统是什么，简单的说，就是满足多节点和有状态这两个条件即可。多节点很好理解，有状态则是指这个系统要维护一些数据，不然的话，其实我们无脑的水平扩容就没有任何问题，也就不存在分布式系统的问题了。</p><p>常见的分布式系统， 无论是mysql, cassandra, hbase这些数据库，还是rocketmq, kafka, pulsar这样的消息队列，还是zookeeper之类的基础设施，其实都满足这两个条件。</p><p>这些分布式系统的实现通常来说主要需要关注两个方面：一是自己本身功能的实现，二是在分布式环境下保持良好的性能与稳定性；即便是两个功能完全不一样的系统，其对第二类问题的处理方式也会有很多相似之处。本文的关注重点也即在对第二类问题的处理上。</p><h2 id="分布式系统的核心挑战"><a href="#分布式系统的核心挑战" class="headerlink" title="分布式系统的核心挑战"></a>分布式系统的核心挑战</h2><p>接下来，我们列举一下分布式系统都有哪些常见目标，包括而不限于：</p><ul><li>大量普通的服务器通过网络互联，对外作为整体提供服务；</li><li>随着集群规模增长，系统整体性能表现为线性增长；</li><li>能够自动容错，故障节点自动迁移，不同节点的数据要能保持一致性；</li></ul><p>要达成这些目标，又有哪些挑战呢？大概有以下这些：</p><ol><li><p>进程崩溃： 原因很多，包括硬件故障、软件故障、正常的例行维护等等，在云环境下会有一些更加复杂的原因；<br>进程崩溃导致的最大问题就是会丢数。出于性能的考虑，很多情况下我们不会进行同步的写磁盘，而是会将数据暂时放在内存的缓冲区，再定期刷入磁盘。而在进程崩溃的时候，内存缓冲区中的数据显然会丢失。</p></li><li><p>网络延迟和中断： 节点的通信变到很慢时，一个节点如何确认另一个节点是否正常； </p></li><li><p>网络分区： 集群中节点分裂成两个子集，子集内通信正常，子集之间断开（脑裂），这时候集群要如何提供服务。</p></li></ol><p> 这里插一个彩蛋，在CAP理论的前提下，现实中的系统通常只有两种模式：放弃高可用的CP模式和放弃强一致性的AP模式。为什么没有一种放弃分区容忍性的CA模式？就是因为我们无法假设网络通信一定正常，而一旦接受了集群变成两个分区，再想合并回来就不现实了。</p><ol start="4"><li><p>进程暂停：比如full gc之类的原因导致进程出现短暂的不可用后又迅速恢复，不可用期间集群有可能已经做出了相关的反应，当这个节点再恢复的时候如何维持状态的一致性。</p></li><li><p>时钟不同步和消息乱序：集群内不同节点的操作，我们希望它的顺序是明确的；不同节点之间的时钟不同步，会导致我们无法利用时间戳确保这件事。而消息的乱序就给分布式系统的处理带来了更大的难度。</p></li></ol><p>下面，我们就依次介绍，针对这些问题，都有什么处理方式。</p><h2 id="分区与复制"><a href="#分区与复制" class="headerlink" title="分区与复制"></a>分区与复制</h2><p>对于进程崩溃的问题，首先要明确的是，单纯实现进程崩溃下不丢数，没有任何难度，重要的是怎么在保证系统性能的前提下达到这个目标。</p><p>首先要介绍的就是write-ahead log这种模式，服务器将每个状态更改作为命令存储在硬盘上的仅附加(append-only)文件中。 append操作由于是顺序的磁盘写，通常是非常快的，因此可以在不影响性能的情况下完成。  在服务器故障恢复时，可以重播日志以再次建立内存状态。</p><p>其关键思路是先以一个小成本的方式写入一份持久化数据，不一定局限于顺序写磁盘，此时就可以向client端确认数据已经写入，不用阻塞client端的其他行为。server端再异步的去进行接下来高消耗的操作。</p><p>典型场景及变体：mysql redo log;  redis aof; kafka本身 ;业务开发中的常见行为:对于耗时较高的行为，先写一条数据库记录，表示这个任务将被执行，之后再异步进行实际的任务执行；</p><p>write-ahead log会附带一个小问题，日志会越攒越多，要如何处理其自身的存储问题呢？有两个很自然的思路： 拆分和清理。</p><p>拆分即将大日志分割成多个小日志，由于WAL的逻辑一般都很简单，所以其拆分也不复杂，比一般的分库分表要容易很多。这种模式叫做 Segmented Log,  典型的实现场景就是kafka的分区。</p><p>关于清理，有一种模式叫做low-water mark(低水位模式)， 低水位，即对于日志中已经可以被清理的部分的标记。标记的方式可以基于其数据情况(redolog), 也可以基于预设的保存时间（kafka)，也可以做一些更精细的清理和压缩(aof)。 </p><h2 id="一致性协议"><a href="#一致性协议" class="headerlink" title="一致性协议"></a>一致性协议</h2><p>再来看网络环境下的问题，首先使用一个非常简单的心跳(HeartBeat)模式，就可以解决节点间状态同步的问题。一段时间内没有收到心跳，就将这个节点视为已宕机处理。</p><p>而关于脑裂的问题，通常会使用大多数(Quorum)这种模式，即要求集群内存活的节点数要能达到一个Quorum值，（通常集群内有2f+1个节点时，最多只能容忍f个节点下线，即quorum值为f+1），才可以对外提供服务。我们看很多分布式系统的实现时，比如rocketmq, zookeeper, 都会发现需要满足至少存活多少个节点才能正常工作，正是Quorum模式的要求。</p><p>Quorum解决了数据持久性的问题，也就是说，成功写入的数据，在节点失败的情况下，是不会丢失的。但是单靠这个，无法提供强一致性的保证，因为不同节点上的数据是会存在时间差的，client连接到不同节点上时，会产生不同的结果。可以通过主从模式(Leader and Followers) 解决一致性的问题。其中一个节点被选举为主节点，负责协调节点间数据的复制，以及决定哪些数据对client是可见的。</p><p>高水位（High-Water Mark）模式是用来决定哪些数据对client可见的模式。一般来说，在quorum个从节点上完成数据写入后，这条数据就可以标记为对client可见。完成复制的这条线，就是高水位。</p><p>主从模式的应用范围实在太广，这里就不做举例了。分布式选举算法很多，比如bully, ZAB, paxos, raft等。其中，paxos无论是理解还是实现难度都太大，bully在节点频繁上下线时会频繁的进行选举，而raft可以说是一种稳定性、实现难度等各方面相对均衡，使用也最广泛的一种分布式选举算法。像elastic search, 在7.0版本里，将选主算法由bully更换为raft；kafka 2.8里，也由利用zk的ZAB协议，修改为raft.</p><p>到这儿，我们先总结一下。实际上，一个对分布式系统的操作，基本上就可以概括为下边这么几步：</p><ol><li>写主节点的Write-Ahead Log;</li><li>写1个从节点的 WAL</li><li>写主节点数据；</li><li>写1个从节点数据</li><li>写quorum个子节点WAL</li><li>写quorum个子节点数据</li></ol><p>其中，2-5步之间的顺序不是固定的。分布式系统平衡性能和稳定性的最重要方式，实质上就是决定这几步操作的顺序，以及决定在哪个时间点向client端返回操作成功的确认信息。例如，mysql的同步复制、异步复制、半同步复制，就是典型的这种区别的场景。</p><h2 id="故障检测与恢复"><a href="#故障检测与恢复" class="headerlink" title="故障检测与恢复"></a>故障检测与恢复</h2><p>关于进程暂停，造成的主要的问题场景是这样的：假如主节点暂停了，暂停期间如果选出了新的主节点，然后原来的主节点恢复了，这时候该怎么办。这时候，使用Generation Clock这种模式就可以，简单的说，就是给主节点设置一个单调递增的代编号，表示是第几代主节点。像raft里的term， ZAB里的epoch这些概念，都是generation clock这个思路的实现。</p><p>再看看时钟不同步问题，在分布式环境下，不同节点的时钟之间必然是会存在区别的。在主从模式下，这种问题其实已经被最大限度的减少了。很多系统会选择将所有操作都在主节点上进行，主从复制也是采取复制日志再重放日志的形式。这样，一般情况下，就不用考虑时钟的事情了。唯一可能出问题的时机就是主从切换的过程中，原主节点和新主节点给出的数就有可能存在乱序。</p><p>一种解决时钟不同步问题的方案就是搞一个专门的服务用来做同步，这种服务叫做NTP服务。但这种方案也不是完美的，毕竟涉及到网络操作，所以难免产生一些误差。所以想依靠NTP解决时钟不同步问题时，系统设计上需要能够容忍一些非常微弱的误差。</p><p>其实，除了强行去把时钟对齐之外，还有一些简单一些的思路可以考虑。首先思考一个问题，我们真的需要保证消息绝对的按照真实世界物理时间去排列吗？其实不是的，我们需要的只是 一个自洽、可重复的确定消息顺序的方式，让各个节点对于消息的顺序能够达成一致即可。也就是说，消息不一定按照物理上的先后排列，但是不同节点排出来的应该一样。</p><p>有一种叫Lamport Clock的技术就能达到这个目标。它的逻辑很简单，如图所示 </p><p><img src="/img/lamport.png" alt="lamport stamp"></p><p>就是本机上的操作会导致本机上的stamp加1，发生网络通信时，比如C接收到B的数据时，会比较自己当前的stamp, 和B的stamp+1, 选出较大的值，变成自己当前的戳。 这样一个简单的操作，就可以保证任何有相关性的两个操作（包括出现在同一节点、有通信两种情况）的顺序在不同节点之间看来是一致的。</p><h2 id="设计原则总结"><a href="#设计原则总结" class="headerlink" title="设计原则总结"></a>设计原则总结</h2><p>另外，还有一些相对简单些的事情，也是分布式系统设计中经常要考虑的，比如怎么让数据均匀的分布在各个节点上。对于这个问题，我们可能需要根据业务情况去找一个合适的分片key, 也可能需要找到一个合适的hash算法。另外，也有一致性哈希这种技术，让我们控制起来更自如。</p><p>分布式系统设计中还需要重点考虑的一块就是如何衡量系统性能，指标包括性能（延迟、吞吐量）、可用性、一致性、可扩展性等等，这些说起来都比较好理解，但要是想更完善的去衡量，尤其是想更方便的去观测这些指标的话，也是一个很大的话题。</p><p>原文地址: <a href="https://lichuanyang.top/posts/45718/">https://lichuanyang.top/posts/45718/</a></p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-分布式系统设计中最重要的是学什么？有哪些核心模式？"><a href="#Q-分布式系统设计中最重要的是学什么？有哪些核心模式？" class="headerlink" title="Q: 分布式系统设计中最重要的是学什么？有哪些核心模式？"></a>Q: 分布式系统设计中最重要的是学什么？有哪些核心模式？</h3><p>最核心的不是某个具体工具（MySQL、Kafka、ZooKeeper 怎么用），而是<strong>跨系统的通用设计模式</strong>。文中总结了六大核心模式：1) <strong>Write-Ahead Log</strong>：先顺序写日志再异步处理，解决性能与持久化的矛盾；2) <strong>Segmented Log + Low-Water Mark</strong>：日志拆分与清理；3) <strong>HeartBeat</strong>：节点间状态同步；4) <strong>Quorum</strong>：大多数投票，解决脑裂和持久性；5) <strong>Leader and Followers + High-Water Mark</strong>：主从复制与数据可见性控制；6) <strong>Generation Clock</strong>：代编号，解决进程暂停后的脑裂。理解了这些模式，再看任何分布式系统都能快速把握其设计思路。</p><h3 id="Q-CAP-理论的实践意义是什么？为什么没有-CA-系统？"><a href="#Q-CAP-理论的实践意义是什么？为什么没有-CA-系统？" class="headerlink" title="Q: CAP 理论的实践意义是什么？为什么没有 CA 系统？"></a>Q: CAP 理论的实践意义是什么？为什么没有 CA 系统？</h3><p>CAP 说一致性（C）、可用性（A）、分区容忍性（P）三者最多同时满足两个。实践中的关键是：<strong>网络分区在分布式系统中是不可避免的</strong>（网络延迟、丢包、断连随时可能发生），因此 P 是必须接受的。一旦接受了 P，就只能选择 CP（放弃可用性，保证一致）或 AP（放弃一致性，保证可用）。不存在 CA 系统是因为：CA 系统假设网络永远正常——实际环境中一旦发生分区，集群无法合并回来，CA 假设就崩溃了。现实中的数据库和中间件无一例外都是 CP 或 AP 的设计选择。</p><h3 id="Q-分布式系统初学者从哪里开始？有没有学习路线？"><a href="#Q-分布式系统初学者从哪里开始？有没有学习路线？" class="headerlink" title="Q: 分布式系统初学者从哪里开始？有没有学习路线？"></a>Q: 分布式系统初学者从哪里开始？有没有学习路线？</h3><p>从文中可以理出一条学习路线：1) <strong>先理解 what 和 why</strong>：分布式系统的定义（多节点 + 有状态）和核心挑战（进程崩溃、网络延迟、脑裂、进程暂停、时钟不同步）；2) <strong>掌握六大核心模式</strong>（WAL、Quorum、Leader-Follower 等），理解每种模式解决哪个挑战；3) <strong>找一个具体系统深挖</strong>：比如读 Raft 论文理解 Leadership 选举和日志复制，或深入 Kafka 的设计理解 WAL + Segmented Log 的实际落地；4) <strong>举一反三</strong>：横向对比 MySQL 半同步复制、Redis AOF、RocketMQ 的 Dledger 等，找出不同系统对同一挑战的不同解决思路。关键是<strong>建立知识体系</strong>而非逐个学工具——学透一个，其余的触类旁通。</p><h3 id="Q-在”性能”和”一致性”之间如何做权衡决策？"><a href="#Q-在”性能”和”一致性”之间如何做权衡决策？" class="headerlink" title="Q: 在”性能”和”一致性”之间如何做权衡决策？"></a>Q: 在”性能”和”一致性”之间如何做权衡决策？</h3><h2 id="文中给出了一个实用的操作框架：通过控制主从复制各步骤的顺序和确认时机来调节。一个分布式写操作可分为六步：写主-WAL-→-写从-WAL-→-写主数据-→-写从数据-→-写-Quorum-个从-WAL-→-写-Quorum-个从数据。越早向客户端返回成功，性能越好但一致性越弱。具体策略：1-完成第-1-步就返回-异步复制（最高性能，可能丢数据）；2-完成第-1-第-3-步再返回-半同步复制（折中）；3-完成-Quorum-级别复制再返回-强一致性（最低性能）。MySQL-的三种复制模式就是这种权衡的典型体现——没有银弹，根据业务对数据丢失的容忍度选择。"><a href="#文中给出了一个实用的操作框架：通过控制主从复制各步骤的顺序和确认时机来调节。一个分布式写操作可分为六步：写主-WAL-→-写从-WAL-→-写主数据-→-写从数据-→-写-Quorum-个从-WAL-→-写-Quorum-个从数据。越早向客户端返回成功，性能越好但一致性越弱。具体策略：1-完成第-1-步就返回-异步复制（最高性能，可能丢数据）；2-完成第-1-第-3-步再返回-半同步复制（折中）；3-完成-Quorum-级别复制再返回-强一致性（最低性能）。MySQL-的三种复制模式就是这种权衡的典型体现——没有银弹，根据业务对数据丢失的容忍度选择。" class="headerlink" title="文中给出了一个实用的操作框架：通过控制主从复制各步骤的顺序和确认时机来调节。一个分布式写操作可分为六步：写主 WAL → 写从 WAL → 写主数据 → 写从数据 → 写 Quorum 个从 WAL → 写 Quorum 个从数据。越早向客户端返回成功，性能越好但一致性越弱。具体策略：1) 完成第 1 步就返回 &#x3D; 异步复制（最高性能，可能丢数据）；2) 完成第 1+第 3 步再返回 &#x3D; 半同步复制（折中）；3) 完成 Quorum 级别复制再返回 &#x3D; 强一致性（最低性能）。MySQL 的三种复制模式就是这种权衡的典型体现——没有银弹，根据业务对数据丢失的容忍度选择。"></a>文中给出了一个实用的操作框架：<strong>通过控制主从复制各步骤的顺序和确认时机来调节</strong>。一个分布式写操作可分为六步：写主 WAL → 写从 WAL → 写主数据 → 写从数据 → 写 Quorum 个从 WAL → 写 Quorum 个从数据。<strong>越早向客户端返回成功，性能越好但一致性越弱</strong>。具体策略：1) 完成第 1 步就返回 &#x3D; 异步复制（最高性能，可能丢数据）；2) 完成第 1+第 3 步再返回 &#x3D; 半同步复制（折中）；3) 完成 Quorum 级别复制再返回 &#x3D; 强一致性（最低性能）。MySQL 的三种复制模式就是这种权衡的典型体现——没有银弹，根据业务对数据丢失的容忍度选择。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/45718/</id>
    <link href="https://lichuanyang.top/posts/45718/"/>
    <published>2022-04-13T12:13:24.000Z</published>
    <summary>分布式系统设计通用方法总结，涵盖多节点有状态系统的核心设计模式和实践经验。</summary>
    <title>分布式系统设计中的通用方法</title>
    <updated>2026-06-27T03:42:25.687Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="架构设计" scheme="https://lichuanyang.top/categories/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="高并发" scheme="https://lichuanyang.top/tags/%E9%AB%98%E5%B9%B6%E5%8F%91/"/>
    <category term="高并发解决方案" scheme="https://lichuanyang.top/tags/%E9%AB%98%E5%B9%B6%E5%8F%91%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/"/>
    <category term="高并发设计" scheme="https://lichuanyang.top/tags/%E9%AB%98%E5%B9%B6%E5%8F%91%E8%AE%BE%E8%AE%A1/"/>
    <content>
      <![CDATA[<p>高请求并发就一定会有高并发问题吗？其实不是的。假设一个应用全部是纯内存计算，无论请求量多大，简单加机器就能线性扩展——那就不存在什么高并发问题。</p><p>高并发问题之所以存在，是因为系统中存在<strong>无法靠粗暴扩容解决的单点瓶颈</strong>。而这个瓶颈，在 90% 以上的场景中，都是<strong>数据库</strong>。</p><span id="more"></span><h2 id="核心思路：降低单库的连接数"><a href="#核心思路：降低单库的连接数" class="headerlink" title="核心思路：降低单库的连接数"></a>核心思路：降低单库的连接数</h2><p>所有高并发技术方案，归根结底都是在做同一件事——<strong>降低单个数据库实例需要承载的并发连接数</strong>。围绕这个目标，有六个层次的技术手段：</p><h2 id="层次一：系统拆分（业务维度）"><a href="#层次一：系统拆分（业务维度）" class="headerlink" title="层次一：系统拆分（业务维度）"></a>层次一：系统拆分（业务维度）</h2><p>最粗粒度的优化：把不同业务模块的数据库物理分开。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">单体架构：所有业务 → 一个数据库（1000 QPS）</span><br><span class="line">拆分后：</span><br><span class="line">  用户服务 → 用户库（400 QPS）</span><br><span class="line">  订单服务 → 订单库（300 QPS）</span><br><span class="line">  商品服务 → 商品库（300 QPS）</span><br></pre></td></tr></table></figure><p>每个业务独享自己的数据库，互不影响。拆分的难点在于分布式事务——以前一个事务搞定的事，现在跨库了。解决方案包括 Saga 模式、Seata、RocketMQ 事务消息等。</p><h2 id="层次二：缓存（减少数据库命中率）"><a href="#层次二：缓存（减少数据库命中率）" class="headerlink" title="层次二：缓存（减少数据库命中率）"></a>层次二：缓存（减少数据库命中率）</h2><p>在应用和数据库之间加一层 Redis，热数据走缓存，只有冷数据才查库。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">请求 → 缓存（命中 90%）→ 返回</span><br><span class="line">     → 缓存（未命中 10%）→ 数据库 → 写入缓存 → 返回</span><br></pre></td></tr></table></figure><p>缓存架构的几个关键决策：</p><table><thead><tr><th>问题</th><th>方案</th></tr></thead><tbody><tr><td>缓存穿透</td><td>布隆过滤器、空值缓存</td></tr><tr><td>缓存击穿</td><td>互斥锁、逻辑过期</td></tr><tr><td>缓存雪崩</td><td>过期时间随机化、多级缓存</td></tr><tr><td>缓存一致性</td><td>Cache Aside（先更新 DB、再删缓存）</td></tr></tbody></table><h2 id="层次三：MQ-削峰（平滑流量）"><a href="#层次三：MQ-削峰（平滑流量）" class="headerlink" title="层次三：MQ 削峰（平滑流量）"></a>层次三：MQ 削峰（平滑流量）</h2><p>瞬时流量如洪水般涌入时，消息队列充当”水库”：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">10 万 QPS → MQ → 1000 QPS 匀速消费 → 数据库</span><br></pre></td></tr></table></figure><p>以秒杀为例，10 万人同时下单，数据库每秒只能处理 1000 个。用 RocketMQ&#x2F;Kafka 做缓冲，请求先进队列，后台服务按自己的节奏慢慢消费。用户体验变成”排队中”，而不是”系统崩溃”。</p><h2 id="层次四：分库分表（同一业务内拆分）"><a href="#层次四：分库分表（同一业务内拆分）" class="headerlink" title="层次四：分库分表（同一业务内拆分）"></a>层次四：分库分表（同一业务内拆分）</h2><p>当一个业务的单库也扛不住时，对同一业务做进一步拆分：</p><ul><li><strong>垂直拆分</strong>：按字段拆——用户基础信息一张表，用户扩展信息一张表</li><li><strong>水平拆分（分表）</strong>：按行拆——user_0, user_1, user_2… 按 user_id 取模路由</li><li><strong>读写分离</strong>：主库写、从库读——适合读多写少的场景</li></ul><p>分库分表的关键是 <strong>sharding key 的选择</strong>——尽量让大部分查询落在单个分片上，避免跨分片查询。ShardingSphere 是目前最流行的分库分表中间件。</p><h2 id="层次五：异构存储（不同引擎各司其职）"><a href="#层次五：异构存储（不同引擎各司其职）" class="headerlink" title="层次五：异构存储（不同引擎各司其职）"></a>层次五：异构存储（不同引擎各司其职）</h2><p>MySQL 不是万能的，不同类型的查询用不同的存储引擎：</p><table><thead><tr><th>数据引擎</th><th>擅长</th><th>场景</th></tr></thead><tbody><tr><td>MySQL</td><td>事务、关联查询</td><td>核心业务数据</td></tr><tr><td>Elasticsearch</td><td>全文搜索、聚合分析</td><td>商品搜索、日志查询</td></tr><tr><td>ClickHouse</td><td>OLAP 分析、海量聚合</td><td>用户行为分析、BI 报表</td></tr><tr><td>Redis</td><td>超高 QPS、简单 KV</td><td>缓存、计数器、排行榜</td></tr><tr><td>HBase</td><td>海量稀疏数据、宽表</td><td>用户画像、时序数据</td></tr></tbody></table><h2 id="层次六：限流与熔断（兜底保护）"><a href="#层次六：限流与熔断（兜底保护）" class="headerlink" title="层次六：限流与熔断（兜底保护）"></a>层次六：限流与熔断（兜底保护）</h2><p>前面的手段是”尽力而为”，限流是”保证下限”——即使扛不住，也别让整个系统崩溃。</p><p>常用的限流算法：</p><ul><li><strong>令牌桶</strong>（Guava RateLimiter）：匀速放令牌，请求来取令牌，没令牌就拒绝</li><li><strong>漏桶</strong>：固定速率漏水，请求像水倒进桶里，满了溢出</li><li><strong>滑动窗口</strong>（Sentinel）：统计窗口内的请求数，超过阈值拒绝</li></ul><p><strong>熔断</strong>（Circuit Breaker）：当某个下游服务持续失败时，不再徒劳地继续调用，快速失败，给下游喘息空间。Hystrix（已停更）→ Resilience4j → Sentinel，是熔断框架的演进路线。</p><h2 id="高并发架构全景图"><a href="#高并发架构全景图" class="headerlink" title="高并发架构全景图"></a>高并发架构全景图</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">             ┌──────────────┐</span><br><span class="line">限流 ←───────│   负载均衡    │───────→ 限流</span><br><span class="line">             └──────┬───────┘</span><br><span class="line">                    │</span><br><span class="line">      ┌─────────────┼─────────────┐</span><br><span class="line">      ▼             ▼             ▼</span><br><span class="line"> ┌─────────┐  ┌─────────┐  ┌─────────┐</span><br><span class="line"> │ 用户服务  │  │ 订单服务  │  │ 商品服务  │  ← 系统拆分</span><br><span class="line"> └────┬────┘  └────┬────┘  └────┬────┘</span><br><span class="line">      │             │             │</span><br><span class="line"> ┌────▼────┐  ┌────▼────┐  ┌────▼────┐</span><br><span class="line"> │  Redis   │  │   MQ    │  │   ES    │  ← 缓存/削峰/异构存储</span><br><span class="line"> └────┬────┘  └────┬────┘  └────┬────┘</span><br><span class="line">      │             │             │</span><br><span class="line"> ┌────▼─────────────▼─────────────▼────┐</span><br><span class="line"> │          MySQL（分库分表）              │  ← 分库分表</span><br><span class="line"> │  user_0  user_1  order_0  order_1   │</span><br><span class="line"> └─────────────────────────────────────┘</span><br></pre></td></tr></table></figure><h2 id="池子（连接池-线程池）的调优"><a href="#池子（连接池-线程池）的调优" class="headerlink" title="池子（连接池&#x2F;线程池）的调优"></a>池子（连接池&#x2F;线程池）的调优</h2><p>各种”池子”本质也是一种限流手段：</p><ul><li><strong>Tomcat 线程池</strong>：限制同时处理的 HTTP 请求数</li><li><strong>数据库连接池</strong>（HikariCP）：限制同时打到数据库的连接数</li><li><strong>线程池</strong>：限制并发执行的任务数</li></ul><p>调优原则：</p><ul><li><strong>池子不能太大</strong>：超过数据库承载能力，大家都慢</li><li><strong>池子不能太小</strong>：资源利用不充分</li><li><strong>做好监控</strong>：activeCount、queueSize、waitCount —— 任何一个接近上限，就该准备扩容了</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>高并发的本质是一道”流量管理题”。六个层次的技术手段，从粗到细逐步降低单库的连接数：</p><blockquote><p>系统拆分 → 缓存 → MQ 削峰 → 分库分表 → 异构存储 → 限流熔断</p></blockquote><h2 id="理解了”降低单库连接数”这条主线，你会发现各种高并发技术方案不再是零散的技巧，而是一个层次分明的体系。"><a href="#理解了”降低单库连接数”这条主线，你会发现各种高并发技术方案不再是零散的技巧，而是一个层次分明的体系。" class="headerlink" title="理解了”降低单库连接数”这条主线，你会发现各种高并发技术方案不再是零散的技巧，而是一个层次分明的体系。"></a>理解了”降低单库连接数”这条主线，你会发现各种高并发技术方案不再是零散的技巧，而是一个层次分明的体系。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/11970/</id>
    <link href="https://lichuanyang.top/posts/11970/"/>
    <published>2022-03-29T10:40:46.000Z</published>
    <summary>从数据库瓶颈的本质出发，轻松讲清楚高并发设计的核心思路和常见解决方案。</summary>
    <title>高并发解决方案很难吗？轻松聊清楚高并发设计</title>
    <updated>2026-06-27T02:06:09.318Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="计算广告" scheme="https://lichuanyang.top/tags/%E8%AE%A1%E7%AE%97%E5%B9%BF%E5%91%8A/"/>
    <category term="互联网广告" scheme="https://lichuanyang.top/tags/%E4%BA%92%E8%81%94%E7%BD%91%E5%B9%BF%E5%91%8A/"/>
    <category term="RTB" scheme="https://lichuanyang.top/tags/RTB/"/>
    <content>
      <![CDATA[<p>本文中，我会介绍一下互联网广告的发展状态，并梳理一下互联网广告的底层逻辑，帮助大家更好的理解这个行业。</p><span id="more"></span><h2 id="什么是广告"><a href="#什么是广告" class="headerlink" title="什么是广告"></a>什么是广告</h2><p>讲互联网广告，首先要从广告本身是什么开始讲起。只有理解了广告的底层逻辑，才能更好的帮助我们理解在互联网广告中会有什么问题, 为什么会出现广告的程序化交易，adx, ssp, dsp,dmp等等各种各样不同的系统为什么会出现，以及诸如此类的等等问题。</p><p>广告这个行业其实自古就有，从远古时代小商小贩的吆喝，各种建筑物上的告示，到纸质报纸、杂志、电视等传统媒体上的广告，再到互联网广告，这个行业总是再随着社会的发展产生它自己的变化。</p><p>对于广告，先上一个非常官方、非常标准的定义：广告是由已确定的出资人通过各种媒介进行的有关产品（商品、服务和观点）的，并且通常是有偿的、综合的、劝服性的非人员的信息传播活动。</p><p>而更通俗的讲，在广告交易中，核心的参与方有三个：媒体(即供给方，Supply), 广告主（即需求方， Demand），受众（即媒体的用户）。其中，媒体和广告主是主动的参与方，受众则是被动的参与方。而广告实际上就是这三方相互博弈的一个过程。对于广告主来说，希望有更多的受众能看到广告，并且从广告中获取更高的转化；对于媒体来说，希望在获得最大的广告利润的同时，又能减少对用户的打扰；而对受众，也就是普通用户，则是希望不被自己不想看到的广告打扰。</p><p>接下来我们分析一下这个流程里的博弈点。众所周知，如果一件事对于各参与方是零和博弈的话，这件事情就难免陷入到恶性竞争中，必定是难以发展的。而广告行业既然能持续健康的发展，主要是靠对广告生态之外产生的影响，所以我们就分析一下有哪些对广告生态外的影响因素。</p><p>我们先拆解下各方都有哪些考虑指标，对于媒体，主要就是广告收益和自己产品的留存，对于广告主，则是广告费用和通过广告获得的收益，受众作为被动参与者，在整个过程中能做的不多，只能用脚投票，离开广告太烂的产品。这里边，在整个广告生态之外的，就是广告主通过广告获得的收益，和减少广告对受众造成的打扰，两个方面。事实上，对这两点的提升也就是整个广告行业不断发展的源动力。</p><p>理解了这一点，能帮助我们更好的理解互联网广告的发展历程。</p><h2 id="互联网广告的发展历程"><a href="#互联网广告的发展历程" class="headerlink" title="互联网广告的发展历程"></a>互联网广告的发展历程</h2><h2 id="互联网广告的早期发展"><a href="#互联网广告的早期发展" class="headerlink" title="互联网广告的早期发展"></a>互联网广告的早期发展</h2><p>最早的互联网广告，实质上就是把传统媒体的广告形式直接搬到线上来。比如说一份报纸里，会有一个区域专门用来放广告。像我们早些年在搜狐、新浪这些网站上看到的广告，也都是这种形式。广告主直接购买网页上的某个区域，固定的显示某些广告内容。</p><p>接下来，有些人会逐渐注意到互联网广告和传统媒体的一个关键区别：互联网广告的内容是可以因人而异的。已经印刷出去的报纸，所有人看到的都一样，而对于网站来说，完全可以做到对不同人群，比如男性和女性展示不同的广告。而媒体只需向广告主保证投放量，以及投放量不能完成时的一些赔偿方式。这就是合约广告。</p><p>合约广告的一个很自然的发展趋势就是定向越来越精细，诸如性别、年龄、地域、兴趣爱好等，都可以成为合约条件的一部分。同时，参与进来的广告主越来越多。这两方面的因素都会导致履约的难度急剧上升。同时，这么复杂的逻辑下，媒体也会觉得自己很多流量没有卖出最高价。</p><p>在这种情况下，新的广告形式应运而生，也就是竞价广告。供给方不再以合约的方式给出量的保证，只保证单位流量的成本；每一次广告展示都按照收益最高的原则来决策。这样，广告就逐渐发展成了类似「流量交易」这种模式,  形成了程序化广告交易这种模式。</p><h2 id="程序化交易广告"><a href="#程序化交易广告" class="headerlink" title="程序化交易广告"></a>程序化交易广告</h2><p>在竞价广告的场景下，没有了合约的约束，大量媒体、广告主进入了一个多方博弈的环境，交易过程也就越来越复杂，产生了诸如RTB(real time bidding，实时竞价)这样的交易方式。程序化广告交易正是在这种情况下产生的。</p><p>同时，随着技术的发展，更多的数据、深入的计算与预测成为可能，广告主、媒体以及相关的广告代理等各个参与方也都需要更加丰富的功能。我们标题上提到的ssp,adx, dsp, rtb这些名词，也都是在这种情况下，逐步发展成为广告业态中非常重要的系统。</p><p>总体来说，程序化广告交易和股票交易等实时交易的场景都非常类似，都是存在一个交易场（ADX）, 买方（广告主）和卖方（媒体），各自在其中实时的做买卖决策。唯一一个重要的区别是，对于媒体来说，没有择时的机会，流量来了只能立刻卖出去，没有办法攒起来等一个好时机。可以说，这一点正是广告相关各项技术发展的出发点。</p><h2 id="互联网广告的关键问题和组成部分"><a href="#互联网广告的关键问题和组成部分" class="headerlink" title="互联网广告的关键问题和组成部分"></a>互联网广告的关键问题和组成部分</h2><p>前面说了，互联网广告发展的源动力有两点，一是提升广告主通过广告获得的收益，二是减少广告对受众造成的打扰。这两点关系到的是广告的有效性，所以我们分析一下广告有效性有哪些影响因素。</p><p>一个广告的生命周期大概可以拆解成这么几步：</p><p>曝光-关注-理解-接受-保持-决策</p><p>简单的说，就是广告先要展示出来，然后能被受众看到，并且理解广告想传达的信息，进一步，能接受这个信息，并且保持一段时间，最终完成对广告所宣传的产品决策（下载、购买、付费等）。</p><p>接下来，我们梳理一下每一步的具体影响，以及互联网技术能对其起到什么作用。</p><p>曝光：这一阶段主要取决于广告位的物理熟悉，比如一个app开屏位置的广告，和某个隐藏的很深的页面上的一条广告，其有效性必然差别很大。这一点，在现实世界的广告中也是一样的，也没有太多技术上可优化的空间。</p><p>关注：即让用户的注意力能够到这条广告上。值得注意的事，「看到」并不等于「关注到」，如果产品设计不合理，很容易导致用户对眼前的东西视而不见。比如说，用户到某个页面上是非常明确的要执行完一个流程的，比如要下载个东西，那么，你在他下载过程中强行插入一条广告，很可能会导致用户完全忽视掉它。所以，这一点和产品设计关系非常大。另外，这也是一个技术能发挥重要作用的阶段。通过搭建DMP平台，结合各种机器学习算法，将广告投放给更有可能感兴趣的用户，从而提升广告的有效性。</p><p>理解：这一步影响最大的是广告素材，素材内容设计的好，就能让用户更好的理解。</p><p>接受：这一点说白了就是能不能让用户认可你的广告给出来的信息或观点，这个一方面与素材是否专业有关，另一方面和媒体的自身属性也有很大关系，比如丁香医生和某莆田医院官网上放同样的信息，其有效性也必然是大为不同的。</p><p>保持和关注：这两步其实逐渐就脱离了广告业态的范围，不过通过一些优质的素材设计、合适的场景、精准的人群投放，还是可以为最终转化做好铺垫的。</p><p>此外，在其中这些技术无法直接干预的阶段，提供更完善的数据、更方便的测试方法，也是能给广告效果带来很大帮助的。</p><p>接下来，我们再分别从媒体和广告主出发，看看他们各自有什么需求, 以及简单看看ssp, dsp这些平台是怎么做的。由于篇幅限制，这一部分先不详细展开，如果感兴趣的话，我们后边再逐一展开介绍。</p><p>服务媒体的平台即常说的SSP, 其主要功能就是帮助媒体提升营收。里边一些基本的功能，比如让媒体配置一个广告位，以及广告位的长宽等信息。 对于提升营收，主要也就是通过合适的人群与广告匹配，优化定价策略等方式，同时设法控制网络等IT成本来进行。</p><p>与SSP相对的DSP, 就是服务广告主这一侧的平台。DSP的基本功能就是让广告主设置投放计划，比如在多长时间内，向什么样特征的用户，投放总预算多少的广告。而平台发挥的价值主要是帮助广告主花更少的钱获取更好的广告效果。</p><p>直到今天，广告生态中涉及到的每一个模块其实都还在持续不断的迭代中，但万变不离其宗，关键的动力其实就是让广告投放的效果更好，用户也能获得更好的浏览广告的体验。只有这样，整个行业才能朝着健康的方向去发展。</p><p>原文地址：<a href="https://lichuanyang.top/posts/27934/---">https://lichuanyang.top/posts/27934/---</a></p>]]>
    </content>
    <id>https://lichuanyang.top/posts/27934/</id>
    <link href="https://lichuanyang.top/posts/27934/"/>
    <published>2022-03-07T08:28:58.000Z</published>
    <summary>从广告的本质出发，系统梳理互联网广告的发展脉络，讲清楚 SSP、DSP、RTB、ADX 等核心概念及程序化交易的底层逻辑。</summary>
    <title>SSP、DSP、RTB、ADX都是什么？讲讲互联网广告的概念与发展</title>
    <updated>2026-06-27T03:57:07.064Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="数据库" scheme="https://lichuanyang.top/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <category term="数据库事务" scheme="https://lichuanyang.top/tags/%E6%95%B0%E6%8D%AE%E5%BA%93%E4%BA%8B%E5%8A%A1/"/>
    <category term="数据库" scheme="https://lichuanyang.top/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <category term="ACID" scheme="https://lichuanyang.top/tags/ACID/"/>
    <category term="事务隔离级别" scheme="https://lichuanyang.top/tags/%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB/"/>
    <content>
      <![CDATA[<p>前些日子读了周志明老师的《凤凰架构》这本书，对于很多方面的技术有了更深的认知，因此打算做一些总结。今天先以讲事务的这一段做个印子，结合书中内容和个人理解，争取将本地事务的相关知识讲个命名白白。如果有讲的不对的地方，欢迎大家多多指正。</p><span id="more"></span><p>所谓事务，就是保证数据库中的数据都是符合期望的，在不断的增删改查中，数据库会不断的从一个正确的状态变化到另一个正确的状态，而不会被外界感知到不“正确”的中间状态。</p><p>举个常见的例子，就是在 A有100元，B有100元的状态下，A要给B转账10元，肯定会有A先转出10元，再转给B,这样的中间状态。事务就是，对于用户来说，只能感知到 A100 B100 和 A90 B110 这两个状态，而不会感知到过程中的A90 B100等等奇奇怪怪的状态。</p><p>这个介绍，其实也就是事务ACID特性中一致性的概念。ACID这个说法虽然很流行，但A、C、I、D之间其实并不是平等的概念，简单来说，AID是方法，C是目的。也就是说，实现了原子性、持久性、隔离性，也就实现了一致性，也就实现了事务。</p><p>接下来，我们就依次看看AID分别要如何实现。</p><h2 id="原子性和持久性"><a href="#原子性和持久性" class="headerlink" title="原子性和持久性"></a>原子性和持久性</h2><p>实现原子性和持久性面临的相同问题其实挺多的，所以把它们放在一起介绍。</p><p>先复习一下基本概念，所谓原子性，就是事务内的操作要么都成功，要么都失败；所谓持久性，就是已经完成的操作不会丢失。</p><p>值得说明的是，单纯的“实现原子性和持久性”并不存在任何难度，要讨论的问题其实是“如何更高性能的实现原子性和持久性”，（后面要说的隔离性也是一个概念，只有高性能的隔离性才有意义）。</p><p>其中一个关键点在于，写磁盘是一个非常重的操作，所以通常会存在一个内存缓冲区，要写磁盘的数据会先写到缓冲区里，再择机落盘。那么假如在事务已提交而尚未落盘的这个时间点，系统出现故障，那么这部分未落盘的数据自然就会丢失，数据库也就失去了持久性。针对这个问题，一个很自然的想法就是事务提交的时候强制刷盘。这个方案可不可行？当然是可行的。但它的问题就是会影响性能。系统出故障毕竟是小概率事件，为了处理这个小概率事件，相当于所有操作都要额外付出一些代价。</p><p>实际上为了解决这个问题，一个常规的处理思路就是使用一个commit Log, 也就是在实际写数据之前，先将所有要修改的信息记录在一个log里，如果出现上面描述的问题，系统重启时，会先根据commit log进行数据恢复。而由于写这份log是一个顺序写磁盘的操作，性能会远远好于随机写磁盘，所以这个方式的性能是没有问题的。在数据真正写入之后，再加一个标记，表示这条log已经完成了持久化。</p><p>接下来，我们再看一下commit log是否还有优化空间？自然是有的。Commit log的一个重要缺陷就是所有真实的磁盘操作都必须发生在事务提交之后。假如说这个事务非常大，就会占用很大的内存缓冲区，这也会影响系统的性能。改进方案是 write-ahead log 这个机制，这个机制我在之前的文章(<a href="https://lichuanyang.top/posts/3914/">https://lichuanyang.top/posts/3914/</a>)里也介绍过，和commit log其实非常像，也是先顺序写一个log文件，唯一的区别就是write-ahead log允许在事务提交之前写入。mysql里的redo log，其实就是一个典型的write-ahead log实现。</p><p>讲到这里，我们先暂停一下，回顾一下上面的内容，会发现上边其实基本都在说持久性，原因是对于上边的机制来说，原子性其实都是自然而然的事情，commit log写进去了，这条事务就相当于完成了；commit log没写入，这个事务就相当于不存在。但是使用了write-ahead log的话，情况就不一样了，一个事务会涉及多次磁盘写入，所以也就不满足原子性了。因此，需要引入别的机制来保证原子性，undo log就是实现这个目标的一个典型思路。当变动数据写入磁盘前，必须先记录undo log，注明修改了哪个位置的数据、从什么值改成什么值等，以便在事务回滚或者崩溃恢复时根据undo log对提前写入的数据变动进行擦除。</p><p>像在mysql中，实际上也就是像我们上边讲的那样，利用redo log和undo log实现高效可靠的持久性和原子性。</p><h2 id="隔离性，隔离级别"><a href="#隔离性，隔离级别" class="headerlink" title="隔离性，隔离级别"></a>隔离性，隔离级别</h2><p>如何实现不同事务之间的隔离，一个很自然的思路就是加锁，实际上常规的数据库实现也就是这么做的。一般来说，有这么几种类型的锁：读锁（也叫共享锁），写锁（也叫排他锁），范围锁。</p><p>对于一条数据，只有一个事务能持有写锁；不同的事务可以同时持有读锁，数据被添加读锁后不能再添加写锁, 添加写锁后也不能再添加读锁；范围锁则是对一个范围加写锁，在这个范围内都不能写入数据。</p><p>我们知道，数据库有四种常见的隔离级别：可串行化，可重复读，读已提交，读未提交。其区别其实就是加锁粒度的不同。</p><p>如果我们把所有操作能加的锁都加上，实际上就是串行化的操作了。这种方式隔离性当然很好，但性能就没法说了，所以一般也不会有人使用。</p><p>可重复读则是对涉及到的数据加读锁和写锁，并持有到事务结束，但不会加范围锁。这样就会出现幻读的问题，即一个事务内执行两次范围查询，如果这两次查询之间有新的数据被插入，就会导致两次范围查询的结果不一致。</p><p>读已提交和可重复读的区别是他的读锁会在查询操作结束之后立刻释放掉，这样，在事务执行过程中，已经查询过的数据是可以被其他事务任意修改的，所以也就会有不可重复读的问题。</p><p>读未提交级别下，则完全不会加读锁。这样造成的问题是，由于读操作时不会去申请读锁，所以反而会导致能够读到其他事务上加了写锁的数据，也就会出现脏读的问题。</p><p>其实说到底，隔离性和性能就是一对相互矛盾的需求。加锁加的越多，隔离性自然越好，性能也自然越差。我们需要根据实际的使用场景来决定锁究竟要加到什么程度。</p><p>而另一个思路，则是再看看有没有锁以外的方式，考虑28原则，看看有没有什么办法，牺牲20%的性能去解决80%的问题。具体来讲，涉及隔离性问题的场景，其实可以简化为一个读事务+一个写事务，和两个写事务，这两种场景。大部分场景下，当然是读+写的情况更多，所以我们找个方式去解决读+写场景下的幻读问题。相信很多人已经猜到了，这种方式就是MVCC。关于MVCC, 网上介绍资料实在太多，我们就不再赘述了。</p><p>经过上面对于事务几个特性的介绍，相信大家已经对本地事务有了非常深刻的认识。如有问题，欢迎留言讨论。下一篇文章，我会继续讲一下分布式事务的相关知识，如果感兴趣，可以关注我的个人博客、知乎或者公众号追更~</p><h2 id="原文地址-https-lichuanyang-top-posts-7774"><a href="#原文地址-https-lichuanyang-top-posts-7774" class="headerlink" title="原文地址: https://lichuanyang.top/posts/7774/"></a>原文地址: <a href="https://lichuanyang.top/posts/7774/">https://lichuanyang.top/posts/7774/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/7774/</id>
    <link href="https://lichuanyang.top/posts/7774/"/>
    <published>2022-02-07T08:40:02.000Z</published>
    <summary>从 redo log、undo log 到隔离级别，刨根问底讲清楚数据库事务和 ACID 的完整机制。</summary>
    <title>从redolog,undolog到隔离级别，刨根问底，讲清楚事务和ACID</title>
    <updated>2026-06-27T03:44:21.592Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="云原生" scheme="https://lichuanyang.top/categories/%E4%BA%91%E5%8E%9F%E7%94%9F/"/>
    <category term="kubernetes" scheme="https://lichuanyang.top/tags/kubernetes/"/>
    <category term="持续集成" scheme="https://lichuanyang.top/tags/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/"/>
    <content>
      <![CDATA[<p>很多创业公司相比于大厂，一个非常重要的劣势就是基础设施不完善，没有各种各样完善的工具。因此，我打算整理一下，基于开源社区提供的能力，如何用尽量小的运维和开发成本，去搭建出一套体验良好的开发流程。</p><span id="more"></span><p>首先，我们理一下整个开发流程中的必备步骤都有哪些，我大概罗列一下，包括项目的创建、开发、code review、部署测试环境、部署灰度和线上环境、查看线上监控、日志、以及排查问题等。</p><p>根据这整个流程链条，我们再来理一下为了尽可能的提升开发和运维效率，我们需要把哪些方面保障好，以及这些方面可以有什么低成本的解决方案。</p><p>有几个点吧：</p><ul><li>快速创建一个项目，并且将必备的web、监控、日志等组件添加好；</li><li>提升一些常规的代码开发，比如数据库增删改查的开发效率；</li><li>简化打包到部署的流程，测试环境能自动部署更好；</li><li>能够给每个服务轻松的加上一整套监控和报警，包括通用的机器、jvm、接口层面的指标，和根据业务自定义的指标；</li><li>能有地方集中看各个节点的日志；</li><li>必要的时候，可以在线上节点上有强力的工具帮助排查问题。</li></ul><p>接下来，我们就看一下这些问题分别怎么解决。</p><h2 id="部署环境"><a href="#部署环境" class="headerlink" title="部署环境"></a>部署环境</h2><p>首先，在本文的标题中，其实已经假定了部署环境就是kubernetes, 所以，我先说一下为什么只有kubernetes这一个选项。随便翻开一个kubernetes的介绍资料，我们都可以看到很多它的优势说明，这里我们不多赘述。而其中最关键的，也是每个人都能切身体会到的好处，其实就是它提供了完善的自动扩缩容机制。可以非常简单的设置一个cpu消耗的期望值，k8s集群就可以根据目前cpu的负载去调整pod数量。我对比了我们启用自动扩缩容前后的数据，每天的机器成本可以相差接近600美金，占总成本的1&#x2F;3还要多。</p><p>由于k8s本身的运维成本是很高的，推荐直接购买云服务商的服务，无论aws还是阿里云，都提供了很完善的k8s集群功能。</p><p>另外，推荐使用 kuboard (<a href="https://kuboard.cn/">https://kuboard.cn/</a>) 这个工具对k8s进行管理。kuboard是一个图像化的k8s管理工具，包括部署、配置、扩容、登入pod等常见操作，都可以在图形化界面下操作，使用很方便。</p><h2 id="项目开发"><a href="#项目开发" class="headerlink" title="项目开发"></a>项目开发</h2><p>一般基于spring initializr创建一个项目就可以。对于常用的日志、监控等各类配置，建议整理一份最佳实践并做一个模板项目。新项目可以基于这个模板项目来创建，可以利用mvn archetype或者类似的工具让这个过程更加顺畅。</p><p>至于一些常用的数据库、缓存等基本代码，感觉spring全家桶已经足够好用了。</p><p>对于code review, 我们可以使用gitlab的webhook, 当代码提交时，自动给项目组的人发通知。</p><h2 id="打包-部署"><a href="#打包-部署" class="headerlink" title="打包&amp;部署"></a>打包&amp;部署</h2><p>对于springboot项目，可以用buildpacks 打成docker镜像，而不用再考虑docker file的细节。具体的buildpacks产品，我们用的是paketo buildpacks, 其他的诸如cloudfoundry,heroku也都差不多，目前没有还研究这些包有什么具体需求。</p><p>接下来，我们仍然可以利用gitlab的webhook, 触发一次jenkins的构建任务。在jenkins的构建任务重，我们可以完成打包和部署到kubernetes测试集群这些操作。</p><h2 id="监控-报警"><a href="#监控-报警" class="headerlink" title="监控&amp;报警"></a>监控&amp;报警</h2><p>推荐使用 prometheus + grafana 套餐即可，关于prometheus的使用，以及在springboot项目下的配置，可以参考我之前的文章  h<a href="https://lichuanyang.top/posts/28288/">ttps:&#x2F;&#x2F;lichuanyang.top&#x2F;posts&#x2F;28288&#x2F;</a>  。</p><p>在k8s上，可以装一个weave cloud agent， 然后就可以配置对prometheus接口进行自动抓取了。</p><p>在grafana里，可以直接写promeql配置监控报表。 另外，grafana官网上，有大量的别人共享出来的图表，可以直接使用。</p><p>在grafana里，也可以配置各种各样自定义规则的报警。如果使用飞书的话，在飞书里配置grafana助手，可以很容易的将飞书作为grafana的报警通道。</p><h2 id="日志"><a href="#日志" class="headerlink" title="日志"></a>日志</h2><p>可以使用loki (<a href="https://grafana.com/oss/loki/">https://grafana.com/oss/loki/</a>)作为日志收集、查询的工具。loki可以认为是一个轻量级的ELK, 其维护成本会比ELK低很多。</p><h2 id="排查问题"><a href="#排查问题" class="headerlink" title="排查问题"></a>排查问题</h2><p>对于java的项目，使用神器arthas可以解决绝大多数问题排查的需求。对于arthas的接入，可以采用arthas springboot starter这种方式 。而对于k8s上的pod, 如果环境能够和办公环境打通，那可以利用k8s的port forward 功能，将arthas的端口转发到本地来。 当然，这样做的话，务必要控制好权限。</p><h2 id="金丝雀部署"><a href="#金丝雀部署" class="headerlink" title="金丝雀部署"></a>金丝雀部署</h2><p>关于金丝雀部署的作用和实现方式，可以参考我的另一篇文章 <a href="https://lichuanyang.top/posts/30764/">https://lichuanyang.top/posts/30764/</a></p><p>总结一下, 对于运维来说，只需要维护一些诸如gitlab, kuboard, prometheus, grafana, loki之类的基础设施，而且基本都是一些维护较简单的工具。在此基础上，我们辅助以合理的流程和技巧，就能实现一个非常好的开发体验。</p><h2 id="原文地址-https-lichuanyang-top-posts-40964"><a href="#原文地址-https-lichuanyang-top-posts-40964" class="headerlink" title="原文地址: https://lichuanyang.top/posts/40964/"></a>原文地址: <a href="https://lichuanyang.top/posts/40964/">https://lichuanyang.top/posts/40964/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/40964/</id>
    <link href="https://lichuanyang.top/posts/40964/"/>
    <published>2022-01-26T07:19:32.000Z</published>
    <summary>创业公司低成本使用 Kubernetes 的实践经验，从项目创建到部署监控的全流程开源方案。</summary>
    <title>java项目低学习成本使用kubernetes的实践经验</title>
    <updated>2026-06-27T03:44:21.600Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="感悟" scheme="https://lichuanyang.top/tags/%E6%84%9F%E6%82%9F/"/>
    <content>
      <![CDATA[<p>2021年对于我个人来说可以说是剧变的一年，原本准备长期发展的，工作舒适度和前景都非常高的工作，因为某些原因突然崩塌。随之而来的，关于未来往哪走，也到了一个重要的岔路口上。至少到目前为止，感觉还是做出了一些正确的选择。对于年初定下的一些个人成长上的目标，也有了不错的进展。所以，我还是带着不错的心情来做这个年终总结的。</p><span id="more"></span><h2 id="职业回顾"><a href="#职业回顾" class="headerlink" title="职业回顾"></a>职业回顾</h2><p>2021年里，最大的变化还是来自工作，由于所在的行业一夜之间直接在风口上消失，原本在一家公司长期发展的愿望落空，所以我也被迫重新开始了新的职业规划。一个关键的考量点就是要去大公司还是小公司。我在前边为什么说自己走到了一个重要的岔路口上，其实最关键的一点原因就是感觉无论做哪个选择，在这个年龄其实都不会有什么回头路了。大公司和小公司对一个资深员工的能力要求，差别实际上是非常大的。到了这个阶段，不会再有刚毕业那几年随时大小公司左右横跳的机会。</p><p>为了做这个决策，我问了自己几个问题。</p><p>在大公司，个人能力会比在小公司成长的更快吗？</p><p>在大公司，生活是否会比小公司更舒适？</p><p>我在将来是否还需要一份新大公司经历做背书？</p><p>在大公司能获得更多的经济收入吗？</p><p>经过慎重的考虑，对这几个问题的回答其实都是否。个人成长看个人，我实质上不是一个会受环境影响太大的人。对于work life balance的问题，按理说其实和公司大小没太大的关系，只是凑巧国内的几个大公司这方面都不太行。背书的问题，因为我本来也有大厂的经历，包括综合学历和之前的其他经历，我觉得是不太有可能在这方面吃亏的，另外，根据这一波找工作，以及这两年面试别人的感受，我也会觉得项目本身的含金量，会比在哪个公司更重要一些。至于收入，看大环境、看公司的变化情况、看老板风格、看运气，总之不会和公司大小有太大的关系。</p><p>想来想去，感觉去大厂的唯一作用，就是说出去面子上好看一些。所以，也就把这个决定做下了，就是彻底放弃大公司的想法，而是去寻找一些氛围好、前景优良的创业公司机会。说到这儿，有时候我其实非常感激上天的眷顾，仿佛她总会在我一些关键节点上适时的给我抛出一些合适的选项。当然实际上主要是感谢猎头小姐姐提供的信息和整个过程中的帮助，让我来到了一个各方面都非常满意的新公司。在新公司也快4个月了，这段时间包括做事的空间、个人成长、WLB、经济回报，其实都是高于预期的。</p><h2 id="生活变化"><a href="#生活变化" class="headerlink" title="生活变化"></a>生活变化</h2><p>在新的工作时间下，也有了更多的时间陪女儿。小姑娘最近也处在一个语言的爆发期，基本每天都会说新的话，生活的幸福感也到了一个非常高的阶段。</p><h2 id="技术成长"><a href="#技术成长" class="headerlink" title="技术成长"></a>技术成长</h2><p>另外，我每年初都会定一些个人成长上的目标。2021年初，我的关键目标其实就是扩大和巩固自己的能力圈，争取掌握一些新的技能，并巩固已有的技能。具体来说包括本职工作相关的技术和管理能力提升，提升商业认知和投资能力，以及建立一些底层的同样技能。</p><p>本职工作上，阅读了周志明老师的《凤凰架构》这本书，这本书的关键作用是帮助我建立起了更完善的知识体系，也补全了一些知识上的盲区。学习了极客时间上王争小哥的设计模式专栏，对设计模式有了更深刻的理解，也在公司内做了一次设计模式主题的分享。此外，对于用了很久都没太认真了解过的elasticsearch, 做了一次集中的学习。</p><p>对于管理，今年内学习了一些理论知识，也在设法将理论知识运用到实际工作中，感觉还不错。</p><p>投资上，今年实际上做了一些非常错误的尝试。今年初开始，我在试图自己去分析公司的基本面，从而找到合适的投资标的，读了很多书，也看了很多大V的经验。我的目前结论是，这个事情的门槛是非常高的，通过自己的学习去达到专业人士的投资效果，是不现实的。基于此次尝试，我的投资收益在今年第一次变成负数。而在未来，我也会放弃个股投资，转向彻底的基金和指数定投。但是我倒不会停止对商业和投资知识的学习，因为我感受到了这些知识对于我思维方式的转变，实际上是会有不错的收获的。</p><p>关于底层能力，我今年着重培养的是”清晰力“这么一个能力。这个概念来自于《认知觉醒》这本书，意思是说我们的恐惧很多时候是“模糊”，即没有想明白要面对的究竟是个什么事。而一旦能对事情有清晰的认知，会发现事情并没有那么难。当第一次看到这个概念的时候，我立刻就感觉到我自己一直以来在这方面上问题很大，所以就着手在这方面上进行刻意的练习。效果也很不错，无论是工作还是生活上的事，处理起来都感觉得心应手了很多。</p><h2 id="2022-展望"><a href="#2022-展望" class="headerlink" title="2022 展望"></a>2022 展望</h2><p>总而言之，这一年对我来说，是一个关键的抉择之年，也在向着人生的目标稳步前行。感谢所有曾经帮助过我的人。希望2022也继续加油。</p><p>原文地址: <a href="https://lichuanyang.top/posts/2345/">https://lichuanyang.top/posts/2345/</a></p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-中年工程师的出路在哪？"><a href="#Q-中年工程师的出路在哪？" class="headerlink" title="Q: 中年工程师的出路在哪？"></a>Q: 中年工程师的出路在哪？</h3><p>出路不在公司大小，而在你的能力圈。到了职业中后期，比”去哪个公司”更关键的是：你对什么领域有独到见解、你能解决什么级别的技术问题、你的经验是否形成了可迁移的方法论。作者的选择——放弃大厂光环，去寻找氛围好、前景优良的创业公司——给出了一条可行的路径。但路径不是唯一的，关键是主动思考你要什么，而不是被动等机会砸下来。</p><h3 id="Q-技术人如何应对行业变化？"><a href="#Q-技术人如何应对行业变化？" class="headerlink" title="Q: 技术人如何应对行业变化？"></a>Q: 技术人如何应对行业变化？</h3><p>三个字：能力圈。行业风口可能一夜消失（如作者经历的教培行业），但如果你平时就在持续巩固和扩大自己的能力圈，就拥有了在不同行业间迁移的资本。具体来说：保持技术敏感度（读经典书籍如《凤凰架构》）、培养底层可迁移能力（如”清晰力”）、不要把职业安全感绑定在某一家公司或某一个行业上。</p><h3 id="Q-大公司还是小公司更适合资深工程师？"><a href="#Q-大公司还是小公司更适合资深工程师？" class="headerlink" title="Q: 大公司还是小公司更适合资深工程师？"></a>Q: 大公司还是小公司更适合资深工程师？</h3><h2 id="这取决于你现阶段最需要什么。大公司提供平台背书和资源规模，但个人成长速度和-WLB-未必理想；小公司提供更大的做事空间和更快的决策节奏，但稳定性相对较低。作者对比后的结论是：个人成长靠自己，不靠公司大小；大厂经历已有的话，背书需求不大；收入更多看大环境和个人运气。核心建议是——想清楚自己现阶段的选择标准，再做决策，因为到了这个阶段，不太会有回头路。"><a href="#这取决于你现阶段最需要什么。大公司提供平台背书和资源规模，但个人成长速度和-WLB-未必理想；小公司提供更大的做事空间和更快的决策节奏，但稳定性相对较低。作者对比后的结论是：个人成长靠自己，不靠公司大小；大厂经历已有的话，背书需求不大；收入更多看大环境和个人运气。核心建议是——想清楚自己现阶段的选择标准，再做决策，因为到了这个阶段，不太会有回头路。" class="headerlink" title="这取决于你现阶段最需要什么。大公司提供平台背书和资源规模，但个人成长速度和 WLB 未必理想；小公司提供更大的做事空间和更快的决策节奏，但稳定性相对较低。作者对比后的结论是：个人成长靠自己，不靠公司大小；大厂经历已有的话，背书需求不大；收入更多看大环境和个人运气。核心建议是——想清楚自己现阶段的选择标准，再做决策，因为到了这个阶段，不太会有回头路。"></a>这取决于你现阶段最需要什么。大公司提供平台背书和资源规模，但个人成长速度和 WLB 未必理想；小公司提供更大的做事空间和更快的决策节奏，但稳定性相对较低。作者对比后的结论是：个人成长靠自己，不靠公司大小；大厂经历已有的话，背书需求不大；收入更多看大环境和个人运气。核心建议是——想清楚自己现阶段的选择标准，再做决策，因为到了这个阶段，不太会有回头路。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/2345/</id>
    <link href="https://lichuanyang.top/posts/2345/"/>
    <published>2022-01-04T13:49:57.000Z</published>
    <summary>2021 年终总结：行业剧变下的职业转折、个人成长目标的进展，以及对未来的思考。</summary>
    <title>剧变中的2021-一个中年工程师的年终总结</title>
    <updated>2026-06-27T02:31:54.492Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="云原生" scheme="https://lichuanyang.top/categories/%E4%BA%91%E5%8E%9F%E7%94%9F/"/>
    <category term="云原生" scheme="https://lichuanyang.top/tags/%E4%BA%91%E5%8E%9F%E7%94%9F/"/>
    <category term="持续集成" scheme="https://lichuanyang.top/tags/%E6%8C%81%E7%BB%AD%E9%9B%86%E6%88%90/"/>
    <category term="金丝雀" scheme="https://lichuanyang.top/tags/%E9%87%91%E4%B8%9D%E9%9B%80/"/>
    <content>
      <![CDATA[<p>金丝雀发布其实是一种非常适合在云原生体系下进行的流程，因此相信很多人有在kubernetes下做金丝雀发布的需求。通过本文，我们就介绍一种非常简单的做金丝雀发布的思路。</p><span id="more"></span><h2 id="什么是金丝雀发布"><a href="#什么是金丝雀发布" class="headerlink" title="什么是金丝雀发布"></a>什么是金丝雀发布</h2><p>首先介绍一下金丝雀发布是什么，金丝雀这个名字，起源是，矿井工人发现，金丝雀对瓦斯气体很敏感，矿工会在下井之前，先放一只金丝雀到井中，如果金丝雀不叫了，就代表瓦斯浓度高。</p><p>在系统层面的具体含义就是，发布开始后，先启动一个新版本应用，但是并不直接将流量切过来，而是测试人员对新版本进行线上测试，启动的这个新版本应用，就是我们的金丝雀。在金丝雀上测试没有问题后，再将线上的流量切换到新版本上。</p><p>再具体点讲，可以将一个域名映射到两组服务器上，一组是正式的线上环境，另一组就可以是金丝雀环境了。</p><p>假如说我们不是用k8s做部署，而是直接部署到多台服务器上的话，金丝雀部署就很简单，任意指定一台或多台机器做金丝雀即可。</p><p>而在k8s环境下，由于k8s接管了部署的流程，我们就需要做一些别的事情来实现这个效果。</p><h2 id="K8s-环境下的金丝雀发布方案"><a href="#K8s-环境下的金丝雀发布方案" class="headerlink" title="K8s 环境下的金丝雀发布方案"></a>K8s 环境下的金丝雀发布方案</h2><p>有一种很简单的思路，就是创建两个deployment, 但是通过label关联到同一个service上，就可以将同一个service的流量分配到两组容器中了。两个deployment可以分别部署，也就可以部署不同版本的镜像了。</p><h2 id="部署配置示例"><a href="#部署配置示例" class="headerlink" title="部署配置示例"></a>部署配置示例</h2><p>例如下边两个deployment</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">apps/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Deployment</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">nginx-deployment</span></span><br><span class="line">  <span class="attr">labels:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">replicas:</span> <span class="number">3</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">matchLabels:</span></span><br><span class="line">      <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">  <span class="attr">template:</span></span><br><span class="line">    <span class="attr">metadata:</span></span><br><span class="line">      <span class="attr">labels:</span></span><br><span class="line">        <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">    <span class="attr">spec:</span></span><br><span class="line">      <span class="attr">containers:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">nginx</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">nginx:1.7.9</span></span><br><span class="line"><span class="meta">---</span></span><br><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Service</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">nginx-service</span></span><br><span class="line">  <span class="attr">labels:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">  <span class="attr">ports:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">nginx-port</span></span><br><span class="line">    <span class="attr">protocol:</span> <span class="string">TCP</span></span><br><span class="line">    <span class="attr">port:</span> <span class="number">80</span></span><br><span class="line">    <span class="attr">nodePort:</span> <span class="number">32600</span></span><br><span class="line">    <span class="attr">targetPort:</span> <span class="number">80</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">NodePort</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">apps/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Deployment</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">nginx-deployment-canary</span></span><br><span class="line">  <span class="attr">labels:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">    <span class="attr">track:</span> <span class="string">canary</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">replicas:</span> <span class="number">1</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">matchLabels:</span></span><br><span class="line">      <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">      <span class="attr">track:</span> <span class="string">canary</span></span><br><span class="line">  <span class="attr">template:</span></span><br><span class="line">    <span class="attr">metadata:</span></span><br><span class="line">      <span class="attr">labels:</span></span><br><span class="line">        <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">        <span class="attr">track:</span> <span class="string">canary</span></span><br><span class="line">    <span class="attr">spec:</span></span><br><span class="line">      <span class="attr">containers:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">nginx</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">nginx:1.8.0</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>只在第一个deployment中配置service，其配置的selector规则是app:nginx, 然后两个deployment都打上app:nginx的label.</p><p>这样就实现了我们所说的效果。</p><h2 id="局限性"><a href="#局限性" class="headerlink" title="局限性"></a>局限性</h2><p>当然，这种方式只是实现了一种非常简单的金丝雀发布流程，是没有办法做更精细，比如说按用户信息去灰度的规则的。对于同一个用户，也有可能一次请求到了金丝雀中，下一次请求又到了正式环境中。假如需要更加精细的灰度规则，可以考虑采用spring cloud, istio等工具。</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-金丝雀发布和蓝绿部署、滚动更新有什么区别？"><a href="#Q-金丝雀发布和蓝绿部署、滚动更新有什么区别？" class="headerlink" title="Q: 金丝雀发布和蓝绿部署、滚动更新有什么区别？"></a>Q: 金丝雀发布和蓝绿部署、滚动更新有什么区别？</h3><ul><li><strong>滚动更新</strong>：逐批替换旧 Pod，新版本逐步上线。最简单，但新旧版本短暂共存期间流量混合，回滚只能反向逐批操作。</li><li><strong>蓝绿部署</strong>：准备一套完整的”绿色”新环境，测试通过后一次性切换流量。切换快、回滚快，但需要双倍资源。</li><li><strong>金丝雀发布</strong>：先放一小部分流量到新版本（金丝雀），验证无误后逐步扩大。最安全，但需要更精细的流量控制能力。</li></ul><p>三种方式不是互斥的：金丝雀可以看作蓝绿的”渐进版”，实际项目中常常组合使用。</p><h3 id="Q-这种双-Deployment-方案的流量能精确控制吗？"><a href="#Q-这种双-Deployment-方案的流量能精确控制吗？" class="headerlink" title="Q: 这种双 Deployment 方案的流量能精确控制吗？"></a>Q: 这种双 Deployment 方案的流量能精确控制吗？</h3><p>不能精确控制百分比。Service 通过 kube-proxy 的 iptables&#x2F;ipvs 规则做负载均衡，默认是轮询，分配到两个 Deployment 的 Pod 数量比例大致等于 replica 数量比。如果你部署 3 个旧版本 Pod + 1 个新版本 Pod，大约 25% 流量到新版本——但这只是统计近似，不是精确控制。</p><p>如果需要精确的流量百分比（比如严格 10% 到金丝雀），需要借助 Ingress Controller（如 Nginx Ingress 的 canary 注解）或 Service Mesh（如 Istio 的 VirtualService）。</p><h3 id="Q-如果想按用户维度做灰度怎么办？"><a href="#Q-如果想按用户维度做灰度怎么办？" class="headerlink" title="Q: 如果想按用户维度做灰度怎么办？"></a>Q: 如果想按用户维度做灰度怎么办？</h3><p>本文介绍的方案是基于<strong>流量百分比</strong>的简单灰度，无法按用户维度（比如 VIP 用户走新版本、内部员工走新版本）做路由。如果需要按用户维度灰度，推荐：</p><ul><li><strong>Istio &#x2F; Linkerd</strong>：通过 HTTP Header（如 Cookie、User-Agent）做路由规则，可以做到”userId&#x3D;xxx 的请求走金丝雀”</li><li><strong>Spring Cloud Gateway</strong>：在网关层根据请求参数做路由</li><li><strong>Nginx Ingress canary-by-header</strong>：基于特定 Header 值路由到 canary 服务</li></ul><p>原文地址: <a href="https://lichuanyang.top/posts/30764/">https://lichuanyang.top/posts/30764/</a></p>]]>
    </content>
    <id>https://lichuanyang.top/posts/30764/</id>
    <link href="https://lichuanyang.top/posts/30764/"/>
    <published>2021-12-23T10:55:20.000Z</published>
    <summary>在 Kubernetes 环境下实现金丝雀发布的简单思路，通过逐步引流新版本来降低发布风险。</summary>
    <title>kubernetes环境下做金丝雀发布的一种思路</title>
    <updated>2026-06-27T02:31:54.495Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="云原生" scheme="https://lichuanyang.top/categories/%E4%BA%91%E5%8E%9F%E7%94%9F/"/>
    <category term="监控" scheme="https://lichuanyang.top/tags/%E7%9B%91%E6%8E%A7/"/>
    <category term="云原生" scheme="https://lichuanyang.top/tags/%E4%BA%91%E5%8E%9F%E7%94%9F/"/>
    <category term="prometheus" scheme="https://lichuanyang.top/tags/prometheus/"/>
    <category term="grafana" scheme="https://lichuanyang.top/tags/grafana/"/>
    <category term="prometheus教程" scheme="https://lichuanyang.top/tags/prometheus%E6%95%99%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>作为云原生体系下的“默认”监控系统，prometheus正在获得越来越广泛的关注。今天，我们就写一篇教程，讲一下prometheus的设计理念，看看它是如何用非常简单的设计支撑起如此复杂的功能的。</p><span id="more"></span><p>首先，我们来思考一下，如果要做一个类似prometheus的监控系统，都有哪些难点，比如</p><ul><li>每个服务的监控需求都不一样，那么对于监控系统来说，要怎么设计其数据模型，才能取得易用性和通用性之间的平衡</li><li>大量的数据量要如何存储</li><li>怎样能实现各种复杂的报表</li><li>…</li></ul><p>带着这些问题，我们就来看看prometheus是怎么设计的。</p><h2 id="历史"><a href="#历史" class="headerlink" title="历史"></a>历史</h2><p>让我们先从历史说起，prometheus最早由SoundCloud开发，后来捐赠到开源社区。在2016年假如CNCF, 即云原生计算基金会。Prometheus是CNCF的第二个项目，仅次于kubernets。 因此，可想而知，promethous在整个云原生体系中有多么重要的作用。Prometheus也逐渐成了云原生下监控系统的事实标准。</p><h2 id="核心设计理念"><a href="#核心设计理念" class="headerlink" title="核心设计理念"></a>核心设计理念</h2><p>对于一个监控系统来说，核心要解决的问题其实就三个：</p><ol><li>监控指标用什么形式表示</li><li>怎么收集和存储指标</li><li>怎么利用指标生成报表</li></ol><p>对于这三个问题，prometheus都给出了很巧妙的解决方案。</p><h2 id="数据模型"><a href="#数据模型" class="headerlink" title="数据模型"></a>数据模型</h2><p>romethous的数据模型，简而言之，就是一个「时序」的 Metric数据。所谓metric, 就是数据的测量值，而所谓时序，就是这些metric, 会源源不断的产生不同时间点的数据。</p><p>Metric有唯一的名称标识，也可以设置多个label, 可以用于过滤和聚合，其格式如下。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&lt;metric name&gt;&#123;&lt;label name&gt;=&lt;label value&gt;, ...&#125;</span><br></pre></td></tr></table></figure><p>这样，对于任何业务，我们都可以将监控数据设计成统一的metric格式。这样对于promethous来说，方案可以足够简单，只用处理这一种数据格式就可以。而同时又足以方便的应对千变万化的业务场景。</p><p>Prometheus提供了 counter, gauge, histogram, summary 四种核心的metric, 不过其区别仅体现在client端和promQL中。截至目前(2021.11)， 不同的metric 类型在 prometheus server 这一侧并不会有什么区别，</p><h2 id="数据收集和存储"><a href="#数据收集和存储" class="headerlink" title="数据收集和存储"></a>数据收集和存储</h2><p>Prometheus server会定时从要监控的服务暴露出的http接口上抓取数据，是一种典型的拉模型。</p><p>相对推模型，拉模型会有一些好处，比如更容易监测某一个节点是否正常；更容易本地调试等。当然，对于一个监控系统来说，采用推还是拉，其实并不是一个主要问题。</p><p>Prometheus的数据是典型的时序数据，prometheus本身会将数据存储在本地磁盘上。要注意的是，本地存储不可复制，无法构建集群，如果本地磁盘或节点出现故障，存储将无法扩展和迁移。因此一般只能把本地存储视为近期数据的短暂滑动窗口。</p><p>而关于持久化存储的问题，prometheus实际上并没有试图解决。它的做法是定义出标准的读写接口，从而可以将数据存储到任意一个第三方存储上。</p><h2 id="生成报表"><a href="#生成报表" class="headerlink" title="生成报表"></a>生成报表</h2><p>Prometheus定义了功能强大的promQL, 可以满足各种复杂的查询场景，具体可参考 <a href="https://prometheus.io/docs/prometheus/latest/querying/basics/">https://prometheus.io/docs/prometheus/latest/querying/basics/</a></p><h2 id="周边生态"><a href="#周边生态" class="headerlink" title="周边生态"></a>周边生态</h2><p>一个开源项目的发展，当然离不开周边生态的发展。而prometheus目前已经有了很完善的生态，在java, go, python等主流的开发语言下，都有完善的client包可以使用； 像spring中，可以很容易的为多种组件增加打点，这一点，在下边的实战环节我们会细讲；在kubernetes中，可以轻易的配置自动去各个节点抓取prometheus数据；借助grafana等工具，也可以配置出多种多样的报表。</p><h2 id="实战"><a href="#实战" class="headerlink" title="实战"></a>实战</h2><p>教程的接下来一部分，我们会以springboot项目为例，来看一看prometheus的实际效果。</p><p>其核心思路就是使用spring-actuator 为springboot应用配置监控，并以promethous的结构暴露出来。</p><p>首先，引入依赖</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">implementation(&quot;org.springframework.boot:spring-boot-starter-actuator&quot;)</span><br><span class="line">implementation(&quot;io.micrometer:micrometer-registry-prometheus&quot;)</span><br></pre></td></tr></table></figure><p>然后添加spring配置</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">management:</span><br><span class="line">  endpoints:</span><br><span class="line">    web:</span><br><span class="line">      exposure:</span><br><span class="line">        include: &quot;prometheus&quot;</span><br><span class="line">  metrics:</span><br><span class="line">    distribution:</span><br><span class="line">      sla:</span><br><span class="line">        http:</span><br><span class="line">          server:</span><br><span class="line">            requests: &quot;100ms,150ms,250ms,500ms,1s&quot;</span><br><span class="line">      percentiles-histogram:</span><br><span class="line">        http:</span><br><span class="line">          server:</span><br><span class="line">            requests: true</span><br><span class="line">    web:</span><br><span class="line">      server:</span><br><span class="line">        request:</span><br><span class="line">          autotime:</span><br><span class="line">            enabled: true</span><br><span class="line">    export:</span><br><span class="line">      prometheus:</span><br><span class="line">        enabled: true</span><br><span class="line">    tags:</span><br><span class="line">      application: name</span><br></pre></td></tr></table></figure><p>这个配置里，其实做了几件事：将数据以prometheus的格式暴露出来；自动为http请求添加histogram监控；增加一个application标识，这个标识会作为一个label出现在所有metric中。</p><p>之后，启动springboot项目，并且访问&#x2F;actuator&#x2F;prometheus路径，就可以看到大量metric, 比如</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"># HELP executor_pool_size_threads The current number of threads in the pool</span><br><span class="line"># TYPE executor_pool_size_threads gauge</span><br><span class="line">executor_pool_size_threads&#123;application=&quot;ads-programad&quot;,name=&quot;asyncExecutor&quot;,&#125; 0.0</span><br><span class="line"># HELP tomcat_servlet_request_seconds  </span><br><span class="line"># TYPE tomcat_servlet_request_seconds summary</span><br><span class="line">tomcat_servlet_request_seconds_count&#123;application=&quot;ads-programad&quot;,name=&quot;dispatcherServlet&quot;,&#125; 1.0</span><br><span class="line">tomcat_servlet_request_seconds_sum&#123;application=&quot;ads-programad&quot;,name=&quot;dispatcherServlet&quot;,&#125; 0.0</span><br><span class="line"># HELP executor_pool_core_threads The core number of threads for the pool</span><br><span class="line"># TYPE executor_pool_core_threads gauge</span><br><span class="line">executor_pool_core_threads&#123;application=&quot;ads-programad&quot;,name=&quot;asyncExecutor&quot;,&#125; 70.0</span><br><span class="line"># HELP jvm_classes_unloaded_classes_total The total number of classes unloaded since the Java virtual machine has started execution</span><br><span class="line"># TYPE jvm_classes_unloaded_classes_total counter</span><br><span class="line">jvm_classes_unloaded_classes_total&#123;application=&quot;ads-programad&quot;,&#125; 0.0</span><br><span class="line"># HELP executor_completed_tasks_total The approximate total number of tasks that have completed execution</span><br><span class="line"># TYPE executor_completed_tasks_total counter</span><br><span class="line">executor_completed_tasks_total&#123;application=&quot;ads-programad&quot;,name=&quot;asyncExecutor&quot;,&#125; 0.0</span><br><span class="line"># HELP tomcat_threads_config_max_threads  </span><br><span class="line"># TYPE tomcat_threads_config_max_threads gauge</span><br><span class="line">tomcat_threads_config_max_threads&#123;application=&quot;ads-programad&quot;,name=&quot;http-nio-9000&quot;,&#125; 500.0</span><br><span class="line"># HELP process_cpu_usage The &quot;recent cpu usage&quot; for the Java Virtual Machine process</span><br><span class="line"># TYPE process_cpu_usage gauge</span><br><span class="line">process_cpu_usage&#123;application=&quot;ads-programad&quot;,&#125; 0.0</span><br><span class="line"># HELP tomcat_sessions_active_current_sessions  </span><br><span class="line"># TYPE tomcat_sessions_active_current_sessions gauge</span><br><span class="line">tomcat_sessions_active_current_sessions&#123;application=&quot;ads-programad&quot;,&#125; 0.0</span><br><span class="line"># HELP jvm_memory_committed_bytes The amount of memory in bytes that is committed for the Java virtual machine to use</span><br><span class="line"># TYPE jvm_memory_committed_bytes gauge</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;heap&quot;,id=&quot;G1 Eden Space&quot;,&#125; 3.5651584E7</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;heap&quot;,id=&quot;G1 Old Gen&quot;,&#125; 4.6137344E7</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;nonheap&quot;,id=&quot;Compressed Class Space&quot;,&#125; 5767168.0</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;nonheap&quot;,id=&quot;CodeHeap &#x27;non-profiled nmethods&#x27;&quot;,&#125; 8847360.0</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;nonheap&quot;,id=&quot;CodeHeap &#x27;non-nmethods&#x27;&quot;,&#125; 2555904.0</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;nonheap&quot;,id=&quot;Metaspace&quot;,&#125; 4.2287104E7</span><br><span class="line">jvm_memory_committed_bytes&#123;application=&quot;ads-programad&quot;,area=&quot;heap&quot;,id=&quot;G1 Survivor Space&quot;,&#125; 4194304.0</span><br><span class="line"># HELP tomcat_servlet_request_max_seconds  </span><br><span class="line"># TYPE tomcat_servlet_request_max_seconds gauge</span><br><span class="line">tomcat_servlet_request_max_seconds&#123;application=&quot;ads-programad&quot;,name=&quot;dispatcherServlet&quot;,&#125; 0.0</span><br><span class="line"># HELP tomcat_connections_current_connections  </span><br><span class="line"># TYPE tomcat_connections_current_connections gauge</span><br><span class="line">tomcat_connections_current_connections&#123;application=&quot;ads-programad&quot;,name=&quot;http-nio-9000&quot;,&#125; 3.0</span><br><span class="line"># HELP tomcat_sessions_active_max_sessions  </span><br><span class="line"># TYPE tomcat_sessions_active_max_sessions gauge</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>其中，除了我们显式配置的http监控，其实还有大量的jvm, 机器负载等基础的监控信息。</p><p>除此之外，对于其他组件的监控也很容易添加，诸如线程池、http连接池、自定义监控等，可以参考 <a href="https://github.com/lcy362/springboot-prometheus-demo">https://github.com/lcy362/springboot-prometheus-demo</a></p><p>这样，无论这个springboot项目如何部署，无论是用java原生的部署，还是用docker部署，还是部署在kubernetes上，都可以非常容易的获取各个监控metrics数据。</p><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><h3 id="Step-1-安装和配置-Prometheus"><a href="#Step-1-安装和配置-Prometheus" class="headerlink" title="Step 1: 安装和配置 Prometheus"></a>Step 1: 安装和配置 Prometheus</h3><p>首先在服务器上下载并安装 Prometheus，编辑 <code>prometheus.yml</code> 配置文件，设置 global 抓取间隔和 evaluation interval 等基本参数。启动 Prometheus 后，访问 <code>http://localhost:9090</code> 即可打开 Web UI 界面。</p><h3 id="Step-2-配置数据采集目标"><a href="#Step-2-配置数据采集目标" class="headerlink" title="Step 2: 配置数据采集目标"></a>Step 2: 配置数据采集目标</h3><p>在配置文件的 <code>scrape_configs</code> 中定义需要监控的目标，包括你的应用暴露的 <code>/actuator/prometheus</code> 端点地址。通过 Spring Actuator 和 Micrometer 将 Spring Boot 应用的指标以 Prometheus 格式暴露出来，Prometheus 会按配置的间隔自动拉取数据。</p><h3 id="Step-3-编写-PromQL-查询"><a href="#Step-3-编写-PromQL-查询" class="headerlink" title="Step 3: 编写 PromQL 查询"></a>Step 3: 编写 PromQL 查询</h3><p>利用 Prometheus 内置的 PromQL 查询语言，可以对收集到的时序数据进行灵活查询。你可以按 metric 名称和 label 进行过滤、聚合、计算速率等操作，满足各种监控场景的分析需求。</p><h3 id="Step-4-对接-Grafana-可视化"><a href="#Step-4-对接-Grafana-可视化" class="headerlink" title="Step 4: 对接 Grafana 可视化"></a>Step 4: 对接 Grafana 可视化</h3><p>将 Prometheus 作为数据源添加到 Grafana 中，利用 Grafana 丰富的面板和仪表盘功能，将监控数据以图表等可视化形式呈现。通过 Grafana 还可以设置告警规则，在指标异常时及时通知。</p><p>原文地址: <a href="https://lichuanyang.top/posts/28288/">https://lichuanyang.top/posts/28288/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/28288/</id>
    <link href="https://lichuanyang.top/posts/28288/"/>
    <published>2021-11-10T11:54:05.000Z</published>
    <summary>Prometheus 监控系统教程，从设计理念出发讲清楚它如何用简单设计支撑复杂功能。</summary>
    <title>prometheus教程： 一篇文章讲懂prometheus</title>
    <updated>2026-06-27T03:42:25.686Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="开源项目" scheme="https://lichuanyang.top/tags/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/"/>
    <content>
      <![CDATA[<p>一个高性能的 Java IP 地址归属国家查询工具，核心思路是用二分查找在预加载的 IP 段数组中定位，单次查询延迟在微秒级。</p><span id="more"></span><h2 id="设计思路"><a href="#设计思路" class="headerlink" title="设计思路"></a>设计思路</h2><p>IP 地址本质上是一个 32 位整数。所谓”IP 段”就是一段连续的整数范围。给定一个 IP，我们要找到它落在哪个 IP 段中——这是一个典型的<strong>区间查找</strong>问题。</p><p>最直接的做法是遍历所有 IP 段，O(n) 复杂度。但全球 IP 段有几十万个，遍历太慢。更好的做法是把 IP 段的起始地址排序后放入数组，用<strong>二分查找</strong>定位，O(log n) 复杂度，几十万个段只需要约 19 次比较。</p><h2 id="数据源"><a href="#数据源" class="headerlink" title="数据源"></a>数据源</h2><p>IP 地址库数据可以从 <a href="http://download.ip2location.com/lite/">IP2Location Lite</a> 免费获取。数据格式示例：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">16781312,JP</span><br><span class="line">16785408,CN</span><br><span class="line">16793600,JP</span><br></pre></td></tr></table></figure><p>每行包含两个字段：IP 段的<strong>起始地址</strong>（转换后的整数）和<strong>国家代号</strong>。</p><p>原始数据其实还包含 IP 段的结束地址，但 IP2Location 的数据段是<strong>连续无间隙</strong>的——一个段的结束地址恰好是下一个段的起始地址减一。因此我们可以只存储起始地址，结束地址自然而然地由下一个段的起始地址确定。这个优化节省了一半的内存。</p><h2 id="数据结构"><a href="#数据结构" class="headerlink" title="数据结构"></a>数据结构</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">IpCountryLookup</span> &#123;</span><br><span class="line">    <span class="comment">// IP 段的起始地址（有序）</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">long</span>[] startIps;</span><br><span class="line">    <span class="comment">// 对应的国家代号</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String[] countryCodes;</span><br><span class="line">    <span class="comment">// 总段数</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> size;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>两个并行数组：<code>startIps[i]</code> 和 <code>countryCodes[i]</code> 一一对应。二分查找在 <code>startIps</code> 上进行，找到后从 <code>countryCodes</code> 中取结果。</p><h2 id="核心实现：二分查找"><a href="#核心实现：二分查找" class="headerlink" title="核心实现：二分查找"></a>核心实现：二分查找</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> String <span class="title function_">lookup</span><span class="params">(String ip)</span> &#123;</span><br><span class="line">    <span class="type">long</span> <span class="variable">ipLong</span> <span class="operator">=</span> ipToLong(ip);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 二分查找：找最后一个 &lt;= ipLong 的起始地址</span></span><br><span class="line">    <span class="type">int</span> <span class="variable">left</span> <span class="operator">=</span> <span class="number">0</span>, right = size - <span class="number">1</span>;</span><br><span class="line">    <span class="keyword">while</span> (left &lt;= right) &#123;</span><br><span class="line">        <span class="type">int</span> <span class="variable">mid</span> <span class="operator">=</span> left + (right - left) / <span class="number">2</span>;</span><br><span class="line">        <span class="keyword">if</span> (startIps[mid] &lt;= ipLong) &#123;</span><br><span class="line">            left = mid + <span class="number">1</span>;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            right = mid - <span class="number">1</span>;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// right 就是最后一个 &lt;= ipLong 的位置</span></span><br><span class="line">    <span class="keyword">if</span> (right &gt;= <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> countryCodes[right];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;Unknown&quot;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// IP 字符串转 long</span></span><br><span class="line"><span class="keyword">private</span> <span class="type">long</span> <span class="title function_">ipToLong</span><span class="params">(String ip)</span> &#123;</span><br><span class="line">    String[] parts = ip.split(<span class="string">&quot;\\.&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> (Long.parseLong(parts[<span class="number">0</span>]) &lt;&lt; <span class="number">24</span>)</span><br><span class="line">         + (Long.parseLong(parts[<span class="number">1</span>]) &lt;&lt; <span class="number">16</span>)</span><br><span class="line">         + (Long.parseLong(parts[<span class="number">2</span>]) &lt;&lt; <span class="number">8</span>)</span><br><span class="line">         + Long.parseLong(parts[<span class="number">3</span>]);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="性能数据"><a href="#性能数据" class="headerlink" title="性能数据"></a>性能数据</h2><ul><li><strong>内存占用</strong>：约 30 万条 IP 段，两个数组各 ~30 万元素，总计约 4 MB</li><li><strong>查询延迟</strong>：单次查询 &lt; 10 微秒（二分查找约 19 次比较）</li><li><strong>QPS</strong>：单机轻松支撑百万级</li><li><strong>数据更新</strong>：IP 库约每月更新一次，定时任务自动拉取并重载</li></ul><h2 id="其他数据源对比"><a href="#其他数据源对比" class="headerlink" title="其他数据源对比"></a>其他数据源对比</h2><table><thead><tr><th>数据源</th><th>精度</th><th>免费版</th><th>特点</th></tr></thead><tbody><tr><td>IP2Location Lite</td><td>国家</td><td>✓</td><td>数据干净，格式统一</td></tr><tr><td>GeoIP2（MaxMind）</td><td>城市</td><td>✓（有限）</td><td>社区认可度高，Java API 完善</td></tr><tr><td>纯真 IP 库</td><td>运营商</td><td>✓</td><td>国内精度最高</td></tr><tr><td>ipip.net</td><td>城市&#x2F;运营商</td><td>✗</td><td>国内最准确，但收费</td></tr></tbody></table><p>如果只需要国家粒度，IP2Location Lite 足够。需要城市精度则推荐 MaxMind GeoLite2。</p><h2 id="部署"><a href="#部署" class="headerlink" title="部署"></a>部署</h2><p>项目基于 Spring Boot，支持两种运行方式：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 直接运行</span></span><br><span class="line">mvn spring-boot:run</span><br><span class="line"></span><br><span class="line"><span class="comment"># Docker</span></span><br><span class="line">docker run -p 8080:8080 lcy362/ip-country</span><br></pre></td></tr></table></figure><p>在线体验：<a href="http://ip-country.lichuanyang.top/">http://ip-country.lichuanyang.top/</a></p><p>项目源码：<a href="https://github.com/lcy362/ip-country">https://github.com/lcy362/ip-country</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/36780/</id>
    <link href="https://lichuanyang.top/posts/36780/"/>
    <published>2021-10-12T11:25:15.000Z</published>
    <summary>用 Java 实现高性能 IP 地址归属国家查询工具，基于 IP2Location 数据源，支持定时更新。</summary>
    <title>实现一个简单的java版本高性能获取ip地址所属国家工具</title>
    <updated>2026-06-27T03:45:23.489Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="iterm2" scheme="https://lichuanyang.top/tags/iterm2/"/>
    <category term="ssh自动登录" scheme="https://lichuanyang.top/tags/ssh%E8%87%AA%E5%8A%A8%E7%99%BB%E5%BD%95/"/>
    <category term="ssh免密码登录" scheme="https://lichuanyang.top/tags/ssh%E5%85%8D%E5%AF%86%E7%A0%81%E7%99%BB%E5%BD%95/"/>
    <content>
      <![CDATA[<h2 id="痛点：频繁-SSH-登录"><a href="#痛点：频繁-SSH-登录" class="headerlink" title="痛点：频繁 SSH 登录"></a>痛点：频繁 SSH 登录</h2><p>如果你像我一样，需要经常性的访问不同的远程服务器，记录服务器的ip和输入密码就是一件非常痛苦的事情。好在，通过在item2中做一些配置，可以很好的解决这个痛点。最终实现的效果，就是类似配置了一些ssh书签，能够在iterm2中记住ssh密码, 实现免密码登录和自动登录的效果。</p><span id="more"></span><h2 id="完整配置步骤"><a href="#完整配置步骤" class="headerlink" title="完整配置步骤"></a>完整配置步骤</h2><p>iterm2 （<a href="https://iterm2.com/">https://iterm2.com/</a>)  是mac下使用非常广泛的一款终端替代产品，提供了很多强大的功能。要实现ssh书签，实现免密码登录、自动登录的效果，关键点是其中的三个特性：profile, trigger 和 password manager.</p><p>profile顾名思义就是一套配置，像我们正常打开iterm2时，其实就是打开了default profile.  配置profile的入口就在工具栏 profiles 选项下，可以增加或编辑现有profile.  我们将需要的profile的general标签下的 commond 模块修改为 Command,  内容填入 ssh命令， 比如 ssh <a href="mailto:&#114;&#x6f;&#111;&#x74;&#64;&#x31;&#46;&#x31;&#x2e;&#49;&#x2e;&#x31;">root@1.1.1.1</a>, 就可以在打开profile时自动执行ssh命令。 profile中其他的文本、颜色等配置都不重要，可以按需填写。</p><p>trigger也是profile的一个特性，入口在profile配置页的advanced标签下，它的作用就是利用关键词触发一个动作，我们现在要做的就是用password这个关键词触发打开 password manager。 操作很简单，就是增加一个trigger, regular expression 填入 password,  action选择 open password manager, 注意勾选instant和enabled两个选项。</p><p>最后一个要配置的是password manager, password manager 就是一个密码管理器，是item2中会默认安装的一款插件，入口在工具栏 window 标签下。打开password manager, 将需要保存的密码都录入进去就可以了。</p><h2 id="安全提醒"><a href="#安全提醒" class="headerlink" title="安全提醒"></a>安全提醒</h2><p>这样，我们就实现了在iterm2中用“书签”保存远程服务器的地址和密码。使用时，直接访问对应的profile, 等待password manager 弹出，选择对应的密码记录，点击输入就可以了。</p><p>原文地址: <a href="https://lichuanyang.top/posts/20763/">https://lichuanyang.top/posts/20763/</a></p><hr><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><h3 id="Step-1-生成SSH密钥"><a href="#Step-1-生成SSH密钥" class="headerlink" title="Step 1: 生成SSH密钥"></a>Step 1: 生成SSH密钥</h3><p>在终端执行 <code>ssh-keygen</code> 生成 SSH 密钥对，将公钥添加到目标服务器的 <code>~/.ssh/authorized_keys</code> 中，确保免密登录基础配置完成。</p><h3 id="Step-2-配置iTerm2-Profile"><a href="#Step-2-配置iTerm2-Profile" class="headerlink" title="Step 2: 配置iTerm2 Profile"></a>Step 2: 配置iTerm2 Profile</h3><p>打开 iTerm2，进入 Profiles → Open Profiles → Edit Profiles。在 General 标签页中将 Command 设置为 <code>ssh root@1.1.1.1</code> 格式的 SSH 命令。</p><h3 id="Step-3-添加书签"><a href="#Step-3-添加书签" class="headerlink" title="Step 3: 添加书签"></a>Step 3: 添加书签</h3><p>在 Profile 编辑页的 Advanced 标签页中添加 Trigger：Regular Expression 填入 <code>password</code>，Action 选择 Open Password Manager，勾选 Instant 和 Enabled。然后在 Window → Password Manager 中录入服务器密码。</p><h3 id="Step-4-验证自动登录"><a href="#Step-4-验证自动登录" class="headerlink" title="Step 4: 验证自动登录"></a>Step 4: 验证自动登录</h3><p>通过 Profiles 菜单选择配置好的 Profile，等待 Password Manager 弹出后选择对应密码记录，确认能自动完成 SSH 登录。</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/20763/</id>
    <link href="https://lichuanyang.top/posts/20763/"/>
    <published>2021-10-08T08:45:42.000Z</published>
    <summary>通过 iTerm2 配置 SSH 书签，实现记住密码和自动登录，告别反复输入 IP 和密码的痛苦。</summary>
    <title>iterm2配置ssh书签, 实现记住密码和自动登录</title>
    <updated>2026-06-27T02:14:57.843Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="技术分享" scheme="https://lichuanyang.top/tags/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/"/>
    <category term="技术分享选题" scheme="https://lichuanyang.top/tags/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB%E9%80%89%E9%A2%98/"/>
    <content>
      <![CDATA[<p>技术分享基本在每个公司都会有。分享的效果因人，因团队而异。在有些团队中，分享会因为种种原因，逐渐沦为一个没什么用的时间杀手。</p><p>写这篇文章，是希望从选题等方面出发，通过一些比较基础的规则，提升技术分享的下限，争取让80%的人都能做出一次80分的技术分享。</p><span id="more"></span><h2 id="选题"><a href="#选题" class="headerlink" title="选题"></a>选题</h2><h3 id="基本原则"><a href="#基本原则" class="headerlink" title="基本原则"></a><strong>基本原则</strong></h3><p>首先要了解到的是，线下的技术分享不是分享知识的唯一形式。其他的，比如写博客写文章，拍视频，或者简单的分享一两句话，都是常见的分享形式。针对不同的知识，要考虑用什么样的方式进行分享更加合适。</p><p>比如，对于某项技术的入门介绍，推荐以文章的形式进行。读者可以进行快速的浏览或者对关键信息进行检索，要比坐下来听一遍效率高很多。</p><h3 id="推荐选题"><a href="#推荐选题" class="headerlink" title="推荐选题"></a><strong>推荐选题</strong></h3><p>通常来说，比较好的选题需要有较强的实用性，能够吸引到听众，且难度适中。能够让听众在听得懂和有新收获之间达到一个平衡。</p><p>举一些比较好的选题例子，以及一些注意事项：</p><ul><li>对于新技术的介绍，需要介绍清楚新技术的产生背景，解决了什么问题，相对之前的类似解决方案有什么优缺点，引入了什么新问题，等；</li><li>对晦涩难懂的技术的讲解，这类选题粒度可以小一些，需要在充分理解问题的基础上有较好的分享技巧，保证大家听的明白；</li><li>比较细节的技术深入讨论，与上一类类似，要注意背景介绍；</li><li>对于一类问题的高度总结，比如cache的一致性、分布式id、分布式事务等，这类话题可以从实际存在的问题切入，分析不同解决方案的优劣，以及技术方案的演进等；</li><li>业务分享，这一类最好是选择常见的业务领域，同时要注意对背景的介绍；</li></ul><h2 id="内容准备"><a href="#内容准备" class="headerlink" title="内容准备"></a>内容准备</h2><p>对于分享效果，虽然会受各方面的影响，但归根结底最重要的还是分享内容中是否有足够的干货。</p><p>为了能够有干货, 最好的方法还是平常的积累，建议平常多思考，对平常工作中用到的东西建立起更深的认知。推荐阅读《卡片笔记写作法》这本书，平时有持续输入，才能有输出产生。</p><p>此外，</p><p>准备过程中注意思考和总结，形成自己的观点。单纯对其他人的东西做搬运，实际上造成的是自己时间的浪费；</p><p>最好能将实际的应用场景融汇到分享内容中；</p><p>比较难的知识点，注意分享技巧，让听众能听明白，可以考虑先就分享的关键部分，先找一两个同事试讲；</p><p>通过看视频，除了可以了解分享内容本身，也可以了解到其他人是怎么讲的，对分享效果比较有帮助。</p><p>除了分享的真正内容外，建议增加以下内容的准备：</p><p>预习资料。可以提前准备一些有助于理解分享内容的资料，比如某本书的某几章，某篇文章等，要求大家提前阅读。分享者需要给出每个材料的预计阅读时间，单次分享整体需要的阅读时间不宜超过1个小时。</p><p>对背景的详细阐述，尤其是涉及到一些业务知识或者不常见的场景时。</p><h2 id="分享流程"><a href="#分享流程" class="headerlink" title="分享流程"></a>分享流程</h2><p>准备好分享内容后，需要找人进行review，reviewer可以是自己的mentor, leader, 或者其他在相关领域非常熟悉的人。</p><p>reviewer在充分理解分享内容的基础上，主要对选题和分享内容进行审核，确保满足本文列举的其他要求。同时尽量保证分享内容不要有技术性的错误。</p><p>分享材料一定要提前发出来，让听众有提前熟悉的机会。</p><p>分享时注意语速控制，不宜过快。</p><p>原文地址: <a href="https://lichuanyang.top/posts/54216/">https://lichuanyang.top/posts/54216/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/54216/</id>
    <link href="https://lichuanyang.top/posts/54216/"/>
    <published>2021-08-31T06:27:43.000Z</published>
    <summary>从选题、结构到演讲技巧，系统性地分享如何做出一次 80 分以上的技术分享。</summary>
    <title>怎样做一个好的技术分享</title>
    <updated>2026-06-08T07:50:49.062Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="云原生" scheme="https://lichuanyang.top/categories/%E4%BA%91%E5%8E%9F%E7%94%9F/"/>
    <category term="微服务" scheme="https://lichuanyang.top/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    <category term="云原生架构" scheme="https://lichuanyang.top/tags/%E4%BA%91%E5%8E%9F%E7%94%9F%E6%9E%B6%E6%9E%84/"/>
    <content>
      <![CDATA[<h2 id="云原生的定义"><a href="#云原生的定义" class="headerlink" title="云原生的定义"></a>云原生的定义</h2><p>近年来，云原生在整个开源社区中正在成为越来越流行的概念。那么云原生究竟是什么？架构？平台？又能影响什么？系统安全？开发效率？所以我们今天就刨根问底，理一下到底什么是云原生。</p><span id="more"></span><p>要理解什么是云原生，就要从云原生的名字说起，云原生的英文名是cloud native, 很显然，其含义就包含云和原生两部分。云就是说应用是运行在云上，而非本地。原生，就是说应用要以最适合云的方式去运行，而不是仅仅从本地迁移到应用上。</p><p>那么，什么样的应用才是适合云的呢？其实就是能最大化利用云的能力，发挥云的优势。</p><p>而云计算的核心优势，其实无非就是将更多的资源集中管理，统一调配，也就更方便按需灵活配置资源，提高资源利用率。</p><p>类比一下，相信很多人用过storm等流式框架，它们的优势是什么呢？其中一个重要的因素就是可以将一个复杂的流程拆解成若干个子节点，每个节点可以根据其需求配置不同的并发度，并发需求高的节点可以获得更多资源。这样，资源利用率也就提升了。</p><h2 id="微服务"><a href="#微服务" class="headerlink" title="微服务"></a>微服务</h2><p>对微服务来说，也是类似，将不同功能拆分成不同的服务，就可以单独对更小粒度的功能单独做扩缩容。</p><p>值得注意的是，拆分不仅包含拆分不同的业务，也包括将业务代码、三方软件（第三方库）、非功能特性（高可用、安全、可观测性等三类代码进行分离。</p><p>单纯业务的拆分，实际上从软件开发的非常早期的阶段就在进行了。而伴随着云原生概念兴起的趋势，正是将云应用中的非业务代码部分进行最大化的剥离，从而让云设施接管应用中原有的大量非功能特性（如弹性、韧性、安全、可观测性、灰度等），也就是所谓的service mesh.</p><p>由于云上的资源和应用并不是强绑定的，为了能更方便的利用资源，我们需要一种更通用的运行形势，让应用可以和运行环境有一定程度的解耦。</p><h2 id="容器化"><a href="#容器化" class="headerlink" title="容器化"></a>容器化</h2><p>这就是容器技术。容器提供了一种逻辑打包机制，以这种机制打包的应用可以脱离其实际运行的环境。利用这种脱离，不管目标环境是私有数据中心、公有云，还是开发者的个人笔记本电脑，您都可以轻松、一致地部署基于容器的应用。容器化使开发者和 IT 运营团队的关注点泾渭分明 - 开发者专注于应用逻辑和依赖项，而 IT 运营团队可以专注于部署和管理，不必为具体的软件版本和应用特有的配置等应用细节分心。</p><h2 id="可观测性"><a href="#可观测性" class="headerlink" title="可观测性"></a>可观测性</h2><p>另一方面，服务更小粒度的拆分之后，系统本身的复杂度显然会有所提升，比如本地调用变成了网络请求，调用链也无法通过代码结构体现。因此，运维上需要更加智能化和自动化，要保障单个服务更强的稳定性；同时需要一个强大的监控系统，要能够分析出各个微服务之间的依赖关系，还要快速检测出系统中的异常。</p><p>同时，在单个服务规模更小，且监控数据很完善的前提下，我们有可能去更频繁的去部署，甚至每次更改之后直接部署到生产环境。如果部署有问题，我们可以通过监控及时的发现，从而将损失控制在更小的程度。而小规模的部署，也让我们更容易去定位问题或者回滚。</p><p>从上边的分析中，我们可以整理出和云原生相关的一些关键词，比如服务化、弹性、可观测、韧性、自动化等等，这些关键词可以被总结成4类，即微服务、DevOps、持续交付和容器化。</p><p>这4类的关键特征如下：</p><p>微服务：可被独立部署、更新、重启、scale</p><p>DevOps:  自动化、快速、开发运维协同</p><p>持续交付：频繁发布、快速反馈</p><p>容器化：逻辑打包机制</p><h2 id="云原生的思维方式"><a href="#云原生的思维方式" class="headerlink" title="云原生的思维方式"></a>云原生的思维方式</h2><p>上边讲了很多理论上的知识，那么，要采用云原生的话，有什么具体的执行路径呢？可以从以下几个方面考虑：</p><ol><li>业务服务的拆分：这是软件开发中非常基础的一件事，拆分需要满足SOLID原则等基础的设计原则。</li><li>完善的监控体系:  包括log, trace, metric, alert几个维度的信息都要收集起来，其中log侧重于记录代码运行过程中的信息，trace主要用于追踪同一请求在不同服务下的流转，metric是针对系统运行状况的监控，alert则是针对异常状态的报警。业界也已经有了很多开源的实现，比如prometheus, jaeger等等</li><li>容器及容器编排： 这一部分基本就是 docker和k8s</li><li>中间件mesh化： 即业务应用中只保留很薄的一层client, 中间件的主要逻辑放在在mesh层</li><li>DevOps和持续交付：这个主要是开发流程、和开发运维协作上的很多流程的事。在云环境下，我们更推崇小批量、频繁发布、快速反馈的模式。</li></ol><p>我是流沙，希望通过这篇文章，可以让大家更清晰的理解云原生究竟是什么。其实，云原生说起来很简单，就是采用各种方式去更好的利用云上的资源。而具体说起来，它又是一套非常庞大的体系，涵盖了从开发到运维的方方面面。欢迎大家关注我的公众号(Mobility) ，或者<a href="https://lichuanyang.top/">个人网站</a>, 我会在后续的文章里逐渐展开讲讲云原生的方方面面。</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-云原生就是上-Kubernetes-吗？"><a href="#Q-云原生就是上-Kubernetes-吗？" class="headerlink" title="Q: 云原生就是上 Kubernetes 吗？"></a>Q: 云原生就是上 Kubernetes 吗？</h3><p>不是。Kubernetes 是云原生体系中容器编排的核心工具，但云原生远不止 K8s。云原生是一整套方法论，包括微服务架构、容器化、DevOps、持续交付、可观测性等多个维度。上了 K8s 不等于就”云原生”了——如果你的应用仍然是单体巨石、没有自动化 CI&#x2F;CD、没有监控告警，那只是”在 K8s 上跑传统应用”而已。</p><h3 id="Q-传统应用有必要迁移到云原生吗？"><a href="#Q-传统应用有必要迁移到云原生吗？" class="headerlink" title="Q: 传统应用有必要迁移到云原生吗？"></a>Q: 传统应用有必要迁移到云原生吗？</h3><p>取决于场景。如果应用规模小、迭代频率低、团队人数少，强行上全套云原生反而增加复杂度。但如果应用需要频繁迭代、弹性伸缩、高可用，或者团队规模大到需要微服务拆分——云原生就能发挥价值。一句话：不是为了云原生而云原生，而是为了解决实际问题。</p><h3 id="Q-容器化和虚拟化有什么区别？"><a href="#Q-容器化和虚拟化有什么区别？" class="headerlink" title="Q: 容器化和虚拟化有什么区别？"></a>Q: 容器化和虚拟化有什么区别？</h3><p>虚拟机（VM）虚拟的是硬件层，每个 VM 有独立的操作系统内核，启动慢、资源开销大。容器虚拟的是操作系统层，所有容器共享宿主机内核，只隔离应用和依赖，启动快（秒级）、资源占用小。可以简单理解：虚拟机是”一栋楼里每家一个厨房”，容器是”共享一个大厨房，每人一个灶台”。</p><h3 id="Q-微服务和云原生是什么关系？"><a href="#Q-微服务和云原生是什么关系？" class="headerlink" title="Q: 微服务和云原生是什么关系？"></a>Q: 微服务和云原生是什么关系？</h3><p>微服务是云原生架构的核心组成部分，但不是全部。微服务解决的是”如何拆分应用以独立部署和扩缩容”，而云原生还包括了这些微服务”如何运行”（容器和编排）、”如何交付”（CI&#x2F;CD）、”如何监控”（可观测性）、”如何通信”（Service Mesh）等。可以把微服务理解为云原生架构的”业务层”。</p><p>原文地址: <a href="https://lichuanyang.top/posts/42843/">https://lichuanyang.top/posts/42843/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/42843/</id>
    <link href="https://lichuanyang.top/posts/42843/"/>
    <published>2021-06-09T11:54:37.000Z</published>
    <summary>从 Cloud Native 的字面含义出发，系统梳理云原生的定义、核心理念和技术生态。</summary>
    <title>云原生究竟是什么</title>
    <updated>2026-06-27T03:44:21.596Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="个人成长" scheme="https://lichuanyang.top/tags/%E4%B8%AA%E4%BA%BA%E6%88%90%E9%95%BF/"/>
    <category term="读书笔记" scheme="https://lichuanyang.top/tags/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"/>
    <content>
      <![CDATA[<p>《干法》是一本讲工作态度、工作方法的经典书籍。在劳资矛盾严重、阶级趋向固化的今天，是很难完全向该书作者稻盛和夫先生一样，去践行这一套理念的。但这不代表这本书就没有价值了。努力工作，提升自己的价值，一直都是藏在人类内心深处的精神诉求，我们要做的，只是要让价值提升后带来的收益，能够给到我们自己。</p><span id="more"></span><p>在读这本书之前，我强烈建议先认真思考一下你和工作的关系究竟是什么样的。如果你就是公司的老板，那没什么好说，公司的收益就是你的。如果你不是，那就要理一理你创造的价值去了哪，是有了经济收益，有了能力的提升，有了将来可以成为自己能力证明的资历，还是帮老板多买了两辆跑车呢？</p><p>之前读过一篇文章，说是将你自己当做一家公司来经营，将任职的公司当做自己的客户。我觉得这是一个很好的思考的思路，你需要考虑你这家「公司」，是立刻就去追求现金收入，还是先打磨能力与口碑，以期将来去获得更大的收益？要考虑与任职公司的交易是否公平？</p><p>想清楚了这些，再来读《干法》这本书。否则，你可能会觉得这本书是马老师写的。话说回来，马老师确实也在践行这本书的很多理念，在他当英语老师的时候。没有当年“业余”时间的辛勤奋斗，哪里来今天的福报呢？</p><p>好了，下面进入正题。我会从三个方面讲述书中传递出的工作相关的价值观，一是工作的意义，二是如何对待工作，三是工作方法和态度。</p><h2 id="工作的意义"><a href="#工作的意义" class="headerlink" title="工作的意义"></a>工作的意义</h2><p>就像我在文章一开头说的，努力工作，创造价值实际上是埋藏在人类内心深处的一种非常重要的精神诉求。可以了解到，很多已经财务自由的人，依然在寻求一些工作机会，或是创业，或是找个安逸自由的工作。这就是内心深处的这种诉求在驱动人的行为。</p><p>除了通过工作获得的经济收益以外，</p><blockquote><p>一心一意工作，精益求精，本身就是磨炼人格的修行，促进我们成长。劳动可以带来喜悦感，让人明白生活的意义，劳动是高贵的行为。</p></blockquote><p>如果你能接受「将自己当成一家公司来经营」这样的设定，你会发现，眼前的经济收益其实没有那么重要。而个人的成长、工作中带来的幸福感是更有意义的收获，而在收获了这些之后，经济收益也会自然而然的随之而来。</p><h2 id="如何对待工作"><a href="#如何对待工作" class="headerlink" title="如何对待工作"></a>如何对待工作</h2><blockquote><p>大多数人初出茅庐只能从自己不喜欢的工作开始。</p></blockquote><p>这个世界上，真正将自己的爱好当做工作的，本身就是少数。在这些少数之中，绝大部分人工作了之后，也会感到这个事情已经不是自己的爱好了。因为作为一个爱好，和作为一份工作，这之中的差别是非常大的。游戏很多人都爱玩吧，很多人会羡慕那些职业的电竞选手，认为他们就是在做自己喜欢的工作。但是关注一下电竞圈，会发现不好好训练的人大有人在，有些人会抓住各种时机，去玩别的游戏。原本是因为喜欢这个游戏做了职业选手，最后却发现自己渐渐的不喜欢这个游戏了。</p><p>所以，做不喜欢的工作，才是这个世界的常态。而我们要做的，是让我们喜欢上工作本身。如何让自己喜欢上工作，作者给我们提供了一些方法。</p><blockquote><p>付出了努力，才会让自己喜欢上工作。</p></blockquote><blockquote><p>为工作中的小小成功感到欣喜，将其带来的能量当做动力，更加努力的工作。</p></blockquote><h2 id="工作方法和态度"><a href="#工作方法和态度" class="headerlink" title="工作方法和态度"></a>工作方法和态度</h2><p>首先，要以高目标为动力。</p><blockquote><p>能力要用“将来进行时”</p></blockquote><p>可以想象一下，如果把一段时间之前的自己放到现在，是不是也觉得自己现在做的事情很难？这是一个正常的现象，当你处在一个正常的发展路径下的时候，能力的提升在不知不觉中就会产生。</p><p>有了高目标，就要持续的付出努力，要去思考目标怎么样才能实现。</p><blockquote><p>一定要去想。不认真思考，就什么都实现不了。</p></blockquote><p>自己的成长，只有自己能够负责，指望其他任何人都是不现实的。我最近看脉脉上的吐槽，很多人对公司的不满是「没有成长」，这就让我非常迷惑，成长和公司究竟是什么关系呢。这个要说回到人和公司的关系，公司作为你的一个客户，你完成一些事情，并且获得一些报酬。这和个人成长有任何关系吗？成长是需要自己去思考的，而不是等着别人派下来，说这些这些干完，你就成长了。我能想到的一个人在一家公司里会没有任何收获的唯一场景，就是他已经理解了这家公司的所有方面，可以轻而易举的经营另外一家公司来打败这家公司。</p><p>另外，要有完美主义。</p><blockquote><p>橡皮绝对擦不掉错误</p></blockquote><p>很多错误，即使你事后弥补了，也必然会留下一些痕迹。无论做什么事情，要尽力去将它做到，至少是自己认知范围内的，完美。像开发一个需求，要思考完整整个流程，确保没有逻辑上的缺陷，也没有实现上的bug, 而不是看一遍产品稿就简简单单的实现一遍，剩下的所有事交给测试。</p><blockquote><p>工作的结果&#x3D;思维方式<em>热情</em>能力</p></blockquote><p>在书的最后，作者给了一个公式。对于工作来说，思维方式和热情是和能力一样重要的。而在你具备这种认知的前提下，提升热情和改变思维方式，要远比提升能力更简单。所以，想提升工作的效果，其实没有那么难，关键是要建立起对于工作的合理的认知。</p><p>我是流沙，感谢阅读这篇文章。如果觉得写的还不错的话，请多多点赞关注。如有问题，也可以在<a href="https://lichuanyang.top/">个人博客</a>、微信公众号( Mobility ) 等平台和我交流</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-程序员需要读《干法》这种书吗？"><a href="#Q-程序员需要读《干法》这种书吗？" class="headerlink" title="Q: 程序员需要读《干法》这种书吗？"></a>Q: 程序员需要读《干法》这种书吗？</h3><p>需要，但建议带着”审视”的心态去读，而不是全盘接受。这本书的核心价值在于让你先想清楚”我与工作的关系是什么”——你是为老板打工，还是在为自己积累能力和口碑？把这个问题想明白之后，书中关于热情、完美主义、持续付出的理念才会对你产生实际启发。建议配合”把自己当成一家公司经营”这个思维框架一起读，效果更好。</p><h3 id="Q-工作是为了什么？"><a href="#Q-工作是为了什么？" class="headerlink" title="Q: 工作是为了什么？"></a>Q: 工作是为了什么？</h3><p>既是经济来源，也是精神诉求。经济层面不言而喻，但更重要的是，创造价值本身就埋藏在人类内心深处——很多财务自由的人依然会选择工作。书中给出的答案很直接：工作不仅是获取报酬的途径，更是磨炼人格、带来喜悦感、让人理解生活意义的修行。如果暂时还无法完全认同这个观点，不妨从”把自己当成一家公司经营”开始，至少让你的工作产出能实实在在地转化为你自己的积累。</p><h3 id="Q-如何喜欢上自己不喜欢的工作？"><a href="#Q-如何喜欢上自己不喜欢的工作？" class="headerlink" title="Q: 如何喜欢上自己不喜欢的工作？"></a>Q: 如何喜欢上自己不喜欢的工作？</h3><h2 id="做不喜欢的工作才是常态——真正把爱好当工作的人极少，且即使如此，爱好变成工作后也往往变了味。书里给出了两个实用方法：一是先付出努力，努力带来的正反馈会逐渐让你产生投入感；二是为工作中的小小成功感到欣喜，把每一次”我搞定了”的能量积累成继续前行的动力。这个过程不是靠鸡汤支撑，而是靠行动带来的正循环。"><a href="#做不喜欢的工作才是常态——真正把爱好当工作的人极少，且即使如此，爱好变成工作后也往往变了味。书里给出了两个实用方法：一是先付出努力，努力带来的正反馈会逐渐让你产生投入感；二是为工作中的小小成功感到欣喜，把每一次”我搞定了”的能量积累成继续前行的动力。这个过程不是靠鸡汤支撑，而是靠行动带来的正循环。" class="headerlink" title="做不喜欢的工作才是常态——真正把爱好当工作的人极少，且即使如此，爱好变成工作后也往往变了味。书里给出了两个实用方法：一是先付出努力，努力带来的正反馈会逐渐让你产生投入感；二是为工作中的小小成功感到欣喜，把每一次”我搞定了”的能量积累成继续前行的动力。这个过程不是靠鸡汤支撑，而是靠行动带来的正循环。"></a>做不喜欢的工作才是常态——真正把爱好当工作的人极少，且即使如此，爱好变成工作后也往往变了味。书里给出了两个实用方法：一是<strong>先付出努力</strong>，努力带来的正反馈会逐渐让你产生投入感；二是<strong>为工作中的小小成功感到欣喜</strong>，把每一次”我搞定了”的能量积累成继续前行的动力。这个过程不是靠鸡汤支撑，而是靠行动带来的正循环。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/3513/</id>
    <link href="https://lichuanyang.top/posts/3513/"/>
    <published>2021-05-15T09:29:34.000Z</published>
    <summary>稻盛和夫《干法》读书笔记，反思工作态度和工作方法，探讨如何在工作中实现个人价值提升。</summary>
    <title>读书笔记 稻盛和夫《干法》-思考应该怎样去工作</title>
    <updated>2026-06-27T02:31:54.481Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="开发规范" scheme="https://lichuanyang.top/tags/%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83/"/>
    <content>
      <![CDATA[<h2 id="为什么需要-Code-Review"><a href="#为什么需要-Code-Review" class="headerlink" title="为什么需要 Code Review"></a>为什么需要 Code Review</h2><p>在通常的开发流程中，会有各种各样的评审(review)环节，比如产品稿评审、设计评审、测试用例评审、code review等。为了做这些评审，难免会引入一些会议。有的人会觉得这些事没什么用，太浪费时间。而实际上，这些事情是非常重要的，如果你产生了评审不重要的印象，原因无非两个，一是认知不到位，而是你所在的环境评审实施的不好。</p><span id="more"></span><h2 id="Review-对团队的价值"><a href="#Review-对团队的价值" class="headerlink" title="Review 对团队的价值"></a>Review 对团队的价值</h2><p>关于评审的价值，首先对于团队来说，最大的作用其实就是提升团队的下限。试想一下，如果各个环节都不进行评审，而是直接产品交付一个产品稿，开发就对着开发，任何个人的疏忽都会给整个项目带来无尽的风险。多次的评审环节，实际上就是将一个业务方案，分别用产品稿、代码、测试用例等形式描述，再分别引入更多的人来检查，避免一个人的疏忽造成业务上的风险。</p><p>此外，本文的重点，我实际上是想讲讲除了给团队带来的价值外，对于一个个体的人来说，参与评审有什么价值。</p><h2 id="Review-对被审查者的价值"><a href="#Review-对被审查者的价值" class="headerlink" title="Review 对被审查者的价值"></a>Review 对被审查者的价值</h2><p>首先说一下对于被评审的人，即产品稿、代码等的提交人来说，评审就是一个其他人来帮助你提升的过程，是难得的能获取直接的反馈信息的机会。其实对于很多人来说，相比于学生时代，工作时代学东西的最大难点就是没有人批改「作业」，看了书看了视频之后，没办法确认自己究竟学没学会。而及时的反馈，无论学什么，都是非常重要的。而在参与评审的人中，会包括你的上级，能力更强的同事。他们能够详细的了解你的方案、代码的话，相信是能够给出很多有价值的反馈的。评审，实际上就是给你自己提供一个机会，能够接收到他们的反馈信息。</p><p>这里其实隐含了一条对于提交人的要求，就是要能够清晰的表达出自己的方案，这样别人才能低成本的参与到评审中来。比如写代码，要运用合理的设计模式，写出易读的代码。写文档，要有合理的布局、分段，突出方案的重点。向别人讲方案时，要考虑听众的知识背景，确保他们可以听的懂你的方案。</p><p>评审对于提交人的另一个重要作用，是分担风险和责任。要知道，无论你公司的制度是什么样的，无论你的职位如何，当你负责的项目没有做好时，最终的结果，一定是有相当一部分是由你自己承担的。承担的形式，可能是直接的绩效损失，可能是别人对你的能力产生了质疑，不管怎样，都是大家不希望面对的。而评审，就是将一部分后果，转移到整个团队上。当然，别想太多，主体的责任一定是还在提交人本人身上。这里，同样体现了提交人清晰表达的重要性。提交人需要将自己方案的关键点，更多的让评审人们接受到。如果评审人成功的接收这些信息，并且在评审时进行充分的讨论并最终达成共识，这一部分决策就可以认为是团队共同做出的了。</p><h2 id="Review-对审查者的价值"><a href="#Review-对审查者的价值" class="headerlink" title="Review 对审查者的价值"></a>Review 对审查者的价值</h2><p>接下来再来看对于评审人来说，积极的参与对其他人的评审，又有什么作用。首先的价值还是一个学习的机会。所谓三人行，必有我师，从别人的方案里，一定是能够发现一些非常好的点，值得自己学习的。当发现别人的方案和自己的设想不一致时，是一个进行深度思考的机会，可以对自己进行一个连续的追问，看看到底哪种方案比较好，直到得出一个无法质疑的结论。这种深度的思考，对于自己的思考方式会有很好的改善作用。</p><p>另外，参与评审也是一个快速提升经验的方法。参与评审，实际上是花费评审的时间，得到一个无限接近于 自己做了这件事 的效果。评审的足够细致的话，你就会对这件事怎么做，有一个非常全面的认识，这件事也就完全可以作为你项目经历的一部分。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文到这里就要结束了，不知道大家能否通过本文建立起对为什么要做评审、怎么做评审的认知。大家可以通过 公众号( Mobility ), <a href="https://lichuanyang.top/">个人网站</a> 等和我交流。</p><h2 id="原文地址-https-lichuanyang-top-posts-57205"><a href="#原文地址-https-lichuanyang-top-posts-57205" class="headerlink" title="原文地址: https://lichuanyang.top/posts/57205"></a>原文地址: <a href="https://lichuanyang.top/posts/57205">https://lichuanyang.top/posts/57205</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/57205/</id>
    <link href="https://lichuanyang.top/posts/57205/"/>
    <published>2021-04-27T10:07:11.000Z</published>
    <summary>从个人成长角度分析 code review 的价值，不仅是发现 bug，更是团队知识共享和代码质量提升的关键手段。</summary>
    <title>review的个人价值</title>
    <updated>2026-06-27T00:55:31.538Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="职业发展" scheme="https://lichuanyang.top/tags/%E8%81%8C%E4%B8%9A%E5%8F%91%E5%B1%95/"/>
    <content>
      <![CDATA[<p>今天结合我的个人经历来聊一聊应届生的职业选择问题，主要针对后端开发（对岗位的选择，话题也比较大，后边有机会的话会单独开一篇文章写），我这几年的时间里历经了国企、大公司、小公司这样不同的工作环境，所以对于不同公司的情况还是比较有发言权的。</p><span id="more"></span><h2 id="四个维度的取舍"><a href="#四个维度的取舍" class="headerlink" title="四个维度的取舍"></a>四个维度的取舍</h2><p>职业选择，最重要的无非就是城市和公司选择。城市选择是一件非常主观的事情，比如我，作为一个北方人，既不喜欢南方城市的气候，又想离家近，还需要大城市的机会，北京就几乎成了唯一选择。当然，给大家的建议，功利点的话，建议选择一些快速发展中的强二线城市，比如重庆、合肥，这样随着城市平台的发展，你自己身处其中，身价也会提升。</p><p>接下来，我会限定在北京的范围内，结合我自己的经历，说一下公司的选择。</p><h2 id="户口：一线城市的入场券"><a href="#户口：一线城市的入场券" class="headerlink" title="户口：一线城市的入场券"></a>户口：一线城市的入场券</h2><p>提到北京，一个不得不提的问题就是户口的问题。不过，<strong>我强烈不建议你依据一个户口就决定了整个人生怎么走</strong>，比如明明不想进体制内，却为了个户口就去了。务必在考虑户口问题之前，优先想好职业发展方向的问题。</p><p>很多人会纠结于体制内&#x2F;体制外、大公司&#x2F;小公司这些明面上的选择，但这些其实都只是表象而已，同样是体制内，也可能有天差地别的变化。一个正确的做法是想清楚自己真正想要的，然后再去寻找合适的工作机会。对于什么是自己想要的，确实不是每个人都能想的清楚。所以，我把这个问题拆解一下，看看工作的选择究竟意味着什么。大家可以思考以下几个问题：</p><ul><li>你最希望从工作中获得什么？钱？权力？还是内心的成就感？或是并不想从工作中获得任何东西？</li><li>你希望工作在你的人生中占据多大的比例</li><li>对于工作中获取的资源（人脉、信息等），和通过工作提升的能力，那一项你更有信心能带到其他的环境里</li><li>你喜欢做常规的事务性工作，还是希望持续的去挑战困难问题</li></ul><p>对于上边的任何一个问题，任何回答都没有高下之分，比如挑战困难问题并不比做常规工作更优越，这个只是大家根据个人价值观做出的选择而已。</p><p>这些选择的任意排列组合，相信都可以找到合适的机会。比如不期待从工作中有所得，不想工作占据太多人生，也愿意做常规的工作，那就可以考虑一些边缘的体制内单位；期望有权力，可以让工作占据很多生活时间，更想通过工作积累资源，就可以考虑核心的、有实权的体制内单位；而如果期望挣钱，更希望通过工作提升个人能力，程序员就是个不错的选择。</p><p>想好了上边的问题，就可以选择公司了。很多时候，我们可能无法找到并顺利的进入一个满足所有要求的地方。这也没关系，我们可以利用跳槽，每次解决其中一部分问题，并且持续的搜集信息和提升能力，寻找到合适的机会，并有能力获得这个机会。</p><h2 id="大厂：平台的价值与代价"><a href="#大厂：平台的价值与代价" class="headerlink" title="大厂：平台的价值与代价"></a>大厂：平台的价值与代价</h2><p>我以自己的经历讲一下我的几个关键选择是怎么考虑的。</p><p>刚毕业的时候，我的期望是获得一个北京户口，并且能留在互联网行业内。那个时候我的选择有两个大方向，一是去互联网公司竞争ssp offer, 但是当时的能力确实不足，所以这条路很难走的通；另一条路就是在体制内寻找业务接近互联网场景的机会，最好是面向普通用户的C端产品，因为这样可以有充足的用户量和数据量，让我不至于离业内先进的技术太远。很幸运，我找到了这样的机会，也成功的抓住了。入职之后，也基本如我所料，虽然公司技术水平并不高，企业文化也有很多问题，但是业务场景是非常好的，我也有充足的自己发挥的机会，借助工作，也学到了很多东西。</p><p>接下来，我要解决的问题就是大厂经历，所以接下来跳槽的时候就只考虑了几个一线大厂。关于工作本身，倒没什么好说的，这段经历虽然过的很不开心，但是希望达到的目标也达到了，所以也没什么遗憾的。</p><p>再下一次跳槽，我期望解决的问题有三个：大幅提高收入，抹平因为在国企呆的时间比较长造成的和同龄人的收入差距；别加班太多；人际关系轻松一些。这三个问题看似矛盾是不是？但是用心去找总是能找到合适的机会的，我也就这样来到了现在的公司。当然不是所有问题都解决的非常好，但是已经足够让我的工作体验非常幸福了。</p><p>通过我自己的经历，其实大家也可以看到，我总是似乎在提互相矛盾的要求，总是想鱼与熊掌兼得，而且最后也能幸运的都得到。其实这世界上公司那么多，并不是说有哪两个条件是必然互相矛盾的。<strong>大家没有必要过早的就舍弃什么，比如做程序员就一定意味着996，意味着放弃生活吗？显然不是。</strong></p><p>上边可以衍生出两个比较细的问题，我也一起说下吧。</p><p>一是要不要选择北京户口。我先说一下我通过这个户口获得了什么吧，首先是赶上了15，16年那波房价大涨，让家庭资产在我并没有什么理财观念的时候，也获得了大幅增值。这一点我觉得放在今天，已经不成立了，房住不炒的大前提下，就算北京的房子，也不可能跑的过股市。第二点，就是一些手续办的方便一点，这个我觉得基本可以忽略不计吧，一年估计都办不了一次事，而且越来越多的手续也支持异地办理了。第三点，就是可以放心的让孩子在北京接受教育，不用担忧未来的不确定性。为什么我说是不确定性，因为我觉得接近二十年之后，非京的孩子是否依旧不能在北京高考，是有很大的不确定性的。北京的高考难度会卷成什么样，也是有很大的不确定性的。北京户口，只能说让我个人未来的选择固定了下来，但这个选择未来会是什么效果，其实很难说的准。</p><p>所以说，当前来看，北京户口的价值其实非常有限。回到2015年的话，我仍然会毫不犹豫的选择要北京户口。但是如果让我在2021年的今天做这个选择，其实我大概率是不要的。</p><p>第二点，是关于大厂经历。这一点，我的看法是必须要有，而且尽早有，但是没必要一辈子都呆在大公司里。大厂经历的价值，其实很多人已经说的很清楚了，所以我也不想再赘述。感兴趣的话可以私下找我沟通。</p><h2 id="如何选择"><a href="#如何选择" class="headerlink" title="如何选择"></a>如何选择</h2><p>上边的篇幅里，主要说的是方向的选择。选定了方向之后，还有个重要的问题是具体公司的选择。面对一个公司，其实考虑两个问题就可以：</p><ul><li>我通过这个工作能得到什么</li><li>我因为这个工作会失去什么，将来有什么办法可以弥补</li></ul><p>以我的第一份工作来说，获得的就是户口和互联网的业务场景。同时会失去大公司的学习资源、优良的公司氛围、高薪水等等，这些问题都需要自己想办法去解决。</p><p>好了，上文已经讲完了本文的大部分内容。有个点，我一直没有提，就是中年危机的问题，可能会有人问。我为什么不提呢，因为我不觉得这是一个能靠选择解决的问题。有些人会觉得体制内很稳定，即使天天混日子也没人能把你怎么着。但是我不觉得这种依赖外部条件的事情能算稳定，历史上体制内不是没出过问题。走这条路或许你有99.99%的机会一辈子过的安安稳稳，但是如果那万分之一的可能性发生了，你打算怎么办呢。而做程序员就不一样，或许只有50%的机会能平稳的呆在一家公司，但是随着工作提升的能力，让我可以很安心的应对另一半可能性。解决中年危机的唯一方式就是提升自己，不管是提升能力还是积累资源，你总要从工作中有所得，才能持续的保持自己的竞争力。</p><p>我是流沙，大家可以通过公众号( Mobility ), <a href="https://lichuanyang.top/">个人网站</a> 等和我交流。</p><p>原文地址: <a href="https://lichuanyang.top/posts/34931/">https://lichuanyang.top/posts/34931/</a></p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-一线城市户口还值不值得争取？2021-年的户口价值发生了什么变化？"><a href="#Q-一线城市户口还值不值得争取？2021-年的户口价值发生了什么变化？" class="headerlink" title="Q: 一线城市户口还值不值得争取？2021 年的户口价值发生了什么变化？"></a>Q: 一线城市户口还值不值得争取？2021 年的户口价值发生了什么变化？</h3><p>户口曾经最大的价值是绑定房产增值（作者在 2015-16 年赶上了那波行情）。但在”房住不炒”大背景下，这个红利已基本消失。教育价值方面，近二十年后的政策不确定性很大，非京籍能否在京高考很难预测。所以 2021 年的观点是：北京户口的实际价值已经非常有限，<strong>不建议仅凭一个户口就决定职业方向</strong>——如果回归 2015 年作者仍会要户口，但如果回到 2021 年，大概率不会要。</p><h3 id="Q-大厂还是小厂？应届生第一份工作选什么最重要？"><a href="#Q-大厂还是小厂？应届生第一份工作选什么最重要？" class="headerlink" title="Q: 大厂还是小厂？应届生第一份工作选什么最重要？"></a>Q: 大厂还是小厂？应届生第一份工作选什么最重要？</h3><p>作者的观点很明确：<strong>大厂经历必须要有，且尽早有，但不用一辈子呆在大厂</strong>。大厂经历的价值在于平台背书、技术视野和成长加速度，这些已经被反复讨论过。第一份工作最重要的不是户口也不是薪资，而是想清楚四个问题：1) 你最想从工作中获得什么（钱&#x2F;权力&#x2F;成就感&#x2F;什么都不想获得）；2) 工作占人生的比例；3) 资源和人脉 vs 个人能力，哪个更有信心带走；4) 喜欢常规事务性工作还是持续挑战困难问题。</p><h3 id="Q-想鱼与熊掌兼得（高薪-不加班-氛围好）可能吗？"><a href="#Q-想鱼与熊掌兼得（高薪-不加班-氛围好）可能吗？" class="headerlink" title="Q: 想鱼与熊掌兼得（高薪 + 不加班 + 氛围好）可能吗？"></a>Q: 想鱼与熊掌兼得（高薪 + 不加班 + 氛围好）可能吗？</h3><p>作者用自己的经历证明：<strong>可能，但需要用心去找</strong>。这世上公司足够多，并没有哪两个条件是必然互斥的——做程序员不等于必然 996、不等于必然放弃生活。关键是不要过早预设”只能选一个”，先问清楚自己真正想要什么，然后持续搜集信息、提升能力，总能找到满足大部分条件的机会。一次跳槽解决不了所有问题就分多次。</p><h3 id="Q-解决中年危机的根本方式是什么？靠选择体制内能一劳永逸吗？"><a href="#Q-解决中年危机的根本方式是什么？靠选择体制内能一劳永逸吗？" class="headerlink" title="Q: 解决中年危机的根本方式是什么？靠选择体制内能一劳永逸吗？"></a>Q: 解决中年危机的根本方式是什么？靠选择体制内能一劳永逸吗？</h3><h2 id="作者明确表示：不觉得中年危机是能靠选择解决的。体制内的”稳定”本质上是依赖外部条件——历史上体制内不是没出过问题。即使有-99-99-的概率安稳，但那万分之一一旦发生，你毫无应对能力。真正的解决方式是在每一份工作中持续积累可带走的能力——做程序员可能只有-50-的概率平稳呆在一家公司，但积累的能力让你能安心应对另一半不确定性。"><a href="#作者明确表示：不觉得中年危机是能靠选择解决的。体制内的”稳定”本质上是依赖外部条件——历史上体制内不是没出过问题。即使有-99-99-的概率安稳，但那万分之一一旦发生，你毫无应对能力。真正的解决方式是在每一份工作中持续积累可带走的能力——做程序员可能只有-50-的概率平稳呆在一家公司，但积累的能力让你能安心应对另一半不确定性。" class="headerlink" title="作者明确表示：不觉得中年危机是能靠选择解决的。体制内的”稳定”本质上是依赖外部条件——历史上体制内不是没出过问题。即使有 99.99% 的概率安稳，但那万分之一一旦发生，你毫无应对能力。真正的解决方式是在每一份工作中持续积累可带走的能力——做程序员可能只有 50% 的概率平稳呆在一家公司，但积累的能力让你能安心应对另一半不确定性。"></a>作者明确表示：<strong>不觉得中年危机是能靠选择解决的</strong>。体制内的”稳定”本质上是依赖外部条件——历史上体制内不是没出过问题。即使有 99.99% 的概率安稳，但那万分之一一旦发生，你毫无应对能力。真正的解决方式是<strong>在每一份工作中持续积累可带走的能力</strong>——做程序员可能只有 50% 的概率平稳呆在一家公司，但积累的能力让你能安心应对另一半不确定性。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/34931/</id>
    <link href="https://lichuanyang.top/posts/34931/"/>
    <published>2021-03-30T10:36:15.000Z</published>
    <summary>结合国企、大厂、小公司的工作经历，为应届程序员提供城市和公司选择的务实建议。</summary>
    <title>户口?大厂?高薪?生活?聊聊应届程序员的职业选择</title>
    <updated>2026-06-27T03:42:25.694Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="架构设计" scheme="https://lichuanyang.top/categories/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="设计模式" scheme="https://lichuanyang.top/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
    <content>
      <![CDATA[<h2 id="问题的提出"><a href="#问题的提出" class="headerlink" title="问题的提出"></a>问题的提出</h2><p>关于rpc接口的输入和输出，一直有一种观点是将输入和输出都包装成单个request和response类。本文，我们就分析一下这种方式是否是一个合理的设计。</p><!--- more --><h2 id="输入参数封装分析"><a href="#输入参数封装分析" class="headerlink" title="输入参数封装分析"></a>输入参数封装分析</h2><p>先说输入, 这个我先说结论，当且仅当输入参数过多时，才应该做封装。其他情况下完全没有做封装的理由。</p><p>对于将输入参数包装成request类的理由，无非是下边这些</p><ul><li>修改接口参数时保持兼容性</li><li>可以定义通用的抽象request类，方便做统一管理</li></ul><p>首先，第一条完全就是个伪命题，第一次看到这种论断的时候，我也差一点被绕进去，但是仔细思考一下，就会发现这个完全不成立。</p><p>即便使用request体的形式，如果接口提供方增加了必传的字段，调用方仍然需要增加这个参数才能调用，那相比于直接传参数，没有解决任何问题。而这种问题实际上也是无法解决的，因为就不应该让这种情况出现。接口提供方有义务保证后续的修改是向前兼容的。而对于增加的是非必传字段这种情况，即便不包装request类，也可以通过适配器的形式实现新接口并兼容老接口，后续再找机会异步的将老接口下线。</p><p>第二条的话，确实存在一定意义。</p><ul><li>比如在抽象类里加一些统一的请求参数，这一点我认为做在框架里更合适，没有必要暴露在业务代码中</li><li>再比如添加统一的参数校验，但是单纯为这种效果的话，我觉得用一个过于抽象的类是不合适的，而是应当一类相关的业务用一个抽象类，因为一类业务才会有这种需求。同时，即便不包装成request类，这类需求也没什么难做的，因为它的难度本身就不在输入结构不统一上。</li></ul><p>而将输入参数包装成request类的最大问题就是会破坏代码的可读性，会造成语义不明确。本身类似 queryById(long id) 这么一个方法的语义是非常明确的，扫一眼接口定义就可以看清楚输入输出。但是强行封装之后，这个接口究竟要输入什么就不是那么明确了。同时，如果request中有三个参数，是否传入任意组合都能返回结果呢？这也是靠看接口看不明白的。而正常的用参数定义的话，可以使用适配器的形式定义一系列接口，接口调用方也更容易理解。</p><p>封装输入参数的唯一条件就是传入参数过多，这时候需要通过其他的方式，比如注释、文档，去约定接口的合法输入值和升级方案。</p><h2 id="输出结果封装分析"><a href="#输出结果封装分析" class="headerlink" title="输出结果封装分析"></a>输出结果封装分析</h2><p>接下来说输出，将输出结果封装成一个response体，相对于封装输入参数，会合理一些，但也不能乱用。一般将输出结果封装的目的就是包一层返回状态码。</p><h2 id="接口隔离原则的视角"><a href="#接口隔离原则的视角" class="headerlink" title="接口隔离原则的视角"></a>接口隔离原则的视角</h2><p>这一点实质上就是强迫调用方来了解接口的异常情况。我们知道，基本的设计原则中有一条叫「接口隔离原则」，其具体描述是 Clients should not be forced to depend upon interfaces that they do not use。对于这儿的情况要类似，如果调用方确实不关心请求成功或者失败的原因，那就不应该强迫调用方来了解这个。比如我就是要查个数，只存在查到或者查不到两种情况，并不想关心到底为什么没查到。</p><p>所以，在设计接口的时候，需要慎重的考虑接口的异常原因是否是一个应当提供出去的信息，在此基础上，才能说保障response合适或者不合适。</p><h2 id="结论与建议"><a href="#结论与建议" class="headerlink" title="结论与建议"></a>结论与建议</h2><p>综上所述，盲目的一味去封装所有输入和输出参数，一定是不可取的。对于何时应该做封装，何时不应该，务必要做慎重的考虑。大家有什么想法的话，也可以跟我沟通。</p><p>原文地址：<a href="https://lichuanyang.top/posts/20888/">https://lichuanyang.top/posts/20888/</a></p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-什么时候该把-RPC-输入-输出封装成-request-response-类？"><a href="#Q-什么时候该把-RPC-输入-输出封装成-request-response-类？" class="headerlink" title="Q: 什么时候该把 RPC 输入&#x2F;输出封装成 request&#x2F;response 类？"></a>Q: 什么时候该把 RPC 输入&#x2F;输出封装成 request&#x2F;response 类？</h3><p>输入参数：<strong>当且仅当参数过多时</strong>才应该封装。如果参数少（如 <code>queryById(long id)</code> 只有一个参数），封装成 request 类反而破坏可读性——你不能一眼看出接口需要什么参数，也无法从接口签名判断传入哪些组合是合法的。输出结果：封装的目的是<strong>包一层返回状态码</strong>，但需要慎重——如果调用方并不关心失败原因（只关心查到&#x2F;查不到），就不该强迫调用方处理这层状态码。</p><h3 id="Q-RPC-返回值加统一状态码和接口隔离原则冲突吗？"><a href="#Q-RPC-返回值加统一状态码和接口隔离原则冲突吗？" class="headerlink" title="Q: RPC 返回值加统一状态码和接口隔离原则冲突吗？"></a>Q: RPC 返回值加统一状态码和接口隔离原则冲突吗？</h3><p>看情况。<strong>接口隔离原则</strong>（Clients should not be forced to depend upon interfaces that they do not use）的核心是不要让调用方依赖它不需要的东西。如果调用方确实需要知道失败原因（如支付接口需要区分余额不足&#x2F;风控拦截&#x2F;网络超时），统一状态码是合理的；但如果调用方只需要”查到&#x2F;查不到”的结果，强迫它处理状态码就违反了接口隔离原则。关键在于：<strong>异常原因是值得对外提供的信息，还是内部实现细节？</strong> 先回答这个问题，再决定 response 的封装方式。</p><h3 id="Q-业界对-RPC-接口设计的普遍实践是什么？"><a href="#Q-业界对-RPC-接口设计的普遍实践是什么？" class="headerlink" title="Q: 业界对 RPC 接口设计的普遍实践是什么？"></a>Q: 业界对 RPC 接口设计的普遍实践是什么？</h3><p>业界没有”绝对正确的标准”，不同场景有不同选择。Google 的 gRPC&#x2F;labcorn一直采用单 request + 单 response 消息的模式，这与其 ProtoBuf 的 IDL 驱动、跨语言代码生成、强类型校验的定位一致。而更轻量的内部 RPC（如早期的 Dubbo 接口）则倾向于直接传参以保持简洁。好的实践是<strong>根据参数数量和调用方需求做判断</strong>，而不是一刀切：参数少且语义清晰的接口直接传参；参数多或需要兼容性管理的接口适度封装。</p><h3 id="Q-封装-request-类后如何避免语义不明确的问题？"><a href="#Q-封装-request-类后如何避免语义不明确的问题？" class="headerlink" title="Q: 封装 request 类后如何避免语义不明确的问题？"></a>Q: 封装 request 类后如何避免语义不明确的问题？</h3><h2 id="文中指出，封装后的最大问题是接口签名不再”自解释”——你无法从-query-Request-request-知道需要传什么。解决方案：1-必须配合文档和注释说明-request-中各字段的必填-选填-互斥关系；2-定义合法的字段组合，并在-request-类上做校验；3-对升级方案做明确约定（哪些字段新增是非必填兼容的，哪些是破坏性变更需要新接口版本）；4-如果参数不多，就不要封装——保持简单是最好的文档。"><a href="#文中指出，封装后的最大问题是接口签名不再”自解释”——你无法从-query-Request-request-知道需要传什么。解决方案：1-必须配合文档和注释说明-request-中各字段的必填-选填-互斥关系；2-定义合法的字段组合，并在-request-类上做校验；3-对升级方案做明确约定（哪些字段新增是非必填兼容的，哪些是破坏性变更需要新接口版本）；4-如果参数不多，就不要封装——保持简单是最好的文档。" class="headerlink" title="文中指出，封装后的最大问题是接口签名不再”自解释”——你无法从 query(Request request) 知道需要传什么。解决方案：1) 必须配合文档和注释说明 request 中各字段的必填&#x2F;选填&#x2F;互斥关系；2) 定义合法的字段组合，并在 request 类上做校验；3) 对升级方案做明确约定（哪些字段新增是非必填兼容的，哪些是破坏性变更需要新接口版本）；4) 如果参数不多，就不要封装——保持简单是最好的文档。"></a>文中指出，封装后的最大问题是接口签名不再”自解释”——你无法从 <code>query(Request request)</code> 知道需要传什么。解决方案：1) <strong>必须配合文档和注释</strong>说明 request 中各字段的必填&#x2F;选填&#x2F;互斥关系；2) 定义合法的字段组合，并在 request 类上做校验；3) 对升级方案做明确约定（哪些字段新增是非必填兼容的，哪些是破坏性变更需要新接口版本）；4) 如果参数不多，就不要封装——保持简单是最好的文档。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/20888/</id>
    <link href="https://lichuanyang.top/posts/20888/"/>
    <published>2021-03-16T10:34:56.000Z</published>
    <summary>深入分析 RPC 接口设计中将输入输出封装成 request/response 类的做法，探讨何时应该封装、何时过度设计。</summary>
    <title>RPC接口将所有输入输出封装成类是合理设计吗</title>
    <updated>2026-06-27T03:44:21.597Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="感悟" scheme="https://lichuanyang.top/tags/%E6%84%9F%E6%82%9F/"/>
    <content>
      <![CDATA[<p>说一下我个人的经历，其实在我刚毕业的第一家公司里，做的就是底层的中间件开发。但是做的过程中，觉得并不喜欢这种远离实际业务的感觉，所以后来换工作的时候就有意的选择了业务部门。所以，今天想讲一下为什么会做这种选择。</p><span id="more"></span><h2 id="我为什么不做底层开发"><a href="#我为什么不做底层开发" class="headerlink" title="我为什么不做底层开发"></a>我为什么不做底层开发</h2><p>首先，我想说一个很多人的认知，就是认为做底层的开发更容易学到东西，成长更快。我觉得这就是业务开发同学的臆想而已，底层开发没有大家想的那么利于学习，业务开发也没有大家想的那么不利于学习，原因我会在后边展开说。而之所以很多人有这样的认知，很大程度上是因为底层开发的招聘标准通常会高一些，所以这些人本身的学习能力和意愿就是要好一些。</p><p>其实底层开发的日常和业务开发也没有太大区别，都是在做需求。只是业务需求的来源是产品经理，底层需求的来源是其他开发，或者底层开发者本人的思考和想法。这里，我要说自己为什么不愿意做底层开发的第一个原因了，就是需求的来源不科学。首先，如果只做底层开发，而脱离了实际业务场景的话，对于技术的认知很可能是有偏差的，这样导致自己想做的一些事很可能是在自嗨，并没有实际应用价值。而如果从其他那些搜集需求呢，也会有类似的问题，业务开发的同学提出的意见可能也就是他自己的非常浅的想法而已，不一定经过了很完善的考虑；再者，业务同学对基础组件究竟该干什么也未必理解准确，他可能分不清楚什么事应该基础组件解决，什么事应该自己解决。</p><p>在这，引入了我不愿意做底层开发的第二个原因，就是职责边界不明确。这个和配合的业务方的能力强相关。有时候，自己有些事没做好，业务方也能找到办法解决；而有时，明明和自己没关系，也要被迫处理一些上层的逻辑。</p><p>大家也可以看到，上边两个问题都极度依赖同事的能力，所以，如果你一定要做底层开发的话，一定要去技术能力储备强的公司，这个直接关系到你的工作幸福度。公司的选择还影响一件事，就是工作内容，简单的说，就是要造轮子，还是完善开源工具，两类工作可以说是天差地别。造轮子，需要对各种底层知识很熟悉，确实是很能促进进步，这个也是“做底层有利于成长”这种想法的来源。但是这种事仅存在为数不多的几家大公司中。其他大量的完善开源工具的工作，其实和业务开发没有任何区别，只需要了解一下开源工具的边边角角，就可以完成大部分工作，既没有深度，也没有广度。我开篇的时候说底层开发并不能帮助学习，原因也就在这。</p><p>所以，要做底层开发，公司的选择至关重要，大，技术能力强，两个条件缺一不可。大家肯定会问，我去这样的公司做不就行了吗。话是这么说没错，但这带来的问题也就来了，这也是我不想做底层开发的第三个原因，职业选择面太窄。国内java底层做的好的也就某里某团两家而已，这两家的企业文化大家也都清楚。所以，不想去这样的公司，是不是就不干了？即使你能接受，巨大的供需量对比，也会带来疯狂的内卷，就像算法岗那样，你是否有足够的把握赢得这场内卷游戏呢？而对于业务开发来说，大厂经历只是一段必要的学习经历和自己简历上的一个亮点，当你对大厂的整个规范流程熟悉了之后，就可以自由的选择想去的公司了。</p><h2 id="业务开发的价值"><a href="#业务开发的价值" class="headerlink" title="业务开发的价值"></a>业务开发的价值</h2><p>前边讲底层开发说的太多了，下边进入正题，就是业务开发的价值。在我看来，业务开发带给我的能力提升主要在三个方面：业务知识的积累，技术广度的提升和综合能力的提升。</p><p>先说业务知识，业务知识其实就是对现实世界的理解。对于我个人来说，工作之余，了解一下其他行业的运转模式，首先就是一个很有意思的事情。其次，不断的去了解不同行业的行业知识，我是完全能感受的到对我认知能力的提升的。这种能力能不能变现？当然是可以的。比如去炒股，工作中的积累让我有了分析一个公司商业模式的能力，对于什么样的是好公司好股票，显然可以比一般人理解的深一些。当然，这儿扯的太远了，大家看看就好，关键是看自己的兴趣，有没有意愿去了解一些技术之外的事情。</p><p>接下来是技术广度。业务开发和底层开发的最大区别，其实就是底层开发对深度要求更高，而业务开发对广度要求更高。业务开发需要对各种各样的框架、中间件都有所了解，并且不能了解的太浅，也需要一定的深度，要能够理解它们能解决什么问题，又会带来什么问题，适用于什么场景；面对复杂的业务场景和复杂的系统，要能够全面理解；发生问题时，要有能力快速定位到问题在哪。这些除了需要知识的积累，也需要实际场景下经验的积累。</p><p>然后说一下综合能力的提升，相比底层开发，业务开发对人的通用基础能力会有更大的锻炼，比如沟通能力、分析解决问题的能力、权衡取舍的能力。这些能力可以说和技术有关，但关联也没那么大，极端的讲，即使你将来转行了，这些普适性的能力也会很有帮助。我时常在想，学着用一个开源工具，和打王者荣耀，究竟有什么区别。作为新手，参考着现成的例子做一个demo, 就相当于第一次接触这个游戏，随便打打。而随着对这个逐渐熟悉，有的人可能就成了没有感情的调包机器，而有的人就可以逐渐去了解底层的实现。就像游戏里，有的人一直浑浑噩噩，打了几千场连电刀和无尽战刃有什么区别都不知道，而有的人就会去探索伤害的数值机制。所以，我觉得大家别把程序员看成一个多特殊的职业，它和其他职业并没有什么本质的区别。大部分职业事实上都是在用不同的方式解决问题，比如律师是依靠法律，基金经理是依靠对市场和基本面的认知，程序员就是靠代码。我们在不断的工作中，锻炼的是解决问题的能力，而解决的方法并没有人要求你一定要限制在技术之内。</p><p>我们在业务开发中，就是要不断积累上边三个方面的能力，这样才能滚起雪球，从而从容的应对中年危机。我写这篇文章，当然也并不是要说业务开发就是好，主要还是想帮大家分析清楚业务开发和底层开发的区别，从而做出一个理性的选择。</p><p>如果大家有什么想法，也可以来和我沟通。</p><p>原文地址: <a href="https://lichuanyang.top/posts/40071/">https://lichuanyang.top/posts/40071/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/40071/</id>
    <link href="https://lichuanyang.top/posts/40071/"/>
    <published>2021-03-12T11:27:36.000Z</published>
    <summary>从个人经历出发，分析业务开发和底层开发的真实差异，打破「底层开发更利于成长」的认知误区。</summary>
    <title>程序员做业务开发的价值</title>
    <updated>2026-06-27T03:45:23.476Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="架构设计" scheme="https://lichuanyang.top/categories/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="设计模式" scheme="https://lichuanyang.top/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
    <content>
      <![CDATA[<p>大家好，我是流沙。设计模式是每个程序员都会经常接触到的东西，但是相信很多人对于设计模式究竟是什么还会有些疑问。所以，我们今天就聊聊这个，主要目标是帮大家理解设计模式的作用，以及要用什么样的心态对待设计模式。</p><span id="more"></span><h2 id="前置知识"><a href="#前置知识" class="headerlink" title="前置知识"></a>前置知识</h2><p>提到设计模式，其实首先需要理解清楚的是面向对象思想。相信大家即使不能非常清晰的描述出来，对面向对象也应该是比较熟悉的。</p><p>我们就快速讲一下，面向对象有四大基本特性：封装、抽象、继承、多态；</p><p>封装：仅暴露有限的接口，授权外部来访问。将逻辑集中，因此更可控；可读性、可维护性也更好；易用性也更好。</p><p>抽象：隐藏方法的具体实现，让调用者只需要关心方法提供了哪些功能，并不需要知道这些功能是如何实现的。</p><p>继承：好处就是代码复用。</p><p>多态：子类可以替换父类。提高代码的可扩展和可复用性。</p><h2 id="什么是设计模式"><a href="#什么是设计模式" class="headerlink" title="什么是设计模式"></a>什么是设计模式</h2><p>设计模式是针对软件开发中经常遇到的一些设计问题，根据基本的设计原则，总结出来的一套实用的解决方案或者设计思路。</p><p>可以看到，设计模式是非常偏实际应用的，相比设计原则更加具体、可执行。</p><p>因此，在了解设计模式之前，就需要了解一些基本的设计原则。这些原则才是指导我们写出好代码的关键。</p><p>那么什么是好代码呢？</p><h2 id="好代码的标准"><a href="#好代码的标准" class="headerlink" title="好代码的标准"></a>好代码的标准</h2><p>这个其实很难描述，我们可以试着归纳一些好代码的特征，比如下面这些：</p><p>可维护、可扩展、可读、可测、可复用、简洁。</p><p>为了达到这些标准，就需要现有一些基本的设计原则。</p><h2 id="基本设计原则"><a href="#基本设计原则" class="headerlink" title="基本设计原则"></a>基本设计原则</h2><h2 id="SOLID"><a href="#SOLID" class="headerlink" title="SOLID"></a>SOLID</h2><h3 id="单一职责-Single-Responsibility-Principle"><a href="#单一职责-Single-Responsibility-Principle" class="headerlink" title="单一职责  Single Responsibility Principle"></a>单一职责  Single Responsibility Principle</h3><p>描述很简单，一个类只负责完成一个职责或者功能。但是实际要做好其实还是很难的。</p><p>关于怎么样才算职责单一，可以定出一些简单的标准，比如代码行数过多；依赖的其他类过多；私有方法过多；难以给类起名；类中大量方法功能集中等。但是无法定义出一个普适的标准，一定要结合实际情况去考虑。</p><p>比如这样一个类</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="title class_">Student</span> &#123;</span><br><span class="line">    int id;</span><br><span class="line"><span class="title class_">String</span> name;</span><br><span class="line"><span class="title class_">String</span> age;</span><br><span class="line">...</span><br><span class="line"><span class="title class_">String</span> province;</span><br><span class="line"><span class="title class_">String</span> city;</span><br><span class="line"><span class="title class_">String</span> county;</span><br><span class="line"><span class="title class_">String</span> detailAddress;</span><br><span class="line">...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在其中至少有四个字段是和地址有关的，那么是否需要把地址相关的职责抽离出来，封装一个新的Address类?</p><p>其实，是要视情况而定的，如果地址这些属性只是单纯的展示信息，那么直接放在Student类里，就没有问题；如果在这背后还有一套复杂的物流系统，也就是有很多地址相关的复杂逻辑，那么这一部分就应当单独抽离出来。</p><p>在实际的开发中，我们可以先写一个粗粒度的类，满足业务需求。随着业务的发展，如果粗粒度的类越来越庞大，代码越来越多，这个时候，我们就可以将这个粗粒度的类，拆分成几个更细粒度的类。这就是所谓的持续重构。</p><h3 id="开闭原则-Open-Closed-Principle"><a href="#开闭原则-Open-Closed-Principle" class="headerlink" title="开闭原则 Open Closed Principle"></a>开闭原则 Open Closed Principle</h3><p>对扩展开放、对修改关闭。</p><p>添加一个新的功能，应该是通过在已有代码基础上扩展代码（新增模块、类、方法、属性等），而非修改已有代码（修改模块、类、方法、属性等）的方式来完成。</p><p>开闭原则是相对难理解的一条原则。</p><p>“怎样的代码改动才被定义为‘扩展’？怎样的代码改动才被定义为‘修改’？怎么才算满足或违反‘开闭原则’？修改代码就一定意味着违反‘开闭原则’吗？”等问题，都很难回答。</p><p>而这条原则又是最有用，因为扩展性是代码质量最重要的衡量标准之一。</p><p>实际上，我们也没必要纠结某个代码改动是“修改”还是“扩展”，更没必要太纠结它是否违反“开闭原则”。我们应当回到它的初衷上，只要没有破坏原有代码的运行，就没有违反开闭原则。一个简单的判断标准，当功能没有变化时，对应的单元测试没被破坏，就是一次满足开闭原则的改动。</p><p>关于如何做到对扩展开放、修改关，也需要慢慢学习和积累。在 23 种经典设计模式中，大部分设计模式都是为了解决代码的扩展性问题而存在的，主要遵从的设计原则就是开闭原则。</p><p>实际写代码时可以假定变化不会发生，当出现变化时，需要抽象以隔离将来的同类变化。</p><h3 id="里式替换原则-Liskov-Substitution-Principle"><a href="#里式替换原则-Liskov-Substitution-Principle" class="headerlink" title="里式替换原则 Liskov Substitution Principle"></a>里式替换原则 Liskov Substitution Principle</h3><p>子类能够在任何地方替换父类，并且保证原来程序的逻辑行为不变及正确性不被破坏。</p><p>里式替换原则还有另外一个更加能落地、更有指导意义的描述，那就是“Design By Contract”，中文翻译就是“按照协议来设计”。描述里子类和父类的关系，可以替换成接口和实现的关系。换一种方式描述里式替换原则，就是实现类不能违反接口声明要提供的功能、输入、输出、异常等。</p><h3 id="接口隔离原则-Interface-Segregation-Principle"><a href="#接口隔离原则-Interface-Segregation-Principle" class="headerlink" title="接口隔离原则 Interface Segregation Principle"></a>接口隔离原则 Interface Segregation Principle</h3><blockquote><p>Clients should not be forced to depend upon interfaces that they do not use。</p></blockquote><p>其核心要素就是一个「接口」的功能要尽量单一，调用方只需要依赖它需要的部分，而不需要去依赖或者调用整个「接口」。</p><p>这里的接口可以指代不同的含义，比如一组api接口集合（也就是我们用的一个proxy）。</p><p>比如有个项目，会做一些业务流程，生成一些数据，然后又要作为基础服务把数据提供出去，这时，最后就是提供了两个接口, 一个是用来处理内部流程；一个则是作为基础服务堆外提供。这样做的好处就是可以一定程度上避免误调用。</p><p>也可以将接口就理解为单独一个接口方法，这时候就是说一个方法的功能要单一。</p><p>接口隔离原则跟单一职责原则有点类似，不过稍微还是有点区别。单一职责原则针对的是模块、类、接口的设计。而接口隔离原则相对于单一职责原则，更侧重于接口的设计。接口隔离原则提供了一种判断接口的职责是否单一的标准：通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能，那接口的设计就不够职责单一。</p><h3 id="依赖反转原则-Dependency-Inversion-Principle"><a href="#依赖反转原则-Dependency-Inversion-Principle" class="headerlink" title="依赖反转原则 Dependency Inversion Principle"></a>依赖反转原则 Dependency Inversion Principle</h3><blockquote><p>High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.</p></blockquote><p>所谓高层模块和低层模块的划分，简单来说就是，在调用链上，调用者属于高层，被调用者属于低层.</p><p>这一原则更多针对框架开发，业务开发里，高层模块依赖底层模块，实际上没有任何问题。</p><p>以tomcat为例，tomcat调用web程序来运行，所以tomcat是高层模块。</p><p>Tomcat 和应用程序代码之间并没有直接的依赖关系，两者都依赖同一个“抽象”，也就是 Servlet 规范。Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节，而 Tomcat 容器和应用程序依赖 Servlet 规范。</p><h3 id="KISS"><a href="#KISS" class="headerlink" title="KISS"></a>KISS</h3><p>Keep It Simple and Stupid.</p><p>Keep It Short and Simple.</p><p>Keep It Simple and Straightforward</p><p>KISS原则有不同的表述方式，但是大体意思都差不多，就是要让代码尽量简单。</p><p>「简单」实际上是一个很主观的事情，code review是一种很好的间接评价代码是否简单的方式。如果同事review你的代码时，会有很多地方看不懂，这就很可能是代码不够简单。</p><p>一些简单的经验：不要重复造轮子；避免过度设计，无论是为了未来不确定的扩展性，还是细微的性能差别。</p><p>举个例子，StringUtils中的很多方法，在一个具体的场景中，我们是大概率可以写出一个性能更好的实现的，因为StringUtils中要考虑的很多通用性的问题，在具体的场景中就不需要考虑了。那么，我们是不是要真的这么去做？</p><h3 id="DRY-原则（Don’t-Repeat-Yourself）"><a href="#DRY-原则（Don’t-Repeat-Yourself）" class="headerlink" title="DRY 原则（Don’t Repeat Yourself）"></a>DRY 原则（Don’t Repeat Yourself）</h3><p>这一条本身很简单，不过要注意这里的repeat和单纯代码重复是有区别的。比如两个无关的方法，实现恰好一模一样，这时候就不认为它违反了DRY原则，因为语义上是不重复的。而两个方法，如果方法名到实现都完全不一样，但是是在做一件事，比如isValidIp(), checkIpValid(), 两个方法各自用不同的方法实现了，我们也认为违反了DRY原则。</p><p>另外要注意执行重复的问题，比如</p><figure class="highlight jsx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span> <span class="title function_">run1</span>(<span class="params"></span>) &#123;</span><br><span class="line"><span class="title function_">a</span>();</span><br><span class="line"><span class="title function_">b</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> <span class="title function_">run2</span>(<span class="params"></span>) &#123;</span><br><span class="line"><span class="title function_">a</span>();</span><br><span class="line"><span class="title function_">c</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="title function_">main</span> () &#123;</span><br><span class="line"><span class="title function_">run1</span>();</span><br><span class="line"><span class="title function_">run2</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>虽然看起来代码封装的很好，但是执行main方法时，a方法执行了两次。如果a是一个开销很大的方法，就会很有问题。</p><h3 id="迪米特法则"><a href="#迪米特法则" class="headerlink" title="迪米特法则"></a>迪米特法则</h3><blockquote><p>Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.</p></blockquote><p>“高内聚、松耦合”是一个比较通用的设计思想，可以用来指导不同粒度代码的设计与开发，比如系统、模块、类、函数。</p><p>所谓高内聚，就是指相近的功能应该放到同一个类（模块、系统）中，不相近的功能不要放到同一个类（模块、系统）中。</p><p>所谓松耦合是说，在代码中，类（模块、系统）与类（模块、系统）之间的依赖关系简单清晰，只依赖必要的东西。</p><p>像qwerty中之前对作业相关逻辑的处理，需要了解每种类型作业的实现细节，就是一种违反了迪米特法则的做法。正确的做法是在作业这一侧定义一个通用的模型，模型中将各类作业的不同逻辑封装好，qwerty则只需要了解这个模型。</p><p>对于这些设计原则，不需要花费很多精力来扣它们的字眼。更重要的是，理解它们的思想，理解为什么这些原则能帮助我们写出来「可维护、可扩展、可读、可测、可复用、简洁」的代码。</p><p>接下来，我们就可以具体说设计模式了。</p><h2 id="常用设计模式"><a href="#常用设计模式" class="headerlink" title="常用设计模式"></a>常用设计模式</h2><p>经典的设计模式有 23 种。随着编程语言的演进，一些设计模式（比如 Singleton）也随之过时，甚至成了反模式，一些则被内置在编程语言中（比如 Iterator），另外还有一些新的模式诞生（比如 Monostate）</p><p>创建型模式，共五种：工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式</p><p>结构型模式，共七种：适配器模式、装饰者模式、代理模式、门面模式、桥接模式、组合模式、享元模式。</p><p>行为型模式，共十一种：策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。</p><p>我们只挑其中一些重要的来讲吧。</p><h2 id="简单工厂-工厂方法"><a href="#简单工厂-工厂方法" class="headerlink" title="简单工厂&amp;工厂方法"></a>简单工厂&amp;工厂方法</h2><p>比如一个需要根据文件格式来创建不同parser，将配置读取到RuleConfig中的功能，简单工厂模式的实现如下。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RuleConfigSource</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> RuleConfig <span class="title function_">load</span><span class="params">(String ruleConfigFilePath)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">ruleConfigFileExtension</span> <span class="operator">=</span> getFileExtension(ruleConfigFilePath);</span><br><span class="line">    <span class="type">IRuleConfigParser</span> <span class="variable">parser</span> <span class="operator">=</span> RuleConfigParserFactory.createParser(ruleConfigFileExtension);</span><br><span class="line">    <span class="keyword">if</span> (parser == <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">InvalidRuleConfigException</span>(</span><br><span class="line">              <span class="string">&quot;Rule config file format is not supported: &quot;</span> + ruleConfigFilePath);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">String</span> <span class="variable">configText</span> <span class="operator">=</span> <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="comment">//从ruleConfigFilePath文件中读取配置文本到configText中</span></span><br><span class="line">    <span class="type">RuleConfig</span> <span class="variable">ruleConfig</span> <span class="operator">=</span> parser.parse(configText);</span><br><span class="line">    <span class="keyword">return</span> ruleConfig;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> String <span class="title function_">getFileExtension</span><span class="params">(String filePath)</span> &#123;</span><br><span class="line">    <span class="comment">//...解析文件名获取扩展名，比如rule.json，返回json</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;json&quot;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RuleConfigParserFactory</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">static</span> IRuleConfigParser <span class="title function_">createParser</span><span class="params">(String configFormat)</span> &#123;</span><br><span class="line">    <span class="type">IRuleConfigParser</span> <span class="variable">parser</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br><span class="line">    <span class="keyword">if</span> (<span class="string">&quot;json&quot;</span>.equalsIgnoreCase(configFormat)) &#123;</span><br><span class="line">      parser = <span class="keyword">new</span> <span class="title class_">JsonRuleConfigParser</span>();</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">&quot;xml&quot;</span>.equalsIgnoreCase(configFormat)) &#123;</span><br><span class="line">      parser = <span class="keyword">new</span> <span class="title class_">XmlRuleConfigParser</span>();</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">&quot;yaml&quot;</span>.equalsIgnoreCase(configFormat)) &#123;</span><br><span class="line">      parser = <span class="keyword">new</span> <span class="title class_">YamlRuleConfigParser</span>();</span><br><span class="line">    &#125; <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">&quot;properties&quot;</span>.equalsIgnoreCase(configFormat)) &#123;</span><br><span class="line">      parser = <span class="keyword">new</span> <span class="title class_">PropertiesRuleConfigParser</span>();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> parser;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>也就是创建一个工厂类，专门用来「生产」parser。</p><p>这里如果就把创建parser的过程，放到load方法里，其实也不是不行。之所以拆出来，就是要把创建这一部分逻辑拆分开。所以，只有当创建逻辑比较复杂的时候才会使用工厂模式。</p><p>上边的实现还可以进一步优化，去除大量的if-else。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RuleConfigSource</span> &#123;</span><br><span class="line">  <span class="keyword">public</span> RuleConfig <span class="title function_">load</span><span class="params">(String ruleConfigFilePath)</span> &#123;</span><br><span class="line">    <span class="type">String</span> <span class="variable">ruleConfigFileExtension</span> <span class="operator">=</span> getFileExtension(ruleConfigFilePath);</span><br><span class="line"></span><br><span class="line">    <span class="type">IRuleConfigParserFactory</span> <span class="variable">parserFactory</span> <span class="operator">=</span> RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);</span><br><span class="line">    <span class="keyword">if</span> (parserFactory == <span class="literal">null</span>) &#123;</span><br><span class="line">      <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">InvalidRuleConfigException</span>(<span class="string">&quot;Rule config file format is not supported: &quot;</span> + ruleConfigFilePath);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">IRuleConfigParser</span> <span class="variable">parser</span> <span class="operator">=</span> parserFactory.createParser();</span><br><span class="line"></span><br><span class="line">    <span class="type">String</span> <span class="variable">configText</span> <span class="operator">=</span> <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="comment">//从ruleConfigFilePath文件中读取配置文本到configText中</span></span><br><span class="line">    <span class="type">RuleConfig</span> <span class="variable">ruleConfig</span> <span class="operator">=</span> parser.parse(configText);</span><br><span class="line">    <span class="keyword">return</span> ruleConfig;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> String <span class="title function_">getFileExtension</span><span class="params">(String filePath)</span> &#123;</span><br><span class="line">    <span class="comment">//...解析文件名获取扩展名，比如rule.json，返回json</span></span><br><span class="line">    <span class="keyword">return</span> <span class="string">&quot;json&quot;</span>;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//因为工厂类只包含方法，不包含成员变量，完全可以复用，</span></span><br><span class="line"><span class="comment">//不需要每次都创建新的工厂类对象，所以，简单工厂模式的第二种实现思路更加合适。</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RuleConfigParserFactoryMap</span> &#123; <span class="comment">//工厂的工厂</span></span><br><span class="line">  <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Map&lt;String, IRuleConfigParserFactory&gt; cachedFactories = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">  <span class="keyword">static</span> &#123;</span><br><span class="line">    cachedFactories.put(<span class="string">&quot;json&quot;</span>, <span class="keyword">new</span> <span class="title class_">JsonRuleConfigParserFactory</span>());</span><br><span class="line">    cachedFactories.put(<span class="string">&quot;xml&quot;</span>, <span class="keyword">new</span> <span class="title class_">XmlRuleConfigParserFactory</span>());</span><br><span class="line">    cachedFactories.put(<span class="string">&quot;yaml&quot;</span>, <span class="keyword">new</span> <span class="title class_">YamlRuleConfigParserFactory</span>());</span><br><span class="line">    cachedFactories.put(<span class="string">&quot;properties&quot;</span>, <span class="keyword">new</span> <span class="title class_">PropertiesRuleConfigParserFactory</span>());</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">public</span> <span class="keyword">static</span> IRuleConfigParserFactory <span class="title function_">getParserFactory</span><span class="params">(String type)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (type == <span class="literal">null</span> || type.isEmpty()) &#123;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="type">IRuleConfigParserFactory</span> <span class="variable">parserFactory</span> <span class="operator">=</span> cachedFactories.get(type.toLowerCase());</span><br><span class="line">    <span class="keyword">return</span> parserFactory;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这就是工厂方法模式，工厂方法模式实质上是为工厂类再创建一个简单工厂，也就是工厂的工厂，用来创建工厂类对象。</p><p>工厂模式的原理和实现都很简单，在实现时其实还有很多可以探究的细节，但在这里就不细说了。我们探讨的关键还是理解什么场景下应该用这种模式，它解决了什么问题。</p><p>对于工厂模式来说，应用的场景就是创建类的逻辑比较复杂时，比如需要有一堆if-else, 动态的决定要创建什么对象；或者虽然创建单个对象，但是创建本身的过程很复杂，调用方无需了解如何去创建。</p><h2 id="建造者"><a href="#建造者" class="headerlink" title="建造者"></a>建造者</h2><p>建造者模式(Builder), 其实应用非常广泛，我们打开任何一个项目，都可以看到很多XXXBuilder.</p><p>建造者模式使用的场景是：一个类有多个可配置项，使用方可以自由选择其中若干个进行配置。</p><p>如果采用不同构造方法来实现这种效果，面对的就会说无穷无尽的组合。</p><p>而如果采用set方法来配置，那么首先当前类就彻底失去了对配置项的控制，调用方可以随时随地调用set方法。另外，如果需要在配置项被配置好之后进行一些合法性的校验，这种方式也无法实现。</p><p>这种情况下的解决方案就是建造者模式。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ResourcePoolConfig</span> <span class="variable">config</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ResourcePoolConfig</span>.Builder()</span><br><span class="line"> .setName(<span class="string">&quot;dbconnectionpool&quot;</span>) </span><br><span class="line"> .setMaxTotal(<span class="number">16</span>) </span><br><span class="line"> .setMaxIdle(<span class="number">10</span>) </span><br><span class="line"> .setMinIdle(<span class="number">12</span>)</span><br><span class="line"> .build();</span><br></pre></td></tr></table></figure><p>实现也比较简单。</p><p>工厂模式是用来创建一组相关但不同的的对象，建造者模式则是对同一个对象进行不同方式的定制。</p><h2 id="单例"><a href="#单例" class="headerlink" title="单例"></a>单例</h2><p>关于单例是什么，怎么实现单例，作为经典的面试题，就不多说了。只说一下单例的问题，为什么单例主键变成了一种反模式。</p><p>单例的问题：</p><p>隐藏类之间的依赖关系；</p><p>同时，依赖的直接就是单例的实现，违反了基于接口而非实现的设计原则；</p><p>可测性不好: 无法被mock(Mockito中)；全局变量有可能导致测试之间互相影响；</p><p>不支持有参数的构造函数。</p><p>但是，单例模式的优点是有些场景下使用起来更简洁方便，所以也没必要直接就认定不能使用单例。</p><h2 id="代理-装饰器"><a href="#代理-装饰器" class="headerlink" title="代理&amp;装饰器"></a>代理&amp;装饰器</h2><p>代理模式和被代理类实现同一个接口，负责在业务代码执行前后附加其他逻辑代码（比如性能监控、计数等），然后用委托的形式调用被代理类完成业务功能，从而实现业务代码和框架代码的解耦。</p><p>装饰器模式是通过继承被装饰类，来实现功能的增强。比如java中inputStream系列类，就是装饰器模式的应用。</p><p>实现层面，代理模式和装饰器模式几乎一样，但是其意图不同。</p><p>代理模式中，代理类附加的是跟原始类无关的功能，而在装饰器模式中，装饰器类附加的是跟原始类相关的增强功能。</p><p>还是这个观点，对于设计模式，我们没必要扣字眼深究每种设计模式到底是什么，只要知道有这么一种技巧，可以处理类似的这种场景就可以了。</p><h2 id="适配器"><a href="#适配器" class="headerlink" title="适配器"></a>适配器</h2><p>适配器模式很简单，我们可以直接看例子。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">debug</span><span class="params">(String msg)</span> &#123; </span><br><span class="line">logger.log(FQCN, Level.DEBUG, msg, <span class="literal">null</span>); </span><br><span class="line">&#125; </span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">debug</span><span class="params">(String format, Object arg)</span> &#123; </span><br><span class="line"><span class="keyword">if</span> (logger.isDebugEnabled()) &#123; </span><br><span class="line"><span class="type">FormattingTuple</span> <span class="variable">ft</span> <span class="operator">=</span> MessageFormatter.format(format, arg); </span><br><span class="line">logger.log(FQCN, Level.DEBUG, ft.getMessage(), ft.getThrowable()); </span><br><span class="line">&#125; </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>应用场景包括：</p><p>兼容老版本接口；</p><p>应对不同的输入；</p><p>封装有缺陷的接口设计；</p><p>…</p><h2 id="门面"><a href="#门面" class="headerlink" title="门面"></a>门面</h2><p>门面模式为子系统提供一组统一的接口，定义一组高层接口让子系统更易用。这里主要是关系到一个接口的粒度应该有多大，从而平衡接口的通用性和易用性。</p><p>通常，粒度小的接口通用性肯定会好一些，但这样调用方就需要调用多个方法，必然不太好用。相反，如果接口粒度太大，就会有通用性不足的问题。</p><p>门面模式的作用就是解决这个问题。我是做在线教育的，有一个场景是在B端展示各种各样的题目，比如课前的题、课中的题、课后的题，每类中又分很多具体类型，每个类型有不同的数据来源。</p><p>这里，就需要权衡接口的通用性和可用性了。而门面模式刚好可以解决我们的问题。首先，我们要定义一个通用的模型，包括题干、参考答案、用户作答等，然后实现一批具体类型的接口。在此之上根据业务场景，封装一些门面层，门面层中只是简单的将子系统的数据组合起来。</p><h2 id="策略"><a href="#策略" class="headerlink" title="策略"></a>策略</h2><p>定义一族算法类，将每个算法分别封装起来，让它们可以互相替换。</p><p>实现方式是定义一个策略接口和一组实现这个接口的策略，运行时可以根据一些条件动态的选择执行哪个策略。</p><h2 id="模板方法"><a href="#模板方法" class="headerlink" title="模板方法"></a>模板方法</h2><p>模板方法模式在一个方法中定义一个算法骨架，并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下，重新定义算法中的某些步骤。</p><p>比如java的AbstractList 的addAll方法就是一个模板方法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">addAll</span><span class="params">(<span class="type">int</span> index, Collection&lt;? extends E&gt; c)</span> &#123;</span><br><span class="line">    rangeCheckForAdd(index);</span><br><span class="line">    <span class="type">boolean</span> <span class="variable">modified</span> <span class="operator">=</span> <span class="literal">false</span>;</span><br><span class="line">    <span class="keyword">for</span> (E e : c) &#123;</span><br><span class="line">        add(index++, e);</span><br><span class="line">        modified = <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> modified;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">add</span><span class="params">(<span class="type">int</span> index, E element)</span> &#123;</span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">UnsupportedOperationException</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="观察者"><a href="#观察者" class="headerlink" title="观察者"></a>观察者</h2><p>在对象之间定义一个一对多的依赖，当一个对象状态改变的时候，所有依赖的对象都会自动收到通知。也就是发布订阅模式。</p><p>观察者模式是一个比较抽象的模式，根据不同的应用场景和需求，有完全不同的实现方式，但是思想很容易理解，所以也就不列代码了。像我们使用mq, 也就是在使用观察者模式。</p><h2 id="责任链"><a href="#责任链" class="headerlink" title="责任链"></a>责任链</h2><p>将请求的发送和接收解耦，让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链，并沿着这条链传递这个请求，直到链上的某个接收对象能够处理它为止。</p><p>实际使用中，责任链模式的一种变体应用也很广泛，就是让链上的每个接收对象都处理一遍这个请求。</p><p>比如我们有一个拼装数据的逻辑，就是责任链变体的一个应用。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="type">boolean</span> <span class="title function_">fillUserDataCollection</span><span class="params">(Map&lt;Integer, UserDataCollection&gt; userExpandMap, Map&lt;Integer, UserDataCollection&gt; originUserExpandMap)</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> !fillingServices</span><br><span class="line">                .stream()</span><br><span class="line">                .map(filler -&gt; filler.fillUserDataWithFallback(userExpandMap, originUserExpandMap))</span><br><span class="line">                .collect(toSet())</span><br><span class="line">                .contains(<span class="literal">false</span>);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>UserDataCollection中包含大量字段，每个fillingService负责其中一部分数据的补充。</p><p>责任链的核心思路就是拆分，将大类拆成小类。这也是解决代码复杂度的重要手段之一。</p><h2 id="经验总结"><a href="#经验总结" class="headerlink" title="经验总结"></a>经验总结</h2><p>对于如何写出好的代码，最关键的是要先建立起心理上的认知，知道什么是好代码，也有写好代码的意愿。在此基础上，要对面向对象的思想和各类设计原则有充分的理解，知道为什么好代码要符合这些原则。</p><p>实践上，写出好代码的最佳方式就是持续重构。一开始业务发展不明朗的时候，代码无法设计到最好是很正常的。而随着业务的发展，我们会逐渐意识到代码里存在的问题，这时候可以翻翻设计模式中有没有什么现成的解决方案，然后应用到自己的项目中。</p><p>最后，说一下，这篇文章中的很多内容实际上来自极客时间的专栏《设计模式之美》。我觉得这个专栏是讲设计模式最好的一个专栏，推荐大家也可以去看看。点击下方图片购买即可。</p><p><img src="/img/design.jpg" alt="design"></p><p>原文地址: <a href="https://lichuanyang.top/posts/58527/">https://lichuanyang.top/posts/58527/</a></p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-设计模式学了不用会忘怎么办？"><a href="#Q-设计模式学了不用会忘怎么办？" class="headerlink" title="Q: 设计模式学了不用会忘怎么办？"></a>Q: 设计模式学了不用会忘怎么办？</h3><p>这很正常，关键在于<strong>学习方法要对</strong>。不要试图一次性背下 23 种模式的定义和结构——这种做法必然学了就忘。正确的路线是：先理解 SOLID 等设计原则的思想，然后在实际项目中遇到代码设计问题时，翻翻设计模式中有没有现成的解决方案，有目标地用、持续地重构。用得多了，对应场景和方案的映射就自然内化了，不需要刻意记忆。</p><h3 id="Q-哪种设计模式最常用？"><a href="#Q-哪种设计模式最常用？" class="headerlink" title="Q: 哪种设计模式最常用？"></a>Q: 哪种设计模式最常用？</h3><p>创建型模式中<strong>工厂模式</strong>和<strong>建造者模式</strong>用得最多——几乎任何有一定复杂度的项目都能在依赖注入和对象构造中看到它们的身影。结构型模式中<strong>代理模式</strong>（如 AOP、RPC 框架）、<strong>适配器模式</strong>（兼容不同接口）和<strong>装饰器模式</strong>（如 Java IO 流）是最常见的。行为型模式中<strong>策略模式</strong>、<strong>模板方法模式</strong>和<strong>责任链模式</strong>在实际业务开发中出场率最高，因为它们直接对应”根据条件选择算法””定义流程骨架””链式处理请求”这三种高频场景。</p><h3 id="Q-设计模式和设计原则哪个更重要？"><a href="#Q-设计模式和设计原则哪个更重要？" class="headerlink" title="Q: 设计模式和设计原则哪个更重要？"></a>Q: 设计模式和设计原则哪个更重要？</h3><p>设计原则更重要。原则是”为什么”，模式是”怎么做”。SOLID、KISS、DRY、迪米特法则等设计原则指导你判断代码质量的得失，是所有好代码的共同基础。设计模式只是这些原则在某些典型场景下的具体实现方案。理解了原则，即使不记得某个模式的具体实现，也能写出好代码；反过来只记模式不识原理，就容易生搬硬套。</p><h3 id="Q-单例模式为什么变成了反模式？"><a href="#Q-单例模式为什么变成了反模式？" class="headerlink" title="Q: 单例模式为什么变成了反模式？"></a>Q: 单例模式为什么变成了反模式？</h3><h2 id="主要问题在于：单例隐藏了类之间的依赖关系——调用方的代码直接依赖-Singleton-getInstance-，从接口上看不出它用了全局状态；可测试性差——单例无法被-Mock，全局状态可能导致测试之间互相影响；不支持有参数的构造函数——限制了灵活性。但这不意味单例完全不能用，在确实需要一个进程内唯一实例且不涉及复杂测试场景时，单例仍然是最简洁的选择。关键是理解它的代价，而不是迷信”单例一定不好”。"><a href="#主要问题在于：单例隐藏了类之间的依赖关系——调用方的代码直接依赖-Singleton-getInstance-，从接口上看不出它用了全局状态；可测试性差——单例无法被-Mock，全局状态可能导致测试之间互相影响；不支持有参数的构造函数——限制了灵活性。但这不意味单例完全不能用，在确实需要一个进程内唯一实例且不涉及复杂测试场景时，单例仍然是最简洁的选择。关键是理解它的代价，而不是迷信”单例一定不好”。" class="headerlink" title="主要问题在于：单例隐藏了类之间的依赖关系——调用方的代码直接依赖 Singleton.getInstance()，从接口上看不出它用了全局状态；可测试性差——单例无法被 Mock，全局状态可能导致测试之间互相影响；不支持有参数的构造函数——限制了灵活性。但这不意味单例完全不能用，在确实需要一个进程内唯一实例且不涉及复杂测试场景时，单例仍然是最简洁的选择。关键是理解它的代价，而不是迷信”单例一定不好”。"></a>主要问题在于：单例<strong>隐藏了类之间的依赖关系</strong>——调用方的代码直接依赖 <code>Singleton.getInstance()</code>，从接口上看不出它用了全局状态；<strong>可测试性差</strong>——单例无法被 Mock，全局状态可能导致测试之间互相影响；<strong>不支持有参数的构造函数</strong>——限制了灵活性。但这不意味单例完全不能用，在确实需要一个进程内唯一实例且不涉及复杂测试场景时，单例仍然是最简洁的选择。关键是理解它的代价，而不是迷信”单例一定不好”。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/58527/</id>
    <link href="https://lichuanyang.top/posts/58527/"/>
    <published>2021-03-04T11:26:30.000Z</published>
    <summary>从面向对象的四大特性出发，帮助程序员真正理解设计模式的本质和正确的学习心态。</summary>
    <title>设计模式的原则，设计模式究竟是什么</title>
    <updated>2026-06-27T02:31:54.490Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="感悟" scheme="https://lichuanyang.top/tags/%E6%84%9F%E6%82%9F/"/>
    <content>
      <![CDATA[<p>大家好，我是流沙，一名近六年工作经验的程序员。我的工作经历里，包含了整体文化、氛围差别非常大的公司，和很多不同类型的人合作过，观察到过很多没效率的现象。同时，我一直觉得我自己的开发效率是非常高的。整个职业经历里，我很少在八小时的工作时间外处理任务性的事项。即使有时迫于公司制度不得不进行加班，也是在执行自己的学习计划，或者进行一些深层的思考。因此，我想做一个系统性的关于开发效率的总结，分享给大家。</p><span id="more"></span><p>首先，列两个非常没效率的场景。</p><ul><li>小A开发过程中碰到一个没见过的报错，于是抱着电脑去找团队里的大牛小X, 小X努力的搜集相关的各种必要信息。与此同时，小A就在旁边看着，并且不时的和小X还有旁边的小Y聊着天。</li><li>小C拿到一个大型需求，匆匆看了一遍，接着就开始开发。开发一半了，发现产品稿上有个点看不明白，又去和产品问。问的过程中产品又想起来别的事，又说了起来。最后聊了半天，小C发现自己之前的工作基本白做了。</li></ul><p>虽然有夸张的成分，但是这些事也是确实每时每刻都在发生的。造成这些现象的原因很多，我们今天就好好剖析一下。在剖析效率问题之前，我们需要先看看程序员日常都要做哪些事。其中对工作效率影响比较大的，我把他们主要归为三类：</p><ol><li>日常开发</li><li>沟通</li><li>排查和解决问题</li></ol><p>接下来，我们就对这三项逐一分析，如何提升工作效率。</p><h2 id="日常开发"><a href="#日常开发" class="headerlink" title="日常开发"></a>日常开发</h2><p>日常开发中都有什么情况会造成效率下降呢，我列举几个：</p><ul><li>像上边小C一样的返工行为</li><li>日常工作中大量重复劳动，比如写sql，写模板代码</li><li>本来是一个很小的改动点，改了才发现这也要改，那也要改；更严重的是，根本不知道「那也要改」，结果一改就崩</li><li>个人注意力不集中，老走神</li></ul><p>可以看到，这里边大部分是和代码设计有关的。对于设计，其实就两句话，一是要设计，二是要设计好。</p><p>也就是说，首先，你得知道开发之前是得有设计这么个环节的。软件工程是一门科学，它有概要设计、详细设计这些步骤是有道理的。要设计到什么程度再进开发呢？我的观点是越细越好，理论上，除了实际把代码码出来，其他所有事都可以在设计阶段做完。</p><p>关于设计的好不好，一个重要的评价标准，就是后续改起来是否容易。如果一个变化的原因会导致多处代码修改，那就是一个不好的设计。如何提升这一点，需要去学习设计模式、架构方面的知识，没有速成之路。</p><p>另外，关于注意力的问题，当然大家可以借助番茄钟这样的工具。但是工具永远都只能是辅助，最重要的还是自己去提升主观上的自控力。</p><h2 id="沟通"><a href="#沟通" class="headerlink" title="沟通"></a>沟通</h2><p>接着说沟通这块，沟通分主动沟通和被动沟通。主动沟通就是你自己这边发起，要去找其他人问问题、讨论；被动沟通则是别人发起，来问你问题或者讨论事。</p><p>主动沟通的主要问题就是越聊越远，聊着聊着就不知道聊哪去了。这里我给两个建议，一是发起沟通前一定要想清楚自己想通过这次沟通达到什么目的，不能有个模糊的想法就迷迷糊糊的去了，这种沟通就是最大的时间杀手；二是可以列一个沟通清单，沟通过程中话题扩散其实是个常见的现象，我觉得没必要刻意去杜绝这种事，关键是要保证自己本来想聊的聊出结果来，这就需要一个可以用来check的清单。</p><p>被动沟通其实是一种很容易打乱工作节奏的事，但是很多情况下无法避免，比如突然又线上问题要处理，领导有事找你，等等。我们能做的就只有减少不必要的被动沟通次数。比如自己提供的功能，写好文档和注释，其他人就不需要靠问你就能看明白。方法名、返回值等团队内要建立起一些固有约定，大家都遵守，也就不需要问才能了解了。</p><p>另外还有一点是做好任务的拆分，做若干个小任务，而不是做一个大任务。这样，当被迫打断工作时，「切换上下文」的成本会低一些。</p><h2 id="排查和解决问题"><a href="#排查和解决问题" class="headerlink" title="排查和解决问题"></a>排查和解决问题</h2><p>这也是很重要的一个方面。对有些人来说，查Bug改Bug的时间，可能会远高于开发新需求。对于这个，我们的主要目标就是降低排查难度，减少排查时间。可以从下面几个方面做。</p><ol><li><p>设法尽早暴露问题，比如通过写单元测试。同样一个问题，出现在一个小模块里，和出现在一个复杂系统中，排查难度是不言而喻的。</p></li><li><p>要有足够多的信息，比如日志，比如各类监控数据。就像我们体检一样，只有先看得到那些异常指标，才能再去针对性的详细排查。否则什么指标没有，就只能两眼抓瞎乱查了。</p></li><li><p>再一个就是排查方式，每个人都需要逐渐积累经验，建立起相对固定的排查问题的套路。比如我，习惯就是遇到问题先把各种能看到的信息都扫一遍，从中发现异常指标，指标看不懂的话就借助搜索引擎或者他人来理解，然后根据指标思考有什么可能原因，再逐步去验证。这里边很重要的一点就是什么样的问题要自己思考，什么样的问题应该借助于他人或者搜索引擎。像上边小A的做法，就是非常不可取的，问别人或者搜索引擎的问题，一定是非常具体的，你随手一个报错发过来，别人首先得花大量时间搜集信息。一个1小时搞得定的问题，强行去问别人，很可能要花掉两个人一下午。</p><p>什么叫具体呢，比如遇到一个ClassNotFoundException, 如果你压根不知道它是什么意思，那去搜一下没有问题。而假如你已经知道它的意思就是找不到类，那再去搜索就没有任何意义，这是要做的是思考这个类为什么找不到，有哪些可能原因。</p></li></ol><p>除了上边这些之外，其实还有一类原因，就是996大环境大家主动选择的摸鱼。对这一点，我还是建议大家快速干完活之后腾出时间提升自己，摸鱼无益。再一点就是用脚投票，多去找没有加班文化的公司、团队，用自己的努力让他们发展的更好。</p><p>大家有想法也可以在评论区或者公众号( Mobility )和我交流。</p><p>原文地址: <a href="https://lichuanyang.top/posts/3423/">https://lichuanyang.top/posts/3423/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/3423/</id>
    <link href="https://lichuanyang.top/posts/3423/"/>
    <published>2021-02-01T09:38:11.000Z</published>
    <summary>六年工作经验的程序员分享提升开发效率的系统性方法，强调在八小时内高效完成工作。</summary>
    <title>我提升开发效率的经验</title>
    <updated>2026-06-27T03:45:23.479Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="架构设计" scheme="https://lichuanyang.top/categories/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="设计模式" scheme="https://lichuanyang.top/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
    <category term="感悟" scheme="https://lichuanyang.top/tags/%E6%84%9F%E6%82%9F/"/>
    <category term="软件工程" scheme="https://lichuanyang.top/tags/%E8%BD%AF%E4%BB%B6%E5%B7%A5%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>对于一个程序员来说，日常最常说的词恐怕就是「复杂」了，这段代码太复杂了，这个逻辑太复杂了，所以，在这篇文章里，我们就好好掰扯掰扯「复杂」到底是怎么产生的，又要怎么去避免。</p><span id="more"></span><h2 id="复杂度的产生"><a href="#复杂度的产生" class="headerlink" title="复杂度的产生"></a>复杂度的产生</h2><p>我们先列一下当我们在说「复杂」的时候，到底在说什么。</p><ul><li>没有模块划分，整个一大坨代码摊在那儿</li><li>大量的重复代码</li><li>代码整体逻辑看不懂</li><li>本来一个很简单的需求，改的时候发现这也要改，那也要改</li><li>代码没办法迁移，没办法复用</li><li>整天有莫名的bug出现</li><li>…</li></ul><p>上边列的这些问题，其实很多之间是互为因果的，比如模块划分的不好，导致了代码逻辑看不懂，也导致了代码没法复用，代码没法复用，又导致了大量重复代码，重复代码又进一步导致代码逻辑看不懂。</p><p>所以，下面我们尝试着规整一下这些问题，看看问题到底在哪。</p><p>我觉得，首先可以把代码复杂度的问题简单分个类，一类是代码本身的问题，一类是代码对现实世界的表现力的问题。前者主要关乎代码整体的整洁程度，以及容不容易进行持续的维护；后者则主要影响代码容不容易理解，别人看代码时是否需要大量的背景知识。</p><p>对代码本身问题来说，以我的观察，导致代码过度复杂的首要原因就是模块拆分不清晰，甚至没有模块拆分。像上边列的重复代码、无法复用、统一逻辑多处修改等问题，本质原因都在模块拆分。另外有一些命名上的问题，会导致代码进一步不可维护。</p><p>关于代码对现实世界的表现力，我觉得一个很大的问题就是代码和现实割裂太严重，导致单纯依靠业务上的背景知识，并不能理解代码的实现方式；同时，当你以一个不符合现实的设计去实现现实需求的时候，可想而知，每一步都会很难。比如说，一个展示用户机票的业务，上游各航司的数据key都是「用户证件号」，那你这儿的key应该是什么呢，也是证件号吗。如果这么做了，然后再如果做了分库分表，可以想一下，「查询用户最近一张机票」这么一个基础功能，实现起来要有多复杂。</p><p>其实理到这儿，会发现已经能基本涵盖一开始列的那些问题了。像bug多这类问题，其实是逻辑复杂的一种表现。因为逻辑复杂，看不懂，所以Bug才多。</p><p>另外，有一个问题是上边说的每个点，随着程序规模的增加，会出现一个指数级的放大效应。相信很多人可以感受的到，如果有一个5天工作量的需求，和5个1天工作量各自独立的需求，最后做下来，前者实际花费的时间和最后产生的bug都要多很多。</p><p>所以，接下来我们就对上边这些提供一些解决方案</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><h2 id="拆分"><a href="#拆分" class="headerlink" title="拆分"></a>拆分</h2><p>首先，重中之重，就是做好拆分，要做好每个层次的拆分，比如微服务的边界怎么划分，哪些内容放到一个类里，哪些内容放到一个方法里。不夸张的说，模块拆分的好，可以解决开发上80%以上的问题。</p><p>比如，「把大象装进冰箱需要三步，一打开冰箱门，二把大象放进去，三关上冰箱门」，这就是一个非常合理的模块拆分。当然，对于三步中的每一步来说，都可以进一步细化。比如打开冰箱门包括：握住把手，用力拉两步；把大象放进去，包括，找一个大力士，举起大象，放到冰箱里，松手，四步。</p><p>拆分的核心要素是要拆的合理。对于业务逻辑来说，就是要符合对现实世界的认知。比如上面的例子里，你不能强行包一个「用力拉，找大力士，举起大象」这样的方法。但是，这个现象，在实际的业务开发中，实际上非常常见。很多人看到一个方法太长，知道要做下拆分，结果只是随机的从代码里拉住一块，抽了出来。这样，实际上只能让代码看着好一点，无法达到降低复杂度的效果。</p><p>另外，拆分没有止境，每个子模块，都可以进一步拆分成更细的子模块，直到这个模块已经足够容易理解。</p><h2 id="高内聚"><a href="#高内聚" class="headerlink" title="高内聚"></a>高内聚</h2><p>在这儿说内聚，主要就是说要把复杂度控制在最小的范围内。以上边的例子来说，「找大力士」这一步就是最复杂的，因为根本没有这样的大力士。但是，这儿的复杂度只是这个小模块的内部实现，只有这一个小模块复杂，在上层或者其他模块是不应该感受到这个复杂度的。</p><p>但是，实际开发中，我们经常看到一些底层很复杂的结构，不断的暴露在系统各个层次、各个模块中，导致复杂度无法收敛。这样，随着项目规模的扩大，整体的复杂度就会呈指数级上升。</p><h2 id="命名"><a href="#命名" class="headerlink" title="命名"></a>命名</h2><p>这个就比较细节了。命名的核心要求一是要准确，无论是变量名、方法名，看到这个名字，不需要去看代码细节，就能大体了解相关的逻辑。</p><p>再一个，命名要具备语境下的一致性。比如程序员，你不能一会叫coder, 一会叫programmer, 否则别人理解起来就很混乱。</p><p>其实，到这儿已经就说完了，是不是感到比预想的要简单。事实也是如此，只要做好模块的拆分，合理的将每段代码写在它该在的位置，开发就没有那么复杂。</p><p>原文地址: <a href="https://lichuanyang.top/posts/33852/">https://lichuanyang.top/posts/33852/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/33852/</id>
    <link href="https://lichuanyang.top/posts/33852/"/>
    <published>2021-01-04T10:12:34.000Z</published>
    <summary>从模块划分、消除重复、解耦等方面，系统分析软件复杂度的产生原因和降低方法。</summary>
    <title>通过合理的设计降低软件开发复杂度</title>
    <updated>2026-06-27T03:46:06.796Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="感悟" scheme="https://lichuanyang.top/tags/%E6%84%9F%E6%82%9F/"/>
    <content>
      <![CDATA[<h2 id="什么是信息衰减"><a href="#什么是信息衰减" class="headerlink" title="什么是信息衰减"></a>什么是信息衰减</h2><p>看到这个标题，是不是有种要讲信息学的感觉。其实并不是，只是最近观察到一些事情，有些感触所以说一下。最近在网上碰巧看到过两个言论，恰好事实我是比较了解的，所以感慨，二手的信息实在不可信。</p><span id="more"></span><h2 id="实际案例"><a href="#实际案例" class="headerlink" title="实际案例"></a>实际案例</h2><p>第一个，是北京西城德胜片区有一个小学叫三帆附小，因为可以直升三帆中学，这个在西城数一数二的初中，所以一直十分火爆。这个学校今年因为教室不够，招生由七个班缩减到了四个班，所以卡了落户四年，即落户不足四年的都要调剂。其实，调剂也不差，因为调剂校里有西城排名前五，而且学位极度充足的西师附小，也有雷锋消息这样的优质直升校。甚至有人买三帆附小学区房就是为了去调剂，因为西师附小的学区房要更贵。</p><p>然后，在一个公众号的高赞评论里，看到了「德胜」今年卡落户「两三年」这样的言论，言论作者还顺便以此为依据表达了对买学区房这种行为从智商上的鄙视。</p><h2 id="衰减的三个层次"><a href="#衰减的三个层次" class="headerlink" title="衰减的三个层次"></a>衰减的三个层次</h2><p>结合事实和最后言论，其实大概猜的出整个传播链条中信息的变化。德胜的三帆附小逐渐变成了德胜，卡落户四年变成了落户两三年的被调剂，又变成了卡两三年。同时，调剂去的是西师附小这种关键信息被丢失。</p><p>第二个事，上周望京地区除了新冠的病例，然后各个社区在周末组织全员的核酸检测。其中有个检测点，不知道咋回事，排队的人特别多。不过实际上大部分检测点都挺好排队的，有的点甚至你到了还要等一会，凑够一组5个或者10个人才能测。</p><p>然后，这周我恰好听到一个财经类的大V说这个事情，信息变成了「望京地区」几万人寒风中排队做核酸。其实，他说的确实也没错，也没啥恶意，只是提一嘴疫情又有些严重了。不过大家也能感受到，稍微少了那么一些细节，整个感觉就不一样了。你看这个简版的表述，会感觉望京的小伙伴们还挺可怜的，但是实际上，大部分人都轻轻松松、开开心心的就检测完了，至于排大队的那个点，也随时可以去其他点检测。</p><p>所以，看这两件事，虽然都是很小的事，我也彻底对二手信息失去信任。有些信息，传播的过程中，遗失或者微调一些关键信息，会导致整个意思大不相同。再者，有些人说话的时候其实只是顺嘴一提，人家没把这个当成一个重要的事，所以也不会去深究信息到底准不准。但是听的人对这个事情重要程度的判断是不一样的，你如果拿这种信息去做决策，可以预见，会有多惨。</p><p>那么，</p><h2 id="如何对抗衰减"><a href="#如何对抗衰减" class="headerlink" title="如何对抗衰减"></a>如何对抗衰减</h2><p>什么算是一手的信息呢，我这里简单列一些：</p><ul><li>事实。就是实际发生的事，比如今天某地气温X这样的确定性事件，而非某地今天很冷这样的主观判断。</li><li>专业性的书籍。书毕竟会有校对的过程，也会经过无数读者的考验，所以可信度会高很多</li><li>公开政策。这个不细说了，因为这个就是标准。</li><li>可靠来源的数据</li></ul><p>总而言之，就是希望大家多去接触一手信息，杜绝二手信息，做重要决策的时候要尤其重视信息的可信度。</p><h2 id="原文地址-https-lichuanyang-top-posts-54296"><a href="#原文地址-https-lichuanyang-top-posts-54296" class="headerlink" title="原文地址: https://lichuanyang.top/posts/54296/"></a>原文地址: <a href="https://lichuanyang.top/posts/54296/">https://lichuanyang.top/posts/54296/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/54296/</id>
    <link href="https://lichuanyang.top/posts/54296/"/>
    <published>2020-12-29T03:28:44.000Z</published>
    <summary>通过两个亲身经历的案例，分析信息在传播过程中的失真和衰减，揭示二手信息的不可靠性。</summary>
    <title>信息传播过程中的衰减</title>
    <updated>2026-06-27T01:01:10.531Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="感悟" scheme="https://lichuanyang.top/tags/%E6%84%9F%E6%82%9F/"/>
    <category term="记录" scheme="https://lichuanyang.top/tags/%E8%AE%B0%E5%BD%95/"/>
    <content>
      <![CDATA[<p>从今年十一左右开始看房，到上个月初就定了下来。其实没想到会定那么快，只能说缘分到了吧。回想起当时的经历，感觉还是要记录一下，毕竟是人生中最大的几件事之一了。</p><span id="more"></span><h2 id="背景与需求"><a href="#背景与需求" class="headerlink" title="背景与需求"></a>背景与需求</h2><p>其实从年初孩子出生就在考虑买学区房的事情了，不过自己本来房子要到明年初才满五，没法赶在西城731政策之前上车了，算是有点小遗憾。</p><p>首先针对要不要买学区房，我比较认同网上的一类观点：买房子实际上是在买圈子。买的是你的邻居是些什么人，你的孩子要和什么人做同学。同样价格，有的人会选择学区好一点，住的差一点，有的人会想住的舒适一些，学区差点。想了想，我还是愿意和第一类人混一起。</p><p>因此最后和爱人定下了基本的基调：在保障基本居住质量的前提下，去追求能力范围内最好的学区。算了下预算，期望控制在850万左右，最高不超过950万。户型别太差的小两居就可以。</p><h2 id="看房区域分析"><a href="#看房区域分析" class="headerlink" title="看房区域分析"></a>看房区域分析</h2><p>首先做了一轮初步筛选，海淀大部分地方因为离我们俩工作地都太远，所以都不再考虑了。金融街的价格压力太大，因此首先将范围缩小到西城的月坛和德胜两个片区，先看看房子情况。</p><p>十一期间先去月坛看了次房子，主要是育民对口的小区，白云观、白云路、汽南、真武庙啥的。说实话，心理落差非常大，第一印象真的就是脏乱差，几乎就要放弃这个地方了。</p><p>所以之后两周的看房重点，主要就在德胜了。往德胜跑了好多趟，几乎把德胜的小区看了个遍。整体来说，小区环境第一感觉确实比月坛上次看的要好。中间一度几乎要定了，但是二次看房的时候，又感受了一遍德胜的小胡同，觉得无法接受，遂放弃。这样的话，也就意味着放弃了德胜大部分小区，只留了育翔对应的普天大院，三帆附小对口的裕中东里中的几栋楼，西师附小对口的六铺炕一二区做备选。不过这三个地方，都有比较大的难受的地方，普天大院和六铺炕房子太老，三帆附小对口房子太多，交易量又大，731之后调剂风险过高。</p><p>考虑了这几点之后，觉得心里确实会有个疙瘩，再加上觉得德胜学区属性太重，不像月坛本身有地段加成，站岗的风险也有点大。因此继续等这几个小区新出房子的同时，又把东城和朝阳纳入到了考虑范围内。主要是东城的安交和和平里片区，朝阳的陈经纶珠江帝景、朝阳外国语。还没有去实地看房的时候，先在网上看了下。存量房中，珠江帝景只买的起全北的，只能先不考虑了。另外一个综合看倒没什么大问题，但是居住品质并没有感受到相比西城有明显的提升，所以觉得不大合适。</p><p>所以，这几个还是仅仅放到了备选项里，有非常满意的房子出来的话可以考虑。然后又回月坛看了几次房。神奇的是，这几次，观感明显要比第一次好的多。不知道是习惯了还是经纪人的问题，可能第一次把月坛最烂的房子带我们看了个遍？</p><p>这几次看房过程中，也基本熟悉了月坛各小区的情况。但是真的没有什么各方面都满意的房子。所以跟几地的中介都说了情况，决定等新房源出现再看。</p><h2 id="最终决策"><a href="#最终决策" class="headerlink" title="最终决策"></a>最终决策</h2><p>幸运的是，10月底就有一套各方面都非常好的房子，有小区环境，2000年的房子，在周边是最新的，近地铁，户型利用率也比较高，朝向、采光什么的也都可以接受。当然，除了价格。不过经纪人还是建议我们去约房东聊聊。当天从下午四点一直谈到十点，业主大哥确实是个很爽快，也很好的人，但是双方价格差距确实大，再者房子是他父母的，他只能一遍遍打电话去做父母的工作，最终也没谈成。</p><p>只能说，绿中介确实给力，业主大哥也确实给力，我们晚上回去之后，中介又去做大哥工作，大哥又去做父母工作，最后在快12点的时候答应了我们的报价。然后半夜12点，我们又跑回去迁合同。</p><p>这个过程，我也基本摸清了中介工作的套路哈哈。就是先想办法把双方约过去谈，然后硬磨，等双方心理都快崩溃了，价格就好谈了。^_^</p><p>现在回想起一个多月的整个历程，还可以淡定的描述出来。其实，那个时间里，心理压力其实是非常大的。一方面选房子就很难选，同时其实还很纠结，又怕买在高位，又怕多校政策执行的太狠。现在尘埃落定之后，心态其实还可以，毕竟已经尽可能降低风险了，只能尽人事，听天命。房价站岗和被调剂到渣小，这两件事别同时发生，对我来说就是可以接受的。</p><p>当然，还是期待有个好结果^_^。</p><p>原文地址: <a href="https://lichuanyang.top/posts/5236/">https://lichuanyang.top/posts/5236/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/5236/</id>
    <link href="https://lichuanyang.top/posts/5236/"/>
    <published>2020-12-12T09:12:40.000Z</published>
    <summary>2020 年北京西城学区房看房经历记录，分享选房思路和学区政策对购房决策的影响。</summary>
    <title>看房小记-2020北京学区</title>
    <updated>2026-06-27T01:04:06.004Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="读书笔记" scheme="https://lichuanyang.top/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"/>
    <category term="个人成长" scheme="https://lichuanyang.top/tags/%E4%B8%AA%E4%BA%BA%E6%88%90%E9%95%BF/"/>
    <category term="投资" scheme="https://lichuanyang.top/tags/%E6%8A%95%E8%B5%84/"/>
    <category term="读书感悟" scheme="https://lichuanyang.top/tags/%E8%AF%BB%E4%B9%A6%E6%84%9F%E6%82%9F/"/>
    <content>
      <![CDATA[<p>《文明、现代化、价值投资与中国》这本书，是著名的价值投资者李录先生所著的，分别讲述了文明的发展、现代化的产生、价值投资，并且结合中国的实际情况对这些内容作了阐述。个人感觉是一本价值极高的书，值得反复阅读，对于帮助我们认识这个世界的运行逻辑，建立正确的投资理念，都有非常大的帮助。</p><span id="more"></span><p>从书名可以看出，这本书其实讲了两个相对独立的话题，一个是文明、现代化，一个是价值投资，并分别阐述了这些和中国的关系。</p><h2 id="文明与现代化"><a href="#文明与现代化" class="headerlink" title="文明与现代化"></a>文明与现代化</h2><h2 id="农业文明的局限性"><a href="#农业文明的局限性" class="headerlink" title="农业文明的局限性"></a>农业文明的局限性</h2><p>作者首先用了很大篇幅讲述了文明产生与演化的整个过程。其中尤为精彩的就是对于农业文明天花板的论述。也就是农业文明的产出，本质上来源于光合作用。植物通过光合作用生长，而喂养牲畜也需要消耗植物。而光合作用产生的能量上限，受制于土地面积和土地的单位产出，这两者都有非常明显的上限，所以自然资源也就有了上限。有限的自然资源和近乎无限的人口增长，决定了人口最终只能通过非自然灾害来消化。</p><h2 id="现代化的产生与本质"><a href="#现代化的产生与本质" class="headerlink" title="现代化的产生与本质"></a>现代化的产生与本质</h2><p>现代化的产生源自新大陆带来的环大西洋自由市场经济和同时代的科学革命。1776年是一个重要的年份，这一年发生了三件事情，对现代化的发展都起到了至关重要的作用。一是亚当斯密出版了《国富论》，《国富论》讨论的核心问题是大西洋经济的本质，即自由贸易；二是独立宣言发表，一个深受亚当斯密理论影响的全新政权诞生，对于自由市场的发展也会提供非常大的帮助；三是蒸汽机的产生，生产力得以大幅度提高。从此，自由市场和科技的结合，释放出惊人的力量。</p><p>自由市场本身是一个可以不断自我进化、自我进步、自我完善的机制。现代科技的介入使得这一过程异常迅猛。这样，在相互竞争的的不同市场之间，最大的市场最终会成为唯一的市场。</p><p>任何组织如果离开这个市场，都会不断落后，一如当年的苏联。</p><p>我国这几年的很多举措，诸如一带一路，RECP，都是在尽力提升能够影响到的自由市场规模。我们要感谢川建国，不断退群，自己离开这个最大的自由市场。</p><h2 id="现代化与中国"><a href="#现代化与中国" class="headerlink" title="现代化与中国"></a>现代化与中国</h2><p>那么，现代化是否有可能诞生在中国呢。答案是否定的。因为现代化的诞生过程中，私人资本一直起着至关重要的作用。而中国自秦汉起，就一直有着非常强有力的中央政府，私人资本难以兴起。另外，新大陆的发现，其初衷原本就是要寻找中国。中国作为当时的文明中心，并没有动力去寻找更富足的地方。</p><p>可以说，现代化的产生原本就源自一系列非常偶然的因素。原本野蛮的盎格鲁撒克逊人，开中了这张彩票，得以弯道超车。但历史终究会回到她应该在的位置上。所以我们完全没有必要遗憾现代化为什么没有在中国产生。</p><p>过去几十年间，中国的快速发展，其实是在走其他人走过的路。这种情况下，一个强有力的政府可以起到非常重要的作用。而随着我们逐渐赶上了其他人的进度，需要去探索下一步的方向时，自由市场将会逐渐发挥出优势。</p><p>文化上，在中国，恢复传统文化的正确地位其实别无选择。文化的产生，有很深的历史和地理渊源，早已深入骨髓。随着经济发展，人们的精神需求不断提高，很可能会出现中国式的文艺复兴，让国人重新发现中国文化最精华的部分。</p><p>总有一天，大部分国人会认识到，二十四史、唐诗宋词、四大名著、论语、道德经、孙子兵法，这些渊远的文化遗产，是要远比“感恩印第安人被我们杀”这样的文化优质的多的。</p><h2 id="价值投资"><a href="#价值投资" class="headerlink" title="价值投资"></a>价值投资</h2><h2 id="投资行业的现状"><a href="#投资行业的现状" class="headerlink" title="投资行业的现状"></a>投资行业的现状</h2><p>说起投资，不得不看看当前整个行业的状态。当前投资业最大的问题当然就是没有办法评价一个产品的好坏。投资是一个长期的过程，短期的涨跌可以说没有任何参考价值。而当前基金的盈利模式，导致业绩很差的基金经理同样可以收很多钱。</p><p>作者讲到了一个重要的观念，也就是受托人责任。只有行业内大部分人都能有这个概念时，行业才能向好的方向发展。</p><h2 id="投资的历史经验"><a href="#投资的历史经验" class="headerlink" title="投资的历史经验"></a>投资的历史经验</h2><p>长期来看，股票的收益远高过其他任何类型资产几个数量级。在中国，这个情况也不例外。有两方面的原因，一是通货膨胀，二是来源于经济增长，股票事实上可以很大程度体现出GDP的增长。</p><h2 id="价值投资的理念"><a href="#价值投资的理念" class="headerlink" title="价值投资的理念"></a>价值投资的理念</h2><p>价值投资的基本理念只有四个。</p><h3 id="股票代表对公司的所有权"><a href="#股票代表对公司的所有权" class="headerlink" title="股票代表对公司的所有权"></a>股票代表对公司的所有权</h3><p>投资的收益来源于公司的发展，你之所以能获得收益，是因为你给公司的发展带来了帮助。这不是零和游戏。而靠赌市场的波动盈利，这是投机行为，是和赌博没有区别的零和甚至负和游戏。因此，价值投资是一条正道、大道。我们为社会创造价值，然后获得收益，仅此而已，我们不希望通过欺骗、通过剥削别人去获取收益。</p><h3 id="对市场的认知"><a href="#对市场的认知" class="headerlink" title="对市场的认知"></a>对市场的认知</h3><p>市场只是一个为你提供服务的机构而已。你可以在这里获得购买的机会，也可以在要用钱的时候卖出。市场透露出的其他信息，诸如一支股票的短期涨跌，纯粹是噪音而已。</p><h3 id="安全边际"><a href="#安全边际" class="headerlink" title="安全边际"></a>安全边际</h3><p>投资本质是对未来的预测，而预测就必然不可能百分之百正确。因此，提升安全边际，就尤为重要。</p><h3 id="建立能力圈"><a href="#建立能力圈" class="headerlink" title="建立能力圈"></a>建立能力圈</h3><p>能力圈是可以通过长期努力训练获得的。在建立能力圈之前有一个前提条件，是我们需要对自己的能力圈有所认知，能够识别自己的能力边界。这需要我们对知识有一个诚实的态度。当我们有一个观点时，需要找到最聪明的持反对意见的人，并且能够反驳他的观点，这样才配说有了这个观点。</p><p>关于如何建立能力圈，对每个人来说确实是不一样的，这个取决于每个人一贯的学习方法和学习能力。不过李录先生也给我们提了一些建议：1. 以所有者角度看生意 2.慢慢积累 3.用兴趣和机会去主导研究 4.多实践，选一家公司，以投资的心态把它彻头彻尾研究个透。</p><h2 id="什么样的人适合价值投资"><a href="#什么样的人适合价值投资" class="headerlink" title="什么样的人适合价值投资"></a>什么样的人适合价值投资</h2><p>最后，说一下价值投资对人的本性的一些要求。</p><ul><li>独立，需要一个人更看重自己内心的判断标准</li><li>相对客观，受情绪的影响较小</li><li>耐心&amp;果断。这两点或许是有些矛盾的要求，但是对于一个价值投资者来说，既需要有足够的耐心，等待多年不出手；而一旦到了合适的时机，又能果断的迅速出手</li><li>要有对商业的兴趣</li></ul><h2 id="原文地址-https-lichuanyang-top-posts-44866"><a href="#原文地址-https-lichuanyang-top-posts-44866" class="headerlink" title="原文地址: https://lichuanyang.top/posts/44866/"></a>原文地址: <a href="https://lichuanyang.top/posts/44866/">https://lichuanyang.top/posts/44866/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/44866/</id>
    <link href="https://lichuanyang.top/posts/44866/"/>
    <published>2020-12-12T07:24:34.000Z</published>
    <summary>李录《文明、现代化、价值投资与中国》读书感悟，从文明发展到价值投资的系统性认知升级。</summary>
    <title>读书感悟：文明、现代化、价值投资与中国</title>
    <updated>2026-06-27T03:45:23.483Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="读书笔记" scheme="https://lichuanyang.top/categories/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"/>
    <category term="个人成长" scheme="https://lichuanyang.top/tags/%E4%B8%AA%E4%BA%BA%E6%88%90%E9%95%BF/"/>
    <category term="读书感悟" scheme="https://lichuanyang.top/tags/%E8%AF%BB%E4%B9%A6%E6%84%9F%E6%82%9F/"/>
    <category term="教育" scheme="https://lichuanyang.top/tags/%E6%95%99%E8%82%B2/"/>
    <content>
      <![CDATA[<p>今天讲的这本书，《我曾走在崩溃的边缘：俞敏洪亲述新东方创业发展之路》，是俞敏洪老师写的一本自传性质的书，讲述了他从创立新东方以来的诸多经历和心路历程。今天就总结一下这本书的内容，并且讲一下自己的感悟。</p><span id="more"></span><p>这本书是从俞敏洪创立新东方之前说起的，俞敏洪一开始是北大的老师，在和学校产生矛盾离开了学校。当时恰逢出国留学的热潮，俞敏洪原本打算考给别人培训GRE攒够钱就出国留学，最后却在机缘巧合下，将新东方越做越大。</p><p>简单来讲，新东方的发展过程分为这么几个阶段：</p><ol><li>俞敏洪自己几乎包办一切的小作坊阶段</li><li>获取办学许可证，逐渐向正规化发展；</li><li>品牌开始爆炸式传播</li><li>徐小平、王强等老同学加入，俞敏洪开始有很多强力的合作伙伴</li><li>家族成员离开，借鉴西方管理知识，新东方开始制定明确的制度</li><li>公司成立，以及组织架构的不断改革</li><li>外部投资的进入到上市</li><li>老股东退出，新生代崛起；新的业务领域不断发展，直到今天的新东方</li></ol><p>从这本书，我们看到了新东方从一个小的培训班逐步发展成一个国内领先的教育机构的经历，从中也有很多的感悟。</p><h2 id="关于成长"><a href="#关于成长" class="headerlink" title="关于成长"></a>关于成长</h2><p>看所有成功人士的传记，会发现他们都有一个共同的特质：不给自己设限，不会说我只能干这些事，干不了那些事。俞敏洪也不例外。新东方起源于俞敏洪的英语确实非常好，赶上了出国留学的热潮。但今天回过头来看，会发现俞敏洪的英语能力在新东方的整个发展过程中，只是占比很小的一部分。</p><p>自俞敏洪开培训班开始，他就要备各个学科的课，要考虑如何去营销。随着公司发展，要不断去学习公司的管理，学习财务知识，不断的解决各种各样的新问题。</p><p>俞敏洪在书中也常说，一开始他不懂公司管理，不懂财务，往往都是在有了问题之后才去想如何去解决。这里解决这些问题，就需要自身能力的不断成长，自己学习或者引入外部力量，这都需要不断扩充自己的能力圈。</p><p>在职业发展的过程中，我们的工作边界上肯定会有很多不熟悉的领域。如果对这些领域抱着一个开放的态度，就可以很自然的扩大自己的能力边界。能力边界扩大了，自己就会有更多的机会，而在这些机会中，又能接触到新的领域。这样就形成了一个正向的循环，促成自己事业发展和能力圈的双重提升。</p><p>当然，在成长的过程中，运气因素是不可或缺的，我们看到的这些成功者的经历本身都带有幸存者偏差。新东方的发展过程中也的确有几次可以归结于运气好。但是，我们可以设想一下，假如去除那些运气因素，俞敏洪老师的发展路径，可能攒够钱就出国了，之后大概率找到一份很不错的工具；也可能把培训一直做下去，即便没有政策上的利好，只要他不断去扩充自己的能力圈，我想也足够他自己活的挺好的。</p><p>简单来说，大的成功靠命，但如果我们能一直保持自身的成长，保证一个挺舒服的小康生活，还是可以的。</p><h2 id="关于教育"><a href="#关于教育" class="headerlink" title="关于教育"></a>关于教育</h2><p>新东方毕竟是一家教育公司。我恰好也在一家在线教育公司工作，所以格外关注对于教育业务的讲述。看完这本书之后，关于教育最大的感悟还是，不管你针对什么用户，做哪个学科，用什么形式，所有的一切的根本都只能是教学的质量，只有教学本身能支撑一家机构一直走下去。或许你可以靠大规模的营销来短期获得大量生源，或许可以通过销售技巧忽悠用户续报更贵的课程，有很多种途径可以短期内获得很高收益。但是，谁都不傻，用户们很快就会用脚投票。</p><h2 id="关于规则"><a href="#关于规则" class="headerlink" title="关于规则"></a>关于规则</h2><p>对于一个组织来说，合理的规则是必不可少的。关于规则，最重要的两点，一是要有，而是要合理。</p><p>新东方从家族企业向正规企业转变，就是一个规则从无到有的过程。之后无论董事会的成立，还是分校的发展，确定分校的考核机制，则是规则不断发展的过程。规则制定的好了，公司就能不断发展壮大，达到一个企业和员工双赢的局面。制定不好，公司就会不可避免的走向崩溃。好的规则，关键点就在于能让员工的个人利益转化成公司利益。人的适应力都是非常强的，你说用分校的收入来考核分校，那么分校就能找到很多邪魔外道实现收入增长，但这过程中把公司的声誉消耗没了。好的规则下，员工其实还是在追求自己的利益，但在这过程中自然而然的就促成了公司的发展。</p><p>此外，其实还有很多零零碎碎的感悟，比如新东方发展过程中的各种商业模式，像大班模式，像早期的营销手段；又比如标准化对于一个企业做大的重要性。总之，这本书虽然只是一本自传，还是干货满满的，值得好好的去读几遍。</p><h2 id=""><a href="#" class="headerlink" title=""></a></h2><h2 id="原文地址-https-lichuanyang-top-posts-6471"><a href="#原文地址-https-lichuanyang-top-posts-6471" class="headerlink" title="原文地址: https://lichuanyang.top/posts/6471/"></a>原文地址: <a href="https://lichuanyang.top/posts/6471/">https://lichuanyang.top/posts/6471/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/6471/</id>
    <link href="https://lichuanyang.top/posts/6471/"/>
    <published>2020-10-31T02:35:22.000Z</published>
    <summary>俞敏洪《我曾走在崩溃的边缘》读书笔记，回顾新东方从创立到壮大的创业历程和关键决策。</summary>
    <title>读书笔记-我曾走在崩溃的边缘</title>
    <updated>2026-06-27T03:46:06.797Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="分布式系统模式系列" scheme="https://lichuanyang.top/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E6%A8%A1%E5%BC%8F%E7%B3%BB%E5%88%97/"/>
    <category term="分布式系统" scheme="https://lichuanyang.top/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/"/>
    <category term="分布式系统设计" scheme="https://lichuanyang.top/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/"/>
    <content>
      <![CDATA[<p>（最近把这篇文章重新整理了下，欢迎阅读最新版 <a href="https://lichuanyang.top/posts/45718/%EF%BC%89">https://lichuanyang.top/posts/45718/）</a></p><p>本文翻译自<a href="https://martinfowler.com/articles/patterns-of-distributed-systems/">https://martinfowler.com/articles/patterns-of-distributed-systems/</a> ，原作者对目前各类企业级架构中使用的多种分布式系统进行了总结，从中提取出了一些通用的“模式”(pattern)。本文作为系列文章的第一篇，介绍了分布式系统的特点和一些常见问题。 建议好好阅读一下本文以及英文原文，对于分布式系统设计和分布式架构理念，会有非常大的帮助。</p><span id="more"></span><h2 id="What-this-is-about"><a href="#What-this-is-about" class="headerlink" title="What this is about"></a>What this is about</h2><p>分布式系统给程序设计带来了特殊的挑战。 它们通常要求我们拥有多个数据副本，这些副本需要保持同步。 但是我们无法期望各节点能永远可靠地工作，同时，网络延迟也很容易导致不一致的现象出现。 尽管如此，许多组织仍依赖一系列核心的分布式系统来处理数据存储，消息传递，系统管理和计算等。 这些系统面临很多共同的问题，这些问题也可以通过类似的方案来解决。 本文将这些解决方案定义为模式，通过这些模式，我们对于如何更好地理解，交流和教授分布式系统，将建立起更好的认知。</p><p>在过去的几个月中，我一直在ThoughtWorks进行有关分布式系统的研讨会。研讨会中面临的主要挑战之一是如何将分布式系统的理论映射到诸如Kafka、Cassandra之类的开源项目中，同时还要保持讨论足够通用，能涵盖更广泛的解决方案。模式的概念提供了一个不错的出路。</p><p>“模式”这个结构，本质上使我们能够让我们专注于特定问题，从而很清楚地说明为什么需要特定的解决方案。然后，对解决方案的描述允许我们给出一个代码结构，这个结构足以具体到能够表示出实际解决方案的程度，但同时又足够通用以涵盖各种变化。模式还允许我们将各种模式链接在一起以构建一个完整的系统。这样，我们就有足够的词汇表，来讨论分布式系统的实现。</p><p>接下来是在主流开源分布式系统中观察到的第一组模式。我希望这些模式集对所有开发人员都有用。</p><h2 id="分布式系统-实现角度"><a href="#分布式系统-实现角度" class="headerlink" title="分布式系统-实现角度"></a>分布式系统-实现角度</h2><p>现在的企业架构中有很多天然分布式的平台和框架。如果我们在当前典型的架构抽样查看平台、框架，会看到下面这么一些东西。</p><table><thead><tr><th align="left">Type of platform&#x2F;framework</th><th align="left">Example</th></tr></thead><tbody><tr><td align="left">Databases</td><td align="left">Cassandra, HBase, Riak</td></tr><tr><td align="left">Message Brokers</td><td align="left">Kafka, Pulsar</td></tr><tr><td align="left">Infrastructure</td><td align="left">Kubernetes, Mesos, Zookeeper, etcd, Consul</td></tr><tr><td align="left">In Memory Data&#x2F;Compute Grids</td><td align="left">Hazlecast, Pivotal Gemfire</td></tr><tr><td align="left">Stateful Microservices</td><td align="left">Akka Actors, Axon</td></tr><tr><td align="left">File Systems</td><td align="left">HDFS, Ceph</td></tr></tbody></table><p>这些系统都天然是分布式的。关于什么样的系统叫“分布式”系统，包括两个方面：</p><ul><li>运行在多台服务器上。集群内服务器的数量差别可以非常大，从三台到几千台都可能。</li><li>要处理数据，所以它们是内在“有状态”的系统。</li></ul><p>当多个服务器参与存储数据时，有几种出错的可能。 上述所有系统都需要解决这些问题。 对这些问题，这些系统的实现方式中有一些重复的解决方案。 以更通用的形式理解这些解决方案，有助于理解这些系统的广泛实现方式，并且在需要构建新系统时也可以作为很好的指导。</p><p>模式，是Christopher Alexander提出的，在软件社区中被广泛接受的概念，用于在软件系统设计时记录设计架构。 模式为解决问题提供了一种结构化的方式，可以通过多次查看和验证来解决问题。 使用模式的一种有趣方式是能够以模式序列或模式语言的形式将多个模式链接在一起，这为实现“整个”或完整的系统提供了一些指导。 将分布式系统视为一系列模式，是深入了解其实现方式的有效方法。</p><h2 id="问题及其反复出现的解决方案"><a href="#问题及其反复出现的解决方案" class="headerlink" title="问题及其反复出现的解决方案"></a>问题及其反复出现的解决方案</h2><p>当用多台服务器存储数据时，会有几种出问题的方式。</p><h2 id="进程崩溃"><a href="#进程崩溃" class="headerlink" title="进程崩溃"></a>进程崩溃</h2><p>由于硬件或软件因素，进程在任何时刻都可能崩溃。有若干种进程崩溃的方式。</p><ul><li>系统管理员进行常规的系统维护，进程会被关闭</li><li>进行IO操作，但磁盘空间不足，且异常没有被正确处理时，会导致进程被杀掉</li><li>在云环境下，可能会更复杂，一些无关的原因也可能导致系统宕机。</li></ul><p>最重要的是，如果进程负责存储数据，则必须将它们设计为能够对存储在服务器上的数据提供持久性保证。 即使进程突然崩溃，它也应保留所有成功给过用户ack确认消息的数据。 根据访问模式，不同的存储引擎具有不同的存储结构，范围从简单的hash表到复杂的图结构都有。 将数据刷新到磁盘是最耗时的操作之一，因此无法将每次插入或更新都立即刷新到磁盘。 因此，大多数数据库具有内存存储结构，内存中的数据定期刷新到磁盘。 如果进程突然崩溃，可能会丢失所有这些数据。</p><p>一种叫做Write-Ahead Log的技术就是用来解决这个问题的。服务器将每个状态更改作为命令存储在硬盘上的仅附加(append-only)文件中。 append操作通常是非常快的，因此可以在不影响性能的情况下完成。 顺序写单个日志文件，以存储每个更新。 在服务器启动时，可以重播日志以再次建立内存状态。</p><p>这提供了持久性的保证。 即使服务器突然崩溃，然后重新启动，数据也不会丢失。 但是，在服务器恢复之前，客户端将无法获取或存储任何数据。 因此，如果服务器发生故障，我们会缺乏可用性。</p><p>一种显而易见的解决方案是将数据存储在多个服务器上。 因此，我们可以在多个服务器上复制预写日志。</p><p>当涉及多个服务器时，还有更多的故障情况需要考虑。</p><h2 id="网络延迟"><a href="#网络延迟" class="headerlink" title="网络延迟"></a>网络延迟</h2><p>在TCP &#x2F; IP协议栈中，通过网络传输消息时所引起的延迟没有上限。 它会根据网络上的负载而变化。 例如，一个1 Gbps的网络连接可能会被定时触发的大数据作业淹没，从而填满网络缓冲区，并且可能导致某些消息在不确定的延迟后才到达服务器。</p><p>在典型的数据中心中，服务器打包在机架中，并且通过机架式交换机连接多个机架。 可能会有一棵交换机树将数据中心的一部分连接到另一部分。 在某些情况下，一组服务器可以相互通信，但与另一组服务器的连接是断开的。 这种情况称为网络分区。 服务器通过网络进行通信的基本问题之一是，何时能知道特定服务器发生了故障。</p><p>这里有两个问题要解决。</p><ul><li>一台特定的服务器不能通过无限期的等待，去知道另一台服务器已经宕机。</li><li>不应有两套服务器，每套服务器都认为另一套服务器发生了故障，因此继续为不同的客户端提供服务。 这称为脑裂(split brain)。</li></ul><p>为了解决第一个问题，每台服务器都会定期向其他服务器发送心跳消息。如果没有收到心跳，则将对应的服务器视为已崩溃。心跳间隔足够短，能够确保不需要花费很长时间就能检测服务器故障。在最坏的情况下，服务器可能在正常运行，但是被集群整体认为已经宕机，并继续运行。心跳这种方式可以确保提供给客户端的服务不会中断。</p><p>第二个问题是脑裂。在脑裂的情况下，如果两组服务器独立接受更新，则不同的客户端可以获取和设置不同的数据，当脑裂的问题被解决后，是不可能自动解决冲突的。</p><p>为了解决脑裂的问题，我们必须确保彼此断开连接的两组服务器不能独立地正常运行下去。为确保这一点，服务器执行动作时，只有大多数服务器可以确认这个动作，该才将动作视为成功。如果服务器无法选出来一个”大多数“，则它们将无法提供服务，并且某些客户端组可能无法接收该服务。但是集群中的服务器将始终处于一致状态。占多数的服务器数量称为Quorum。如何确定Quorum？这是根据集群可以容忍的故障数决定的。比如，我们有五个节点的群集，则需要三个quorum。通常，如果我们要容忍f个故障，则需要2f + 1的集群大小。</p><p>Quorum确保我们有足够的数据副本以承受某些服务器故障。但是，仅向客户提供强大的一致性保证是不够的。假设客户端在quorum上启动了写操作，但是该写操作仅在一台服务器上成功。Quorum中的其他服务器仍具有旧值。当客户端从Quorum读取值时，如果具有最新值的服务器可用，则它可能会获得最新值。但是，如果仅当客户端开始读取值时，具有最新值的服务器不可用，它就可以获得旧值。为了避免这种情况，需要有人跟踪判断quorum是否同意特定的操作，并且仅将值发送给保证在所有服务器上都可用的客户端。在这种情况下使用主从模式(master&amp;slave)。其中一台服务器当选为master，其他服务器充当slave。Master控制并协调对slave的复制。Master需要确定哪些更改应对客户可见。高水位标记(High-water mark)用于跟踪预写日志中已成功复制到足够的slave中的条目。客户端可以看到所有高水位之前的条目。Master还将高水位标记发送给slave。因此，如果Master失败并且slave之一成为新master，那么客户看到的内容就不会出现不一致之处。</p><h2 id="进程暂停"><a href="#进程暂停" class="headerlink" title="进程暂停"></a>进程暂停</h2><p>但这还不是全部，即使有了Quorums和Master and Slave，仍然需要解决一个棘手的问题。 Master进程随时可能暂停。 进程暂停的原因很多。 对于支持垃圾回收的语言来说，可能会有很长的垃圾回收暂停。 具有较长垃圾收集暂停时间的Master可能会与slave断开连接，并在暂停结束后继续向slave发送消息。 同时，如果slave没有收到master的任何心跳信号，他们可能选择了新的master并接受了客户端的更新。 如果旧master的请求按原样处理，则它们可能会覆盖某些更新。 因此，我们需要一种机制来检测过时master的请求。 Generation Clock用于标记和检测来自老master的请求。 世代(generation)是单调增加的数字。</p><h2 id="时钟不同步及顺序消息"><a href="#时钟不同步及顺序消息" class="headerlink" title="时钟不同步及顺序消息"></a>时钟不同步及顺序消息</h2><p>从较新的消息中检测较旧的master消息的问题是如何保证消息顺序。看起来我们可以使用系统时间来给一组消息排序，但实际上，是不行的。主要原因是我们无法保证跨服务器的系统时钟是同步的。计算机中的一天中的时钟由石英晶体管理，并根据晶体的振荡来测量时间。</p><p>这种机制容易出错，因为晶体可以更快或更慢地振荡，因此不同的服务器可能具有截然不同的时间。一组服务器上的时钟通过称为NTP的服务进行同步。该服务会定期检查一组全局时间服务器，并相应地调整计算机时钟。</p><p>由于这是通过网络上的通信发生的，并且网络延迟可能会如以上各节中所述发生变化，所以由于网络问题，时钟同步可能会延迟。这可能导致服务器时钟彼此之间漂移，并且在NTP同步发生后甚至会倒退。由于计算机时钟存在这些问题，因此通常不将一天中的时间用于顺序事件。取而代之的是使用一种称为Lamport’s timestamp的简单技术。Generation Clock就是一个例子。</p><h2 id="整合-一个分布式系统的例子"><a href="#整合-一个分布式系统的例子" class="headerlink" title="整合-一个分布式系统的例子"></a>整合-一个分布式系统的例子</h2><p>我们可以看到，从头开始理解这些模式是如何帮助我们建立一个完整系统的。 我们将以一致性为例。 分布式一致性是分布式系统实现的一种特例，它提供了最强的一致性保证。 比如流行的企业架构中常见的Zookeeper，etcd和Consul。 他们实现了zab和Raft等一致性算法，以提供复制和强大的一致性。 还有其他流行的算法可以实现一致性，Google的Chubby中用于锁定服务的Paxos，考虑stamp replication and <a href="https://www.cs.cornell.edu/ken/History.pdf">virtual-synchrony</a>。 用非常简单的术语来说，一致性是指在一组服务器中，在存储的数据，存储的顺序以及何时使该数据对客户端可见，这些方面达成一致。</p><h2 id="实现一致性的模式列表"><a href="#实现一致性的模式列表" class="headerlink" title="实现一致性的模式列表"></a>实现一致性的模式列表</h2><p>一致性的实现中，使用 state machine replication来实现容错。在state machine replication中，存储服务，比如键值存储，在所有服务器上复制，而且用户的输入也会在每个服务器上以相同顺序执行。用于实现此目的的关键实现技术是在所有服务器上复制Write-Ahead log以具有“ Replicated Wal”。</p><p>我们可以将这些模式放在一起以实现Replicated Wal，如下所示。</p><p>为了提供持久性保证，使用Write-Ahead log。使用Segmented Log将预写日志分为多个段。这有助于Low-Water Mark进行日志清理。通过在多个服务器上复制预写日志来提供容错能力。服务器之间的复制通过使用主从模式进行管理。更新High-Water Mark时，使用Quorum，以确定哪些值对客户可见。通过使用Single Update Quest，使所有请求均按严格顺序处理。使用Single Socket Channel, 将master的请求发送给slave时，事件将得到维护。为了优化Single Socket Channel上的吞吐量和延迟，使用了Request Pipeline。Slavet通过HeartBeat确定Master的可用性。如果Master由于网络分区而暂时从集群断开连接，则可以使用“Generation Clock”来检测它。</p><p><img src="/img/paterns.jpg" alt="paterns"></p><p>通过这种方式，理解这些问题，和它们的通用解决方式，帮助我们理解构建复杂系统的方式。</p><h2 id="下一步"><a href="#下一步" class="headerlink" title="下一步"></a>下一步</h2><p>分布式系统是一个巨大的话题。 这里涵盖的模式集只是一小部分，涵盖了不同的类别，以展示模式方法如何帮助理解和设计分布式系统。 我将继续向这个合集添加下边这些问题。</p><ul><li>Group Membership and Failure Detection</li><li>Partitioning</li><li>Replication and Consistency</li><li>Storage</li><li>Processing</li></ul><p>原文地址: <a href="https://lichuanyang.top/posts/3914/">https://lichuanyang.top/posts/3914/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/3914/</id>
    <link href="https://lichuanyang.top/posts/3914/"/>
    <published>2020-08-10T12:33:49.000Z</published>
    <summary>Martin Fowler 文章翻译，总结企业级架构中分布式系统的通用模式(pattern)，涵盖共识、复制、分区等核心主题。</summary>
    <title>[翻译][关于分布式架构和系统设计]分布式系统的模式-综述</title>
    <updated>2026-06-27T03:42:25.685Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="redis系列" scheme="https://lichuanyang.top/categories/redis%E7%B3%BB%E5%88%97/"/>
    <category term="redis" scheme="https://lichuanyang.top/tags/redis/"/>
    <content>
      <![CDATA[<p><a href="https://lichuanyang.top/posts/22179/">在上一篇文章中</a>，介绍了redis的底层定义的一些数据结构。接下来，在本文中，我们就结合redis提供的对象，看看这些数据结构是如何使用的。</p><span id="more"></span><p>Redis中主要包括这些对象：字符串、列表、哈希、集合、有序集合、HyperLogLog、GEO等，本文会主要介绍这些对象是如何实现的，对于其命令，可以参考<a href="https://redis.io/commands">redis官网</a>中对每个命令的说明，这里就不一个一个列了。</p><p>在介绍具体的对象之前，先看一下redis中对于对象本身的定义。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">redisObject</span> &#123;</span></span><br><span class="line">    <span class="type">unsigned</span> type:<span class="number">4</span>;</span><br><span class="line">    <span class="type">unsigned</span> encoding:<span class="number">4</span>;</span><br><span class="line">    <span class="type">unsigned</span> lru:LRU_BITS; <span class="comment">/* LRU time (relative to global lru_clock) or</span></span><br><span class="line"><span class="comment">                            * LFU data (least significant 8 bits frequency</span></span><br><span class="line"><span class="comment">                            * and most significant 16 bits access time). */</span></span><br><span class="line">    <span class="type">int</span> refcount;</span><br><span class="line">    <span class="type">void</span> *ptr;</span><br><span class="line">&#125; robj;</span><br></pre></td></tr></table></figure><p>在一个redisObject中，prt字段就是数据的实际内容，这里的存储会应用之前讲的redis的底层数据结构。可以看到，ptr是作为引用出现在redisObject中，也就是说，redisObject本身和实际数据本身内存可能不在一起。</p><p>通过 OBJECT ENCODING KEY 这个命令，可以查看一个key底层实际使用的数据结构。</p><p>接下来，我们就具体讲一下各个对象。</p><h2 id="字符串"><a href="#字符串" class="headerlink" title="字符串"></a>字符串</h2><p>字符串可以说是redis中最基本的一个对象类型了，set 一个普通的key,value,使用的就是字符串对象。字符串对象根据具体情况，会使用int, embstr, raw三种类型的底层编码方式。其中int很好理解，在字符串的值是数字时，就会使用int存储。embstr和raw的底层结构都是简单动态字符串（SDS）,区别在于，embstr中，redisObject和实际存储的SDS在连续的内存空间上。embstr的使用场景是字符串小于等于39字节（如无特别说明，本文中的数值都是使用redis的默认配置），大于39字节时，则使用raw编码。</p><h2 id="列表对象"><a href="#列表对象" class="headerlink" title="列表对象"></a>列表对象</h2><p>即list, redis中的list是一个双向队列，提供左右两侧的push,pop操作，range操作等。</p><p>list的底层实现可以是ziplist或者链表。上一篇文章中，我们介绍了ziplist是一种针对小数据集的，非常高效的数据结构。因此，在使用时，很自然的可以想到，小的list就会用ziplist实现，大的list则用链表实现。对于什么样的列表才算小，redis里使用两个判断标准，一是list中每个元素的最大长度，对应配置list-max-ziplist-value, 二是list中的元素数，对应配置list-max-ziplist-entries。</p><p>list的实际应用场景其实就是对双向队列的利用场景，比如微信朋友圈的时间线就可以通过list实现。有时候，也会拿redis的list用作消息队列。</p><h2 id="哈希"><a href="#哈希" class="headerlink" title="哈希"></a>哈希</h2><p>即我们通常所说的map,与列表对象list, 哈希也会根据数据集的大小选用两个不同的底层结构，分别是ziplist和hashTable(底层是字典)。</p><p>这里可以引申出一个小技巧，当我们需要在redis中存储object时，可以使用hash，而非序列化成字符串存储。因为对于一个正常的object来说，是会被通过ziplist存储的。这样，就通过非常小的资源开销实现了对单个字段的get&#x2F;set.</p><h2 id="集合对象"><a href="#集合对象" class="headerlink" title="集合对象"></a>集合对象</h2><p>即set, 对应redis中的S系列命令，如SADD,SMEMBERS等，也就是元素不重复的”集合”。同样的，redis的集合也有两种实现方式，分别是intset和hashTable.</p><p>redis的集合提供了非常方便的求交集、并集、差集等的方法，可以用在查询共同好友等场景中。</p><h2 id="有序集合"><a href="#有序集合" class="headerlink" title="有序集合"></a>有序集合</h2><p>对应redis中的Z系列命令，如ZADD,ZRANK等，底层的实现对应ziplist或者skiplist。期数，看完上一篇文章中对于这两个数据结构的介绍，我们是可以很容易的理解redis是如何实现有序集合的。</p><p>有序集合也是一种很常用的对象，可以用在排行榜、topK等问题中。</p><h2 id="HyperLogLog"><a href="#HyperLogLog" class="headerlink" title="HyperLogLog"></a>HyperLogLog</h2><p>redis中的hyperLogLog是一个用于基数统计的结构，即统计不重复元素的个数。它的实现是基于概率算法，通过一定概率的误差，在对空间使用非常小的情况下，实现对大数据量的统计。其具体算法非常精妙，我后边会单开一篇文章介绍。这里就先介绍下它的效果，HyperLogLog可以使用12k的空间统计2的64次方的数据，标准误差在0.81%。</p><p>Redis中使用HyperLogLog非常简单，只包含三条命令，PFADD添加元素，PFCOUNT获取当前基数，PFMERGE对多个HyperLogLog进行合并。</p><h2 id="GEO"><a href="#GEO" class="headerlink" title="GEO"></a>GEO</h2><p>顾名思义，redis中的geo就是一种用来处理位置相关信息的结构。redis中的geo对象提供了添加、查看位置信息，计算两个位置间的距离，查找范围内的元素这些功能。</p><p>Geo的底层基于zset实现。其核心处理在于通过GeoHash算法将二维的经纬度编码成一维的hash值，同时还能保住经纬度本身越接近的值，hash值也越接近。GeoHash的细节我们也在后续文章中专门做介绍。经过GeoHash做编码后，就可以利用zset本身的功能来做范围查找等事情了。</p><p>现在很多互联网服务的位置服务会选择使用redis来实现，比如附近的人，附近的餐厅。</p><p>通过上边的介绍，希望可以帮助大家更好的理解和使用redis。可以看到，redis里针对一些特定情况，进行了非常极致的内存优化，而我们在使用时，就要考虑如何更好的去利用这些优化。</p><p>原文地址： <a href="https://lichuanyang.top/posts/25564/">https://lichuanyang.top/posts/25564/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/25564/</id>
    <link href="https://lichuanyang.top/posts/25564/"/>
    <published>2020-08-08T07:05:48.000Z</published>
    <summary>Redis 对象系统源码解析，介绍字符串、列表、哈希、集合、有序集合等对象的底层实现方式。</summary>
    <title>redis的对象</title>
    <updated>2026-06-27T03:44:21.594Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="redis系列" scheme="https://lichuanyang.top/categories/redis%E7%B3%BB%E5%88%97/"/>
    <category term="redis" scheme="https://lichuanyang.top/tags/redis/"/>
    <category term="数据结构，skiplist" scheme="https://lichuanyang.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%EF%BC%8Cskiplist/"/>
    <content>
      <![CDATA[<p>Redis作为当前使用非常广泛的内存数据库，在代码层面做了很多极致的优化，已获取更好的性能。其中重要的一部分，就是对于底层数据结构的使用。Redis会根据数据量、数据大小等来优化对于不同结构的使用，从而获得更佳的运行效率和内存占用。Redis的核心数据结构包括简单动态字符串、列表、字典、跳跃表、整数集合、压缩列表。</p><p>接下来，我们就依次讲讲这些数据结构。</p><span id="more"></span><h2 id="简单动态字符串（SDS）"><a href="#简单动态字符串（SDS）" class="headerlink" title="简单动态字符串（SDS）"></a>简单动态字符串（SDS）</h2><p>Redis是用C语言实现的。先复习一下C，C里的字符串中不记录字符串长度，以空字符标记结尾。这样会显而易见的带来三个问题：1.获取字符串长度需要O(n)的复杂度；2.操作不慎会导致缓冲区溢出，例如内存中紧邻的两个字符串，如果对前一个调用strcat拼接其他字符串，就会造成溢出；3. 一些特殊内容，如图像、音频等转成二进制时，难免其中夹杂空字符等特殊字符，这样就无法被C字符串存储了，即C字符串不具备二进制安全性。</p><p>而这几点，对于Redis的应用场景来说，影响其实都是非常大的。因此，在redis中定义了一个新的结构，用来保存字符串，即SDS。</p><p>SDS的核心思想就是额外使用一个字段记录字符串的长度，这样，上面三个问题就都迎刃而解了。</p><p>此外，redis从4.0开始对SDS做了一个代码层面的优化，优化了内存占用，不过不影响其底层逻辑。</p><p>这是redis 3.0里SDS的源码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">struct sdshdr &#123;</span><br><span class="line">    unsigned <span class="type">int</span> len;</span><br><span class="line">    unsigned <span class="type">int</span> free;</span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>而这是redis 4.0之后SDS的源码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">  struct <span class="title function_">__attribute__</span> <span class="params">((__packed__)</span>) sdshdr5 &#123;</span><br><span class="line">    unsigned <span class="type">char</span> flags; <span class="comment">/* 3 lsb of type, and 5 msb of string length */</span></span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br><span class="line">struct <span class="title function_">__attribute__</span> <span class="params">((__packed__)</span>) sdshdr8 &#123;</span><br><span class="line">    uint8_t len; <span class="comment">/* used */</span></span><br><span class="line">    uint8_t alloc; <span class="comment">/* excluding the header and null terminator */</span></span><br><span class="line">    unsigned <span class="type">char</span> flags; <span class="comment">/* 3 lsb of type, 5 unused bits */</span></span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br><span class="line">struct <span class="title function_">__attribute__</span> <span class="params">((__packed__)</span>) sdshdr16 &#123;</span><br><span class="line">    uint16_t len; <span class="comment">/* used */</span></span><br><span class="line">    uint16_t alloc; <span class="comment">/* excluding the header and null terminator */</span></span><br><span class="line">    unsigned <span class="type">char</span> flags; <span class="comment">/* 3 lsb of type, 5 unused bits */</span></span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br><span class="line">struct <span class="title function_">__attribute__</span> <span class="params">((__packed__)</span>) sdshdr32 &#123;</span><br><span class="line">    uint32_t len; <span class="comment">/* used */</span></span><br><span class="line">    uint32_t alloc; <span class="comment">/* excluding the header and null terminator */</span></span><br><span class="line">    unsigned <span class="type">char</span> flags; <span class="comment">/* 3 lsb of type, 5 unused bits */</span></span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br><span class="line">struct <span class="title function_">__attribute__</span> <span class="params">((__packed__)</span>) sdshdr64 &#123;</span><br><span class="line">    uint64_t len; <span class="comment">/* used */</span></span><br><span class="line">    uint64_t alloc; <span class="comment">/* excluding the header and null terminator */</span></span><br><span class="line">    unsigned <span class="type">char</span> flags; <span class="comment">/* 3 lsb of type, 5 unused bits */</span></span><br><span class="line">    <span class="type">char</span> buf[];</span><br><span class="line">&#125;;</span><br><span class="line">...</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>可以看到，在新版的源码里，数据存储会根据情况使用uint8,uint16等不同类型。在C里，一个int占用4个字节，因此，对于原版的SDS来说，即使存储的信息非常少，也会固定占到8个字节。而uint8只占一个字节，uint16只占2个字节，对于小数据来说，redis的内存占用会有明显优化。</p><p>此外，redis会有空间预分配、惰性释放等机制，减少内存分配的次数。SDS的实现方式也保证了大部分方法可以兼容C字符串，减少了大量实现成本。</p><h2 id="链表"><a href="#链表" class="headerlink" title="链表"></a>链表</h2><p>Redis里的链表是一个普通的双向无环链表，相信大家都很熟悉了，就不细说了，结构如下。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">typedef struct listNode &#123;</span><br><span class="line"></span><br><span class="line">    struct listNode *prev;</span><br><span class="line"></span><br><span class="line">    struct listNode *next;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">void</span> *value;</span><br><span class="line"></span><br><span class="line">&#125; listNode;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Redis中的列表对象，底层就是链表。</p><h2 id="字典"><a href="#字典" class="headerlink" title="字典"></a>字典</h2><p>字典也就是我们常说的map。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">typedef struct dictht &#123;</span><br><span class="line"></span><br><span class="line">    dictEntry **table;</span><br><span class="line"></span><br><span class="line">    unsigned <span class="type">long</span> size; <span class="comment">//hash表长度</span></span><br><span class="line"></span><br><span class="line">    unsigned <span class="type">long</span> sizemask;</span><br><span class="line"></span><br><span class="line">    unsigned <span class="type">long</span> used; <span class="comment">//已有的长度</span></span><br><span class="line"></span><br><span class="line">&#125; dictht;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Redis中的字典是hash表，使用链地址法解决hash地址冲突。</p><p>类似于java等语言中的hashMap, redis的字典也会有rehash的机制，保证其负载因子维持在合理的范围内。</p><h2 id="跳跃表-skiplist"><a href="#跳跃表-skiplist" class="headerlink" title="跳跃表 (skiplist)"></a>跳跃表 (skiplist)</h2><p>Skiplist是一种应用非常广的数据结构，通常是作为AVL树的一种替代选择，和AVL树一样，skiplist的查找复杂度也是O(logn), 但是实现会简单的多，下边我们用短短的几行字就能把SkipList的所有内容讲的非常清楚。此外，在并发环境下，SkipList也会有很大优势，因为AVL数在平衡过程中，可能会涉及到很多节点，也就需要锁住很多节点，SkipList则完全不存在这种问题。</p><p><img src="/img/redis/skiplist.jpg" alt="skiplist"></p><p>从网上找了一张示意图，可以很清楚的展示出SkipList的结构。跳跃表说白了就是一个多层的列表，每一个元素会随机的出现在某一层上，然后某一层的链表中会包含所有高于或等于本层的元素。</p><p>跳跃表的查找就是从高层查起，逐步降层，定位到具体元素。比如要查询7， 其顺序就是9-&gt;6-&gt;7.</p><p>跳跃表的插入也是先做一次查找，然后直接给元素设置一个随机的层数，再调整指针。</p><p>删除则是删除节点，然后调整指针。</p><p>Redis中的有序集合，就是基于跳跃表实现的。</p><h2 id="整数集合-intset-和压缩列表-ziplist"><a href="#整数集合-intset-和压缩列表-ziplist" class="headerlink" title="整数集合(intset)和压缩列表(ziplist)"></a>整数集合(intset)和压缩列表(ziplist)</h2><p>这两个结构非常像，因此就放在一起讲了。它们都是针对特定条件下的小数据集做的特定优化。</p><p>整数集合是一个有序集合，使用的条件是集合中只包含整数，且元素个数不多。</p><p>压缩列表同样是针对列表项非常少的情况，且要求元素只能是小整数值或短字符串。它可以提供类似双向链表的功能。</p><p>因为整数集合和压缩列表都是针对小数据集的，所以可以使用连续的内存空间去保存，实现也就简单了很多，这里就不细说了。</p><p>在实际应用中，zipList可以作为链表或者字典的替代品，应用在redis的列表、哈希、有序集合中。整数集合则作为字典的替代品，用在集合对象中。</p><p>以上就是redis中主要的数据结构，在这些结构的基础上，redis实现了大量功能完善的对象，供我们使用。理解了redis这些底层结构的原理，也可以帮助我们更好的发挥redis的价值。</p><h2 id="原文地址：https-lichuanyang-top-posts-22179"><a href="#原文地址：https-lichuanyang-top-posts-22179" class="headerlink" title="原文地址：https://lichuanyang.top/posts/22179/"></a>原文地址：<a href="https://lichuanyang.top/posts/22179/">https://lichuanyang.top/posts/22179/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/22179/</id>
    <link href="https://lichuanyang.top/posts/22179/"/>
    <published>2020-07-11T08:47:30.000Z</published>
    <summary>Redis 底层数据结构详解，包括 SDS、ziplist、quicklist、dict、skiplist 等核心数据结构的设计与优化。</summary>
    <title>redis里的数据结构</title>
    <updated>2026-06-27T03:43:25.701Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="架构设计" scheme="https://lichuanyang.top/categories/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="设计模式" scheme="https://lichuanyang.top/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
    <content>
      <![CDATA[<p>众所周知，对于程序员来说，设计模式是一门极其重要的学科。不过，由于设计模式的涉及面太广，也有很多非常抽象的概念，还是很难掌握的。要学习设计模式，最好还是能结合实际。每次做需求，尤其是一些复杂的需求，或者嗅到了烂代码味道的时候，就可以翻一遍设计模式，看看有什么可以应用的模式。所以，我总结了这篇文章，以尽量短的语言描述主要的设计模式，可能是定义中的关键部分，也可能是典型的应用场景，或者只是个英文单词，目的在于帮助回忆起每个设计模式的作用和应用场景。</p><span id="more"></span><h2 id="基本设计原则"><a href="#基本设计原则" class="headerlink" title="基本设计原则"></a>基本设计原则</h2><p>要说设计模式，一定要先提一遍六大基本设计原则。说白了，设计模式就是在这些原则的基础上被提炼出来得一系列最佳实践。</p><h2 id="单一职责原则"><a href="#单一职责原则" class="headerlink" title="单一职责原则"></a>单一职责原则</h2><h2 id="开放封闭原则"><a href="#开放封闭原则" class="headerlink" title="开放封闭原则"></a>开放封闭原则</h2><p>通过扩展解决问题，而不是修改已有的实现。（写代码时可以假定变化不会发生，当出现变化时，需要抽象以隔离将来的同类变化）  修改没有破坏原有的单元测试，即可认为不违反开闭原则</p><h2 id="里氏替换原则"><a href="#里氏替换原则" class="headerlink" title="里氏替换原则"></a>里氏替换原则</h2><p>子类应当能代替父类</p><h2 id="依赖倒置原则"><a href="#依赖倒置原则" class="headerlink" title="依赖倒置原则"></a>依赖倒置原则</h2><p>高层模块不能依赖低层模块；应当依赖抽象而非实现</p><h2 id="接口隔离原则"><a href="#接口隔离原则" class="headerlink" title="接口隔离原则"></a>接口隔离原则</h2><p>接口尽量小</p><h2 id="迪米特法则"><a href="#迪米特法则" class="headerlink" title="迪米特法则"></a>迪米特法则</h2><p>类间解耦</p><p>#设计模式</p><h2 id="工厂方法模式"><a href="#工厂方法模式" class="headerlink" title="工厂方法模式"></a>工厂方法模式</h2><p>定义一个用于创建对象的接口</p><h2 id="抽象工厂模式"><a href="#抽象工厂模式" class="headerlink" title="抽象工厂模式"></a>抽象工厂模式</h2><p>创建一组相关或者相互依赖的对象</p><h2 id="单例模式"><a href="#单例模式" class="headerlink" title="单例模式"></a>单例模式</h2><p>一个类只有一个实例</p><h2 id="建造者模式"><a href="#建造者模式" class="headerlink" title="建造者模式"></a>建造者模式</h2><p>分别build实例的各个模块</p><h2 id="原型模式"><a href="#原型模式" class="headerlink" title="原型模式"></a>原型模式</h2><p>复制对象</p><h2 id="适配器模式"><a href="#适配器模式" class="headerlink" title="适配器模式"></a>适配器模式</h2><p>处理接口不兼容</p><h2 id="装饰器模式"><a href="#装饰器模式" class="headerlink" title="装饰器模式"></a>装饰器模式</h2><p>动态给一个对象添加一些额外的职责，比生成子类的方式灵活</p><h2 id="代理模式"><a href="#代理模式" class="headerlink" title="代理模式"></a>代理模式</h2><p>对一些对象提供代理，以限制那些对象去访问其它对象</p><h2 id="门面模式-外观模式-facade"><a href="#门面模式-外观模式-facade" class="headerlink" title="门面模式(外观模式) (facade)"></a>门面模式(外观模式) (facade)</h2><p>门面中包含若干个子系统的部分或全部方法，将子系统组合后对外提供。</p><h2 id="桥接模式"><a href="#桥接模式" class="headerlink" title="桥接模式"></a>桥接模式</h2><p>将抽象部分和实现部分抽离。一个抽象类依赖一个接口，两部分分别可以做扩展</p><h2 id="组合模式"><a href="#组合模式" class="headerlink" title="组合模式"></a>组合模式</h2><p>把一组相似的对象当作一个单一的对象</p><h2 id="享元模式"><a href="#享元模式" class="headerlink" title="享元模式"></a>享元模式</h2><p>共享对象，数据库连接池，线程池等即是用享元模式的应用</p><h2 id="策略模式"><a href="#策略模式" class="headerlink" title="策略模式"></a>策略模式</h2><p>多种不同“策略”动态切换，起到改变对象行为的效果</p><h2 id="模板方法模式"><a href="#模板方法模式" class="headerlink" title="模板方法模式"></a>模板方法模式</h2><p>父类定义算法的骨架，部分细节由子类实现</p><h2 id="观察者模式"><a href="#观察者模式" class="headerlink" title="观察者模式"></a>观察者模式</h2><p>对于一对多的依赖关系，当一个对象的状态发生改变时，所有依赖于它的对象都得到通知</p><h2 id="迭代子模式"><a href="#迭代子模式" class="headerlink" title="迭代子模式"></a>迭代子模式</h2><p>给对象提供一个迭代器（Iterator）</p><h2 id="责任链模式"><a href="#责任链模式" class="headerlink" title="责任链模式"></a>责任链模式</h2><p>多个对象都有机会处理请求，连成一条链，并沿着这条链传递该请求，直到有一个对象处理他为止。</p><h2 id="命令模式"><a href="#命令模式" class="headerlink" title="命令模式"></a>命令模式</h2><p>将来自客户端的请求传入一个对象，实现于“行为请求者”与“行为实现者”的解耦</p><h2 id="备忘录模式"><a href="#备忘录模式" class="headerlink" title="备忘录模式"></a>备忘录模式</h2><p>备忘录对象就是一个用来存储另外一个对象内部状态的快照的对象</p><h2 id="状态模式"><a href="#状态模式" class="headerlink" title="状态模式"></a>状态模式</h2><p>状态机</p><h2 id="访问者模式"><a href="#访问者模式" class="headerlink" title="访问者模式"></a>访问者模式</h2><p>将数据结构与数据操作分离</p><h2 id="中介者模式"><a href="#中介者模式" class="headerlink" title="中介者模式"></a>中介者模式</h2><p>用一个中介者对象封装一系列的对象交互，中介者使各对象不需要显示地相互作用</p><h2 id="解释器模式"><a href="#解释器模式" class="headerlink" title="解释器模式"></a>解释器模式</h2><p>给定一种语言，定义他的文法的一种表示，并定义一个解释器，该解释器使用该表示来解释语言中句子。典型场景SQL解析等</p><h2 id="原文地址-https-lichuanyang-top-posts-21897"><a href="#原文地址-https-lichuanyang-top-posts-21897" class="headerlink" title="原文地址: https://lichuanyang.top/posts//21897/"></a>原文地址: <a href="https://lichuanyang.top/posts//21897/">https://lichuanyang.top/posts//21897/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/21897/</id>
    <link href="https://lichuanyang.top/posts/21897/"/>
    <published>2020-04-08T10:06:51.000Z</published>
    <summary>设计模式速查手册，用精炼语言描述 23 种设计模式的核心要点、典型场景和关键英文术语。</summary>
    <title>设计模式备忘录</title>
    <updated>2026-06-08T07:50:49.047Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="感悟" scheme="https://lichuanyang.top/tags/%E6%84%9F%E6%82%9F/"/>
    <content>
      <![CDATA[<p>工作这些年来，一直在思考着程序员这个职业究竟是在做什么，随着经验增多，其实也一直在刷新着认知。现在写这篇文章，一方面是为了分享，另一方面也是想留下一份记录，过两年再回头来看看自己的认知又有了什么更新。</p><span id="more"></span><h2 id="程序员职业的本质"><a href="#程序员职业的本质" class="headerlink" title="程序员职业的本质"></a>程序员职业的本质</h2><p>我想从两个方面来说。一是从做的事儿上，在我看来，程序员的工作就是发现问题、分析问题、解决问题三类。事实上，世界上绝大多数工作都可以囊括进这三类事中。二是从能力上来看，我把能力分为两种：对现实世界的认知能力，和将现实世界映射成程序语言的能力。</p><p>对于三种类型的事，按发现问题，分析问题，解决问题的顺序，是从宏观到细节的区别，平时做的事，肯定是以解决问题为主，各种琐碎的事，也都是在解决问题。发现问题和分析问题对人的思维深度会有更高的要求，不过对于工作来说，我倒并不觉得这三类事有什么高下之分。如果对确定的问题，总能给出恰当的解决方案，这也是职场上非常硬核的核心竞争力。而对于两类能力，一般还是更关注第二种。但是我觉得对现实世界的认知能力，对于程序员的职业发展是非常重要的。就像抠业务，抠CRUD这事，有门槛吗?我觉得是有的，至少把业务做的很好门槛非常高。有些人总是以非常奇特的方式去实现业务，然后赖产品经理需求描述不清楚，很可能就是缺少对现实的认知。</p><p>接下来，就按发现问题、分析问题、解决问题的顺序，分别详细说一下。</p><p>发现问题的能力，对程序员来说，其实不是必需的。很多问题，即便你发现不了，或者发现了然后假装它不存在，系统一样可以运行的很好。但是，逐步的累积这种问题，最终必然会导致代码不可维护。下面，我拿一个自己的实际经历来举个例子。之前做过一个旅客飞行记录的项目，国内航班的数据，我们几乎都能拿到。数据存储的时候，主键是用的证件号，为什么呢，因为所有的订票系统，都是用证件号去做key的。但是我们的产品形态并不是这样，而是以用户的维度去显示，一个用户会有多个证件。这样就导致了一个问题：任何一个非常简单的问题，比如要查用户最近一条飞行记录，最近十条飞行记录，都会有一套复杂的处理流程。更不用说随着数据量增加，要分库分表，但同一个用户的证件没法往一张表上落，逻辑复杂度再提升一级。在这个时候，不发现这个问题，其实业务也完全开展的下去，但是很显然，做这块业务就会做的很难受。而意识到这是个问题，再做相应的调整后，才会发现原来这一块的业务如此简单。</p><p>那么，怎么才能发现问题呢。这需要我们有对现实世界的认知能力，也需要对设计模式等基础知识有足够的理解。要发现问题，我们首先得有“正常状态”是什么样子的概念，与正常状态不一样的，就是问题。比如前边的例子，一个“正常状态”是查询用户最近的飞行记录，这个需求应该非常简单，还有一个“正常状态”是我们的用户是一个人，而不是一个证件，发现了和政策状态不一致，也就发现了问题。再举一些例子，写单元测试应该是很简单的，当发现单元测试不好写的时候，就需要考虑一下是不是代码的设计出了问题；mysql的一次batch请求，耗时的数量级应该在毫秒级, 而且数据量不应该严重影响耗时，当发现数据量和耗时的关系成了一个非常陡的线性关系，就要考虑一下是不是真的做了次batch(参考 <a href="https://lichuanyang.top/posts/63688)%E3%80%82">https://lichuanyang.top/posts/63688)。</a></p><p>接下来是分析问题。对于分析问题，我觉得说白了就是把一个表现出来的问题，拆解成一个或若干个“指标”出了问题。这些“指标”，可以是内存、cpu这样实际的指标，也可以是一些基本的概念理解、设计思路。还是上一个例子，我们发现的问题就是一些很简单的需求实现起来很难，最后拆解出来的出问题的“指标”就是存储记录主键的选择。</p><p>对于分析问题，要会做合理的可能性推测，然后多利用工具验证或者推翻猜测。之前有一次发现数据存不进数据库，这时候就可以做出若干个合理的推测：数据库本身有问题；和数据库的连接有问题；事务未能正常提交；逻辑错误导致数据未能生成；等等。然后去翻日志，看有没有报错，看Mysql监控，有没有大量未能提交的事务。逐步就定位到问题了。</p><h2 id="技术深度-vs-技术广度"><a href="#技术深度-vs-技术广度" class="headerlink" title="技术深度 vs 技术广度"></a>技术深度 vs 技术广度</h2><p>最后说解决问题，对大部分程序员来说，这个一直会是做的最多的事。而程序员的解决问题，也就是把现实中的概念在计算机中表示出来。其中最关键的，我理解有两个，一是分解任务，把一个大任务拆分成若干个独立的子任务；二是迅速的理解并使用新工具能力，也就是持续学习的能力。很多人会经常抱怨程序员要学的东西太多了，学不过来了。不过一定要注意哪些东西是值得花费大量时间去学的，哪些东西是简单看看就足够了的。发现有很多人热衷于工具的学习，喜欢写各种版本的Hello World, 这个在我看来就是无效的学习。编程方面的学习，需要兼顾深度和广度，深度，是需要非常透彻的研究一些技术，这个看了一些之后，你会发现各种技术，看起来可能没什么关系，比如数据库和消息队列，但是其中就是会有非常多相似的概念和思想。学过一些之后，就可以建立起自己的知识体系了，然后再去学其他的东西，就会非常快。</p><p>因为一个新技术的产生，必然是迫切的要解决一些实际问题。比如为什么会有clickHouse, 因为mysql等的按行存储不适合数据分析，而市面上又没有性能、稳定性都比较好的列存储数据库。再比如jedis直接连redis-server, 在多线程环境下表现不好，所以有了Lettuce。当知道要解决什么问题后，就可以先试着想一下换成你的话，有什么解决方案。然后再去看看人家实际的解决方案。如果方案很接近，那么恭喜你，你对技术的理解已经又上了一个高度。如果不一样，那也恭喜你，又学到了一种新的思想。这些思想，在以后做业务的时候，随时都可以化用。</p><h2 id="我的职业观"><a href="#我的职业观" class="headerlink" title="我的职业观"></a>我的职业观</h2><p>共勉。</p><p>原文地址: <a href="https://lichuanyang.top/posts/27398/">https://lichuanyang.top/posts/27398/</a></p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-程序员是吃青春饭吗？35-岁后做什么？"><a href="#Q-程序员是吃青春饭吗？35-岁后做什么？" class="headerlink" title="Q: 程序员是吃青春饭吗？35 岁后做什么？"></a>Q: 程序员是吃青春饭吗？35 岁后做什么？</h3><p>把程序员看作”吃青春饭”是一种误解。作者认为程序员的本质是<strong>发现问题、分析问题、解决问题</strong>这三种能力——这些能力是随经验增长的，绝非青春饭。35 岁后的出路取决于你在前十年积累了什么：如果只停留在”被告知需求 → 编码实现”的层面，年龄确实会成为劣势；但如果积累了<strong>对现实世界的认知能力</strong>和<strong>将现实映射为程序的能力</strong>，35 岁后恰恰是价值最高的阶段。解决中年危机的唯一方式是在每一份工作中持续获得可以带走的能力。</p><h3 id="Q-技术转管理好不好？什么时候该转？"><a href="#Q-技术转管理好不好？什么时候该转？" class="headerlink" title="Q: 技术转管理好不好？什么时候该转？"></a>Q: 技术转管理好不好？什么时候该转？</h3><p>这个问题文章没有直接回答，但从作者的能力模型框架可以推导：技术和管理只是解决问题的<strong>不同手段</strong>，核心问题始终是”你最有信心的能力是什么”。如果对现实世界的认知能力（理解业务、沟通协调、资源整合）强于将现实映射为程序的能力，管理方向更适合；反之则继续深耕技术。<strong>不必把两者对立起来</strong>——很多技术 Leader 本质上是在用技术视角做管理，而纯管理岗位如果没有技术判断力，在技术团队中很难真正发挥作用。关键不是”该不该转”，而是”转之前有没有积累足够的技术深度”。</p><h3 id="Q-技术深度和技术广度哪个更重要？如何平衡？"><a href="#Q-技术深度和技术广度哪个更重要？如何平衡？" class="headerlink" title="Q: 技术深度和技术广度哪个更重要？如何平衡？"></a>Q: 技术深度和技术广度哪个更重要？如何平衡？</h3><p>作者的观点是：<strong>深度先行，广度在后</strong>。先深入研究几个核心技术，建立起知识体系后，再去看其他技术会发现有大量相似的概念和思想（如数据库和消息队列中都有 WAL、分区、复制等模式）。作者的判断标准很实用：当你学一个新技术时，先想”如果让我来解决这个问题我会怎么设计”，再看实际方案——如果方案接近，说明理解已到新高度；如果不一样，就学到了一种新的思想。至于”写各种 Hello World”式的学习，在作者看来是<strong>无效的</strong>。</p><h3 id="Q-如何发现代码或系统中的”隐性”问题？"><a href="#Q-如何发现代码或系统中的”隐性”问题？" class="headerlink" title="Q: 如何发现代码或系统中的”隐性”问题？"></a>Q: 如何发现代码或系统中的”隐性”问题？</h3><h2 id="作者给出了一个判断准则：知道”正常状态”应该是什么样子，与正常状态不一致的就是问题。具体信号包括：1-单元测试很难写-→-代码设计可能有问题；2-MySQL-batch-请求耗时就应毫秒级，如果数据量轻微增加耗时陡增-→-可能没真正做批量；3-一个语义上很简单的需求（如”查用户最近十条飞行记录”），实现却很复杂-→-数据模型或主键设计可能背离业务本质。这些判断力来源于对现实世界的认知（知道什么是”正常”）和对设计模式等基础知识的理解（知道”正常”在代码中应该长什么样）。"><a href="#作者给出了一个判断准则：知道”正常状态”应该是什么样子，与正常状态不一致的就是问题。具体信号包括：1-单元测试很难写-→-代码设计可能有问题；2-MySQL-batch-请求耗时就应毫秒级，如果数据量轻微增加耗时陡增-→-可能没真正做批量；3-一个语义上很简单的需求（如”查用户最近十条飞行记录”），实现却很复杂-→-数据模型或主键设计可能背离业务本质。这些判断力来源于对现实世界的认知（知道什么是”正常”）和对设计模式等基础知识的理解（知道”正常”在代码中应该长什么样）。" class="headerlink" title="作者给出了一个判断准则：知道”正常状态”应该是什么样子，与正常状态不一致的就是问题。具体信号包括：1) 单元测试很难写 → 代码设计可能有问题；2) MySQL batch 请求耗时就应毫秒级，如果数据量轻微增加耗时陡增 → 可能没真正做批量；3) 一个语义上很简单的需求（如”查用户最近十条飞行记录”），实现却很复杂 → 数据模型或主键设计可能背离业务本质。这些判断力来源于对现实世界的认知（知道什么是”正常”）和对设计模式等基础知识的理解（知道”正常”在代码中应该长什么样）。"></a>作者给出了一个判断准则：<strong>知道”正常状态”应该是什么样子</strong>，与正常状态不一致的就是问题。具体信号包括：1) 单元测试很难写 → 代码设计可能有问题；2) MySQL batch 请求耗时就应毫秒级，如果数据量轻微增加耗时陡增 → 可能没真正做批量；3) 一个语义上很简单的需求（如”查用户最近十条飞行记录”），实现却很复杂 → 数据模型或主键设计可能背离业务本质。这些判断力来源于对现实世界的认知（知道什么是”正常”）和对设计模式等基础知识的理解（知道”正常”在代码中应该长什么样）。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/27398/</id>
    <link href="https://lichuanyang.top/posts/27398/"/>
    <published>2020-03-18T09:21:18.000Z</published>
    <summary>从「做的事」和「能力模型」两个维度，分享对程序员职业的长期思考和认知迭代。</summary>
    <title>聊聊程序员的职业生涯，我对程序员这个职业的理解</title>
    <updated>2026-06-27T02:15:49.473Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="数据库" scheme="https://lichuanyang.top/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <category term="mysql" scheme="https://lichuanyang.top/tags/mysql/"/>
    <category term="batch不生效" scheme="https://lichuanyang.top/tags/batch%E4%B8%8D%E7%94%9F%E6%95%88/"/>
    <content>
      <![CDATA[<p>众所周知，对于mysql，使用批量操作，可以大幅度提升大数据量下操作的性能。不过，在java中使用mysql时，有些细节务必注意，否则就会导致batch操作不生效，也就享受不到批量操作的性能了。</p><span id="more"></span><h2 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a>问题现象</h2><p>前阵子，发现一条sql的性能明细不正常，几千条数据批量insert，耗时居然到了秒级。排除了服务器性能、网络的问题后，开始怀疑是代码的问题，导致batch操作没有真正生效，感觉只有一条一条去跟数据库交互的话才会出现这种性能。</p><h2 id="MySQL-JDBC-Batch-原理"><a href="#MySQL-JDBC-Batch-原理" class="headerlink" title="MySQL JDBC Batch 原理"></a>MySQL JDBC Batch 原理</h2><p>调了一下代码，翻到了mysql-connector里，关键的代码是这么一块：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (!<span class="built_in">this</span>.batchHasPlainStatements &amp;&amp; <span class="built_in">this</span>.connection.getRewriteBatchedStatements()) &#123;</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">if</span> (canRewriteAsMultiValueInsertAtSqlLevel()) &#123;</span><br><span class="line">                        <span class="keyword">return</span> executeBatchedInserts(batchTimeout);</span><br><span class="line">                    &#125;</span><br><span class="line"></span><br><span class="line">                    <span class="keyword">if</span> (<span class="built_in">this</span>.connection.versionMeetsMinimum(<span class="number">4</span>, <span class="number">1</span>, <span class="number">0</span>) &amp;&amp; !<span class="built_in">this</span>.batchHasPlainStatements &amp;&amp; <span class="built_in">this</span>.batchedArgs != <span class="literal">null</span></span><br><span class="line">                            &amp;&amp; <span class="built_in">this</span>.batchedArgs.size() &gt; <span class="number">3</span> <span class="comment">/* cost of option setting rt-wise */</span>) &#123;</span><br><span class="line">                        <span class="keyword">return</span> executePreparedBatchAsMultiStatement(batchTimeout);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                <span class="keyword">return</span> executeBatchSerially(batchTimeout);</span><br></pre></td></tr></table></figure><p>可以看到有executeBatchedInserts和executeBatchSerially两个分支，如果执行到executeBatchSerially的话，其实就是执行了个“假的”batch, 在这个方法里会每条数据单独去跟mysql交互。</p><h2 id="为什么-batch-没生效"><a href="#为什么-batch-没生效" class="headerlink" title="为什么 batch 没生效"></a>为什么 batch 没生效</h2><p>为了能顺利的执行进executeBatchedInserts， 可以看到有一些先决条件：batchHasPlainStatements， connection.getRewriteBatchedStatements()， canRewriteAsMultiValueInsertAtSqlLevel。</p><p>事实上，为了能顺利的执行批量sql, mysql的驱动会对sql做一次“改写（rewrite）”，这个改写是什么意思呢？其实很简单，比如本来有一堆sql:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">insert into</span> () <span class="keyword">values</span>()</span><br><span class="line"><span class="keyword">insert into</span> () <span class="keyword">values</span>()</span><br><span class="line">...</span><br><span class="line"><span class="keyword">insert into</span> () <span class="keyword">values</span>()</span><br></pre></td></tr></table></figure><p>改写完之后，变成了这样:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">insert into</span> () <span class="keyword">values</span> () () ()</span><br></pre></td></tr></table></figure><p>变成了一条sql, 当然就是与mysql交互一次了。</p><p>成功rewrite后，才能批量执行sql。上边的几个条件也基本都是为了能让sql进行改写。这里边关键的其实就是两个点，一是数据库连接层面，需要配置rewriteBatchedStatements&#x3D;true， 允许对sql进行改写。第二是sql层面，得要求这个sql能被改写。我们知道，mysql有这样一种语法:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">insert into</span> () <span class="keyword">set</span> x<span class="operator">=</span>x,y<span class="operator">=</span>y</span><br></pre></td></tr></table></figure><p>这种语法本身不属于sql标准，是mysql的“方言”，也是没有批量操作的形式的。因此，如果把sql写成这种形式，就不然没法执行真正的批量操作了。</p><h2 id="原文地址-https-lichuanyang-top-posts-63688"><a href="#原文地址-https-lichuanyang-top-posts-63688" class="headerlink" title="原文地址: https://lichuanyang.top/posts/63688/"></a>原文地址: <a href="https://lichuanyang.top/posts/63688/">https://lichuanyang.top/posts/63688/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/63688/</id>
    <link href="https://lichuanyang.top/posts/63688/"/>
    <published>2020-03-13T09:54:38.000Z</published>
    <summary>分析 Java 中 MySQL 批量操作不生效的常见原因，排查 batch 性能异常的完整思路。</summary>
    <title>通过mysql批量操作不生效问题聊聊java里mysql的batch</title>
    <updated>2026-06-27T03:46:06.802Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="开发规范" scheme="https://lichuanyang.top/tags/%E5%BC%80%E5%8F%91%E8%A7%84%E8%8C%83/"/>
    <category term="单元测试" scheme="https://lichuanyang.top/tags/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95/"/>
    <category term="单元测试怎么写" scheme="https://lichuanyang.top/tags/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95%E6%80%8E%E4%B9%88%E5%86%99/"/>
    <content>
      <![CDATA[<p>之前一段时间，在尝试完善项目的单元测试，做起来才发现，其实自己对于单元测试该怎么写一直没有特别清晰的认识，因此查了一些资料, 查的过程中发现，其实每个人的看法都不尽相同。每阅读一篇文章，都是对自己认知的一次更新。本文就是结合前人的看法，和自己的实践经验，总结出一些我自己当前认知状态下认为正确的东西，分享给大家。估计也不会是最正确的，也欢迎大家留言讨论。</p><span id="more"></span><p>先说一下我们写单元测试的目的，这个是下边所有讨论的基础。单元测试的意义应当在于保障修改代码的时候，保障存量代码的正确性。也就是说，修改代码后，执行一遍已有的测试用例，能执行的话就代表之前的逻辑依然是可用的。</p><p>下面就具体列一下个人认为写单元测试应当遵守的准侧。</p><h3 id="避免对环境的依赖"><a href="#避免对环境的依赖" class="headerlink" title="避免对环境的依赖"></a>避免对环境的依赖</h3><p>单元测试不应当依赖于环境，包括对网络、db、io、数据、外部接口等的依赖。这一点应该争议不大，因为单元测试会有很多的执行场景，比如开发人员手动触发、提交代码时自动触发、打包时自动触发等，如果单元测试不能解除对环境的依赖，它的执行场景就会大大受限。</p><p>实现这一点，需要借助很多工具，例如内存数据库、redis的内嵌server, mock等待，势必会给写单元测试这个过程增加很多工作量，但是是无法避免的。</p><h3 id="针对业务逻辑测试"><a href="#针对业务逻辑测试" class="headerlink" title="针对业务逻辑测试"></a>针对业务逻辑测试</h3><p>测试应当是针对业务逻辑，而不是实现逻辑。这一点是争议会比较多的一点，主要是大家对于“单元”这个词的定义无法统一。有人认为一个函数就是一个“单元”，但是如果按这个标准去做单元测试，单元测试是达不到预期效果的。</p><p>举个简单的例子，代码重构的时候，要不要改单元测试？答案当然是不能，如果每次代码重构都要对应修改单元测试的话，单元测试的作用可以说就失去了一大半。所以说测试的一个“单元”应当是一个业务上的单元。当然这一点也不能太死板，比如你写了一个很复杂的方法，针对这个方法写一个单元测试也完全没有问题。这一点其实可以换个更准确的方法：针对意图写单元测试，而不是针对实现。也就是说，把代码的实现逻辑当做一个黑盒，只针对输入输出做测试。</p><h3 id="去mock化"><a href="#去mock化" class="headerlink" title="去mock化"></a>去mock化</h3><p>前边第一点里，我们提到过可以利用mock避免对外部环境的依赖。但是一定不能滥用mock, 可以阅读一下<a href="https://gitbook.cn/books/58fa1af500a2684bf77511bc/index.html">mock七宗罪</a> , 一个原则就是能不mock的时候就不要mock。什么时候是必须要mock的呢？就是前边说的要依赖外部环境的时候。除了这种情况，都不要mock. </p><h3 id="单元测试倒逼重构"><a href="#单元测试倒逼重构" class="headerlink" title="单元测试倒逼重构"></a>单元测试倒逼重构</h3><p>在写单元测试的时候，会发现有些测试会非常难写，而具体分析难写的原因，会发现有一些是因为代码本身写的不好。比如，一个类的依赖特别多，可能就是因为这个类的职责不明确，将不相关的代码放到了一起。这时候，就应该直接将代码重构，拆分成几个类，写单元测试的难度也会随之降低。</p><p>原文地址: <a href="https://lichuanyang.top/posts/695/">https://lichuanyang.top/posts/695/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/695/</id>
    <link href="https://lichuanyang.top/posts/695/"/>
    <published>2019-09-26T12:54:36.000Z</published>
    <summary>结合前人观点和实践经验，总结单元测试该怎么写，从目的、原则到具体写法的系统性指南。</summary>
    <title>【实践经验】单元测试怎么写</title>
    <updated>2026-06-08T07:50:49.071Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="消息队列" scheme="https://lichuanyang.top/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    <category term="rocketMq" scheme="https://lichuanyang.top/tags/rocketMq/"/>
    <category term="消息队列" scheme="https://lichuanyang.top/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    <content>
      <![CDATA[<p>RocketMQ 的 MessageQueue 有一个独特设计：将队列拆分为 <code>readQueueNums</code>（读队列数）和 <code>writeQueueNums</code>（写队列数）。这两个值在绝大多数情况下必须相等，一旦不等就会产生严重问题——那为什么要拆开？答案在于<strong>平滑扩缩容</strong>。</p><span id="more"></span><h2 id="MessageQueue-是什么？"><a href="#MessageQueue-是什么？" class="headerlink" title="MessageQueue 是什么？"></a>MessageQueue 是什么？</h2><p>在 RocketMQ 中，Topic 是消息的逻辑分类，而 MessageQueue 是 Topic 的物理分片，类似于 Kafka 的 Partition。关键约束：<strong>Consumer 的数量不能超过 Queue 的数量</strong>——因为每个 Queue 同一时间只能被一个 Consumer 消费。</p><h2 id="正常状态下：readQueue-writeQueue"><a href="#正常状态下：readQueue-writeQueue" class="headerlink" title="正常状态下：readQueue &#x3D; writeQueue"></a>正常状态下：readQueue &#x3D; writeQueue</h2><p>正常情况下，<code>readQueueNums == writeQueueNums</code>，生产者向所有 Queue 写入消息，消费者从所有 Queue 读取消息，一切正常。</p><p>但如果两者不一致：</p><table><thead><tr><th>状态</th><th>后果</th></tr></thead><tbody><tr><td>writeQueue &gt; readQueue</td><td>部分 Queue 有写无读 → <strong>消息永远不会被消费，积压</strong></td></tr><tr><td>readQueue &gt; writeQueue</td><td>部分 Consumer 空跑 → <strong>空转浪费资源，永远收不到消息</strong></td></tr></tbody></table><p>既然一致时没问题、不一致会出事，那拆分开的意义是什么？</p><h2 id="核心价值：平滑扩缩容"><a href="#核心价值：平滑扩缩容" class="headerlink" title="核心价值：平滑扩缩容"></a>核心价值：平滑扩缩容</h2><h3 id="扩容流程（以-4-→-8-为例）"><a href="#扩容流程（以-4-→-8-为例）" class="headerlink" title="扩容流程（以 4 → 8 为例）"></a>扩容流程（以 4 → 8 为例）</h3><p>没有 read&#x2F;write 分离时的困境：修改 Queue 数量是瞬间生效的，会出现以下问题：</p><ul><li>如果先通知 Producer 新的 Queue 数，Producer 开始向新 Queue 写消息，但 Consumer 还没感知到新 Queue → <strong>消息有写无读</strong></li><li>如果先通知 Consumer，Consumer 开始监听新 Queue，但 Producer 还没开始写 → <strong>Consumer 空跑</strong></li></ul><p>有 read&#x2F;write 分离后的<strong>两步式扩容</strong>：</p><table><thead><tr><th>步骤</th><th>操作</th><th>状态</th><th>效果</th></tr></thead><tbody><tr><td><strong>Step 1</strong></td><td><code>readQueueNums: 4→8</code></td><td>write&#x3D;4, read&#x3D;8</td><td>新增 4 个 Consumer，开始监听新 Queue（此时新 Queue 还没有消息，空跑无害）</td></tr><tr><td><strong>Step 2</strong></td><td><code>writeQueueNums: 4→8</code></td><td>write&#x3D;8, read&#x3D;8</td><td>Producer 开始向新 Queue 写消息，Consumer 已经在等了，消息立刻被消费</td></tr></tbody></table><p><strong>整个过程没有消息丢失，也没有 Consumer 空转浪费</strong>。</p><p>如果把两个步骤反过来（先改 write 再改 read），效果也一样——因为 Consumer 扩容是最慢的（需要启动新实例），所以先完成扩容、再开始写消息是最合理的顺序。</p><h3 id="缩容流程（以-8-→-4-为例）"><a href="#缩容流程（以-8-→-4-为例）" class="headerlink" title="缩容流程（以 8 → 4 为例）"></a>缩容流程（以 8 → 4 为例）</h3><table><thead><tr><th>步骤</th><th>操作</th><th>状态</th><th>效果</th></tr></thead><tbody><tr><td><strong>Step 1</strong></td><td><code>writeQueueNums: 8→4</code></td><td>write&#x3D;4, read&#x3D;8</td><td>Producer 停止向即将下线的 Queue 写消息（但 Consumer 还在读，现有消息不会丢失）</td></tr><tr><td><strong>Step 2</strong></td><td><code>readQueueNums: 8→4</code></td><td>write&#x3D;4, read&#x3D;4</td><td>等待旧 Queue 中的消息全部消费完后，Consumer 缩小</td></tr></tbody></table><h2 id="对比：Kafka-的分区扩缩容"><a href="#对比：Kafka-的分区扩缩容" class="headerlink" title="对比：Kafka 的分区扩缩容"></a>对比：Kafka 的分区扩缩容</h2><p>Kafka 的 Partition 数量<strong>只能增加，不能减少</strong>。扩容操作相对简单——增加 Partition 后，Consumer 通过 Rebalance 自动感知。但缩容在 Kafka 中是不支持的操作，一旦 Partition 数量设大了就只能接受。</p><p>RocketMQ 通过 readQueue&#x2F;writeQueue 分离，支持了<strong>双向平滑扩缩容</strong>，这是在阿里大规模生产环境中打磨出来的设计智慧。</p><h2 id="图解"><a href="#图解" class="headerlink" title="图解"></a>图解</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">正常状态：write=4, read=4</span><br><span class="line">Producer ──→ [Q0][Q1][Q2][Q3] ──→ Consumer Group (4 instances)</span><br><span class="line">              ✓   ✓   ✓   ✓</span><br><span class="line"></span><br><span class="line">扩容 Step1：write=4, read=8</span><br><span class="line">Producer ──→ [Q0][Q1][Q2][Q3] ──→ Consumer Group (8 instances)</span><br><span class="line">              ✓   ✓   ✓   ✓       [Q4-Q7 空跑等待]</span><br><span class="line"></span><br><span class="line">扩容 Step2：write=8, read=8</span><br><span class="line">Producer ──→ [Q0][Q1][Q2][Q3][Q4][Q5][Q6][Q7] ──→ Consumer Group (8 instances)</span><br><span class="line">              ✓   ✓   ✓   ✓   ✓   ✓   ✓   ✓</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><code>readQueueNums</code> 和 <code>writeQueueNums</code> 的分离是 RocketMQ 实现<strong>无损扩缩容</strong>的关键设计。日常运维中这两个值应该保持相等，只在执行扩缩容操作的短暂窗口内允许它们不同。这个设计看似违反了”读写应该一致”的直觉，但正是这种”允许短暂的、可控的不一致”，换来了扩缩容过程的完全平滑。</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/32580/</id>
    <link href="https://lichuanyang.top/posts/32580/"/>
    <published>2019-05-18T07:23:02.000Z</published>
    <summary>解释 RocketMQ 中 MessageQueue 为什么要拆分成 readQueue 和 writeQueue 的设计原因。</summary>
    <title>关于rocketmq的readQueue和writeQueue</title>
    <updated>2026-06-27T02:31:54.495Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="数据库" scheme="https://lichuanyang.top/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <category term="大数据" scheme="https://lichuanyang.top/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <category term="clickhouse" scheme="https://lichuanyang.top/tags/clickhouse/"/>
    <category term="clickhouse介绍" scheme="https://lichuanyang.top/tags/clickhouse%E4%BB%8B%E7%BB%8D/"/>
    <content>
      <![CDATA[<p>Clickhouse是Yandex开源的一个用于实时数据分析的数据库，一开始就用在yandex内部的多个数据分析业务上。要介绍clickhouse，还是需要先介绍一下yandex。Clickhouse为什么会出现，其实和yandex的业务关系非常大。Yandex是俄罗斯最大的搜索引擎，会有很多数据分析的业务，其中数据量最大的业务，就是Yandex.Metrica，这是一个和百度统计类似的网站数据分析服务，数据量也仅次于google analysis。自从Clickhouse开源后，在国内外的很多公司的线上业务都已经开始使用。 因此，写这篇clickhouse教程，对clickhouse做一个基础的介绍。</p><span id="more"></span><h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>Clickhouse是极其适合OLAP（联机分析处理）问题的一个数据库。这类问题有如下一些特点：</p><ul><li>请求以读为主，数据添加、更新一般以批量的形式进行；</li><li>表可以很宽，但是实际查询时只会用到有限的几列；</li><li>列值较小，一般是数字或者短字符串;</li><li>查询结果集的大小显著小于源数据；</li><li>事务处理需求较弱</li></ul><p>根据clickhouse提供的性能测试结果，clickhouse的性能要大大领先于所有同类产品。</p><p>针对这类问题，用到的最核心的思路就是列式存储。目前常用的数据库，像MySQL等，都是行式存储。而列式存储，简单的说就是把同一列的数据存储在一起，像下图这样。</p><p><img src="/img/clickhouse/column.png" alt="列式存储说明"></p><p>相对行存储， 列存储查询时只会读取涉及到的列，减少io开销；任何一列都可以作为索引；但是数据写入会相对麻烦一些。对比前面我们介绍的OLAP问题的特点，可以看到，列式存储是及其适合这种在线数据分析的。</p><h2 id="ClickHouse的存储引擎"><a href="#ClickHouse的存储引擎" class="headerlink" title="ClickHouse的存储引擎"></a>ClickHouse的存储引擎</h2><p>ClickHouse中提供了很多种存储引擎，不过90%以上的情况下用MergeTree这个引擎就可以了。下面详细介绍一下这个引擎。</p><p>简单来说，mergeTree有这么几个特点：</p><ul><li>分块(part)存储(一般按日期)；</li><li>part内按主键排序，并分成多个block; </li><li>插入时生成新的part;</li><li>异步merge</li></ul><p>首先来看一下mergeTree的文件结构。(本节的图片都来自clickhouse中文社区（<a href="http://clickhouse.com.cn/%EF%BC%89%E5%86%85%E6%9D%A5%E8%87%AA%E6%96%B0%E6%B5%AA%E9%AB%98%E9%B9%8F%E8%80%81%E5%B8%88%E7%9A%84%E5%88%86%E4%BA%AB">http://clickhouse.com.cn/）内来自新浪高鹏老师的分享</a>)<br><img src="/img/clickhouse/folder.png"><br>其中20171001….这个目录就是代表一个part，下面这些文件，columns.txt记录列信息；每一列有一个bin文件和mrk文件, 其中bin文件是实际数据，primary.idx存储主键信息，结构与mrk一样，类似于稀疏索引。</p><p><img src="/img/clickhouse/mergetree.jpg" alt="具体存储结构"></p><p>这里展示了mrk文件和primary文件的具体结构，可以看到，数据是按照主键排序的，并且会每隔一定大小分隔出很多个block。每个block中也会抽出一个数据作为索引，放到primary.idx和各列的mrk文件中。</p><p>而利用mergetree进行查询时，最关键的步骤就是定位block，这里会根据查询的列是否在主键内有不同的方式。根据主键查询时性能会较好，但是非主键查询时，由于按列存储的关系，虽然会做一次全扫描，性能也没有那么差。所以索引在clickhouse里并不像mysql那么关键。实际使用时一般需要添加按日期的查询条件，保障非主键查询时的性能。</p><p>找到对应的block之后，就是在block内查找数据，获取需要的行，再拼装需要的其他列数据。</p><h2 id="其他特性"><a href="#其他特性" class="headerlink" title="其他特性"></a>其他特性</h2><h3 id="数据压缩"><a href="#数据压缩" class="headerlink" title="数据压缩"></a>数据压缩</h3><p>一些面向列的DBMS（例如InfiniDB CE和MonetDB）没有使用数据压缩。但是，对于clickhouse的性能提升，数据压缩起到了很大作用。对于列式存储来说。相同的字段存储在一起，类型一致，数据类似，更方便进行压缩。clickhouse支持LZ4和ZSTD等压缩算法。</p><h3 id="基于磁盘存储"><a href="#基于磁盘存储" class="headerlink" title="基于磁盘存储"></a>基于磁盘存储</h3><p>clickhouse支持普通磁盘；因此在成本上有较大优势。<br>提供了针对raid, ssd,大内存等的配置，如果有的话，也能加以利用。</p><p>数据按主键顺序存储，因此可以实现延时极低的查询。</p><h3 id="向量化引擎"><a href="#向量化引擎" class="headerlink" title="向量化引擎"></a>向量化引擎</h3><p>现代CPU中有一种叫SIMD的机制，即Single Instruction, Multiple Data，一条指令操作多个数据。对于列式存储的数据，可以很方便的利用这一机制，更好的发挥cpu的性能。</p><h3 id="多核，多服务器并行处理"><a href="#多核，多服务器并行处理" class="headerlink" title="多核，多服务器并行处理"></a>多核，多服务器并行处理</h3><p>在clickHouse中，数据会存储到不同的分片上。查询会在多个分片上并行执行。</p><p>而在每台服务器上，也会多核并行处理，充分利用单机性能。</p><h3 id="支持数据实时写入"><a href="#支持数据实时写入" class="headerlink" title="支持数据实时写入"></a>支持数据实时写入</h3><p>mergetree引擎下,新插入数据后，会先形成新的part, 这个时候数据就已经可以被查到，因此从数据写入到可以被查询的延时是很小的。后续不同part会继续异步进行merge, 以提高存储效率</p><h3 id="数据复制"><a href="#数据复制" class="headerlink" title="数据复制"></a>数据复制</h3><p>clickHouse是基于zookeeper的主主复制。写入任何可用的副本后，数据将分发到所有剩余的副本。复制时是按块进行，复制失败的话可以直接重试。系统在不同的副本上保持相同的数据。</p><p>此外，相对其他列存储数据库，clickhouse对sql语法的支持非常好，包括group by, order by, in, join等常用sql语句都支持。</p><p>原文地址：<a href="https://lichuanyang.top/posts/48312/">https://lichuanyang.top/posts/48312/</a></p><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><ol><li><strong>安装 ClickHouse</strong>：以 Ubuntu 为例，<code>sudo apt-get install clickhouse-server clickhouse-client</code>，启动服务后使用 <code>clickhouse-client</code> 连接。</li><li><strong>创建表</strong>：使用 MergeTree 引擎建表，指定 <code>ORDER BY</code>（排序键）和 <code>PARTITION BY</code>（分区键，通常按日期），列按需定义，无需像 MySQL 那样为每列建索引。</li><li><strong>导入数据</strong>：通过 <code>clickhouse-client --query &quot;INSERT INTO table FORMAT CSV&quot; &lt; data.csv</code> 或使用 <code>clickhouse-local</code> 工具批量导入。数据写入后立即可查，后台自动 merge。</li><li><strong>编写查询</strong>：使用标准 SQL 进行分析，ClickHouse 对 <code>GROUP BY</code>、<code>ORDER BY</code>、<code>JOIN</code> 等均有良好支持。建议查询时加上日期过滤条件以提升非主键查询性能。</li><li><strong>对比 MySQL 性能</strong>：将相同数据导入 MySQL 和 ClickHouse，运行相同的聚合分析查询，感受列式存储在 OLAP 场景下的数量级性能差异。</li></ol>]]>
    </content>
    <id>https://lichuanyang.top/posts/48312/</id>
    <link href="https://lichuanyang.top/posts/48312/"/>
    <published>2018-11-14T13:59:31.000Z</published>
    <summary>ClickHouse 实时分析数据库入门介绍，从 Yandex 的业务需求出发讲解 ClickHouse 的设计背景和核心特性。</summary>
    <title>数据分析的利器-clickhouse介绍</title>
    <updated>2026-06-27T02:31:54.491Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="位运算" scheme="https://lichuanyang.top/tags/%E4%BD%8D%E8%BF%90%E7%AE%97/"/>
    <content>
      <![CDATA[<p>位运算是计算机科学中最基础也最高效的操作之一。今天介绍一个经典的位运算应用：用一行代码完成英文字母的大小写转换。</p><span id="more"></span><h2 id="为什么需要位运算做大小写转换？"><a href="#为什么需要位运算做大小写转换？" class="headerlink" title="为什么需要位运算做大小写转换？"></a>为什么需要位运算做大小写转换？</h2><p>你可能第一反应是：Java 不是有 <code>Character.toUpperCase()</code> 和 <code>Character.toLowerCase()</code> 吗？为什么要自己写？</p><p>确实，99% 的场景下你应该用标准库。但了解这个技巧有几个价值：</p><ol><li><strong>面试中的常见考点</strong>：很多算法面试会考察这种位运算思维</li><li><strong>理解计算机底层</strong>：知道 ASCII 码的设计精妙之处</li><li><strong>嵌入式&#x2F;底层开发</strong>：在没有标准库的环境下，这就是实际操作</li><li><strong>代码简洁</strong>：<code>ch ^= 32</code> 比 <code>Character.isUpperCase(ch) ? Character.toLowerCase(ch) : ch</code> 优雅得多</li></ol><h2 id="ASCII-码的巧妙设计"><a href="#ASCII-码的巧妙设计" class="headerlink" title="ASCII 码的巧妙设计"></a>ASCII 码的巧妙设计</h2><p>要理解这个技巧，先看英文字母在 ASCII 表中的编码：</p><table><thead><tr><th>字母</th><th>十进制</th><th>十六进制</th><th>二进制</th></tr></thead><tbody><tr><td>A</td><td>65</td><td>0x41</td><td><strong>01</strong>00 0001</td></tr><tr><td>a</td><td>97</td><td>0x61</td><td><strong>01</strong>10 0001</td></tr><tr><td>B</td><td>66</td><td>0x42</td><td><strong>01</strong>00 0010</td></tr><tr><td>b</td><td>98</td><td>0x62</td><td><strong>01</strong>10 0010</td></tr><tr><td>Z</td><td>90</td><td>0x5A</td><td><strong>01</strong>01 1010</td></tr><tr><td>z</td><td>122</td><td>0x7A</td><td><strong>01</strong>11 1010</td></tr></tbody></table><p>观察规律：<strong>大写和小写字母的 ASCII 值恰好相差 32（0x20）</strong>。在二进制层面，就是第 6 位（从右数，从 0 开始）的差别：</p><ul><li>大写字母第 6 位为 <strong>0</strong>（如 A: 010<strong>0</strong> 0001）</li><li>小写字母第 6 位为 <strong>1</strong>（如 a: 011<strong>0</strong> 0001）</li></ul><p>这就是 ASCII 设计时的精妙之处——大小写转换只需要翻转一个比特！</p><h2 id="三种位运算方法"><a href="#三种位运算方法" class="headerlink" title="三种位运算方法"></a>三种位运算方法</h2><h3 id="1-异或（Toggle）：大小写互转"><a href="#1-异或（Toggle）：大小写互转" class="headerlink" title="1. 异或（Toggle）：大小写互转"></a>1. 异或（Toggle）：大小写互转</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ch ^= <span class="number">32</span>;  <span class="comment">// 等价于 ch ^= 0x20</span></span><br></pre></td></tr></table></figure><p><code>^</code>（异或）运算的规则是”相同得 0，不同得 1”。与 32（第 6 位为 1）异或，就只翻转第 6 位：大写变小小写变大。</p><h3 id="2-强制转大写"><a href="#2-强制转大写" class="headerlink" title="2. 强制转大写"></a>2. 强制转大写</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ch &amp;= ~<span class="number">32</span>;  <span class="comment">// 等价于 ch &amp;= 0xDF</span></span><br></pre></td></tr></table></figure><p><code>~32</code>（取反）使第 6 位为 0，其余位为 1。<code>&amp;</code> 运算后第 6 位清零，不论输入大小写，输出一定是大写。</p><h3 id="3-强制转小写"><a href="#3-强制转小写" class="headerlink" title="3. 强制转小写"></a>3. 强制转小写</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ch |= <span class="number">32</span>;  <span class="comment">// 等价于 ch |= 0x20</span></span><br></pre></td></tr></table></figure><p><code>|</code> 运算第 6 位置 1，不论输入大小写，输出一定是小写。</p><h2 id="边界情况"><a href="#边界情况" class="headerlink" title="边界情况"></a>边界情况</h2><p>以上操作<strong>只对英文字母有效</strong>。对非字母字符无意义，使用前需要先判断：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (ch &gt;= <span class="string">&#x27;A&#x27;</span> &amp;&amp; ch &lt;= <span class="string">&#x27;Z&#x27;</span> || ch &gt;= <span class="string">&#x27;a&#x27;</span> &amp;&amp; ch &lt;= <span class="string">&#x27;z&#x27;</span>) &#123;</span><br><span class="line">    ch ^= <span class="number">32</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="实际性能如何？"><a href="#实际性能如何？" class="headerlink" title="实际性能如何？"></a>实际性能如何？</h2><p>在现代 JVM 上，<code>Character.toUpperCase()</code> 已被优化为 intrinsic（JVM 级别的原生优化），两者性能几乎没有差距。所以结论是：<strong>了解这个技巧，日常开发优先用标准库</strong>。位运算大小写转换更适合算法题、嵌入式场景，以及读懂那些用了这个技巧的开源代码。</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-位运算大小写转换比标准库快吗？日常开发该用哪个？"><a href="#Q-位运算大小写转换比标准库快吗？日常开发该用哪个？" class="headerlink" title="Q: 位运算大小写转换比标准库快吗？日常开发该用哪个？"></a>Q: 位运算大小写转换比标准库快吗？日常开发该用哪个？</h3><p>在现代 JVM 上，<code>Character.toUpperCase()</code> &#x2F; <code>Character.toLowerCase()</code> 已被 JVM 优化为 <strong>intrinsic</strong>（JVM 级别的原生指令），与位运算版本性能几乎无差距。日常开发<strong>首选标准库</strong>——可读性好、语义清晰、有边界检查。位运算版本更适合理解计算机底层原理和应对面试场景。</p><h3 id="Q-什么场景下值得使用位运算做大小写转换？"><a href="#Q-什么场景下值得使用位运算做大小写转换？" class="headerlink" title="Q: 什么场景下值得使用位运算做大小写转换？"></a>Q: 什么场景下值得使用位运算做大小写转换？</h3><ol><li><strong>算法面试</strong>：位运算思维是常见考察点；2) <strong>嵌入式&#x2F;底层开发</strong>：没有标准库可用的环境；3) <strong>阅读源码</strong>：很多经典开源代码（如 Nginx、Redis 等 C 项目）大量使用这种技巧，需要能读懂；4) <strong>追求代码极简</strong>：<code>ch ^= 32</code> 一行搞定大小写互转，在某些场景下更优雅。</li></ol><h3 id="Q-对非字母字符执行位运算大小写转换会有什么影响？"><a href="#Q-对非字母字符执行位运算大小写转换会有什么影响？" class="headerlink" title="Q: 对非字母字符执行位运算大小写转换会有什么影响？"></a>Q: 对非字母字符执行位运算大小写转换会有什么影响？</h3><p>会有<strong>无意义的结果</strong>。位运算只翻转第 6 位，对非字母字符来说，翻转后产生的字节不一定对应有效字符。比如数字 <code>0</code>（ASCII 48 &#x3D; 0x30）与 32 异或后变成 16（DLE 控制字符）。因此使用前必须加范围判断：<code>if ((ch &gt;= &#39;A&#39; &amp;&amp; ch &lt;= &#39;Z&#39;) || (ch &gt;= &#39;a&#39; &amp;&amp; ch &lt;= &#39;z&#39;))</code>。</p><h3 id="Q-ASCII-码里大小写字母相差-32-是巧合还是有意设计？"><a href="#Q-ASCII-码里大小写字母相差-32-是巧合还是有意设计？" class="headerlink" title="Q: ASCII 码里大小写字母相差 32 是巧合还是有意设计？"></a>Q: ASCII 码里大小写字母相差 32 是巧合还是有意设计？</h3><h2 id="是有意设计。大小写字母的二进制表示恰好只差第-6-位（大写为-0、小写为-1），相差值固定为-32（0x20-0b0010-0000）。这个设计让硬件只需翻转一个比特就能完成大小写转换，在早期计算资源有限的时代极大降低了电路成本。类似的精妙设计还有：十进制数字去掉高-4-位就得到其二进制值。"><a href="#是有意设计。大小写字母的二进制表示恰好只差第-6-位（大写为-0、小写为-1），相差值固定为-32（0x20-0b0010-0000）。这个设计让硬件只需翻转一个比特就能完成大小写转换，在早期计算资源有限的时代极大降低了电路成本。类似的精妙设计还有：十进制数字去掉高-4-位就得到其二进制值。" class="headerlink" title="是有意设计。大小写字母的二进制表示恰好只差第 6 位（大写为 0、小写为 1），相差值固定为 32（0x20 &#x3D; 0b0010_0000）。这个设计让硬件只需翻转一个比特就能完成大小写转换，在早期计算资源有限的时代极大降低了电路成本。类似的精妙设计还有：十进制数字去掉高 4 位就得到其二进制值。"></a>是<strong>有意设计</strong>。大小写字母的二进制表示恰好只差第 6 位（大写为 0、小写为 1），相差值固定为 32（0x20 &#x3D; 0b0010_0000）。这个设计让硬件只需翻转一个比特就能完成大小写转换，在早期计算资源有限的时代极大降低了电路成本。类似的精妙设计还有：十进制数字去掉高 4 位就得到其二进制值。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/9193/</id>
    <link href="https://lichuanyang.top/posts/9193/"/>
    <published>2018-11-14T13:58:47.000Z</published>
    <summary>利用位运算巧妙实现英文大小写转换，深入理解 ASCII 码的二进制特征。</summary>
    <title>通过位运算转换大小写</title>
    <updated>2026-06-27T03:45:23.481Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="activemq系列文章" scheme="https://lichuanyang.top/categories/activemq%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0/"/>
    <category term="activemq" scheme="https://lichuanyang.top/tags/activemq/"/>
    <content>
      <![CDATA[<h2 id="问题场景"><a href="#问题场景" class="headerlink" title="问题场景"></a>问题场景</h2><p>之前在<a href="https://lichuanyang.top/posts/48216/">另一篇文章</a>里介绍过使用activemq时，client端的基本语法。</p><p>值得注意的是消费者，</p><span id="more"></span><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">consume</span><span class="params">()</span> <span class="keyword">throws</span> JMSException &#123;</span><br><span class="line">        <span class="type">ActiveMQConnectionFactory</span> <span class="variable">cf</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ActiveMQConnectionFactory</span>();</span><br><span class="line">        cf.setBrokerURL(<span class="string">&quot;tcp://localhost:61616&quot;</span>);</span><br><span class="line">        <span class="type">Connection</span> <span class="variable">connection</span> <span class="operator">=</span> cf.createConnection();</span><br><span class="line">        connection.start();</span><br><span class="line">        <span class="type">Session</span> <span class="variable">session</span> <span class="operator">=</span> connection.createSession(<span class="literal">false</span>, Session.AUTO_ACKNOWLEDGE);</span><br><span class="line">        <span class="type">Queue</span> <span class="variable">queue</span> <span class="operator">=</span> session.createQueue(<span class="string">&quot;TEST&quot;</span>);</span><br><span class="line">        <span class="type">MessageConsumer</span> <span class="variable">consumer</span> <span class="operator">=</span> session.createConsumer(queue);</span><br><span class="line"></span><br><span class="line"><span class="comment">//        Message message = consumer.receive();</span></span><br><span class="line"><span class="comment">//        TextMessage textMessage = (TextMessage) message;</span></span><br><span class="line"><span class="comment">//        System.out.println(textMessage.getText());</span></span><br><span class="line"></span><br><span class="line">        consumer.setMessageListener(message -&gt; &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;consumer1: &quot;</span> + ((TextMessage) message).getText());</span><br><span class="line">            &#125; <span class="keyword">catch</span> (JMSException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        <span class="type">Session</span> <span class="variable">session2</span> <span class="operator">=</span> connection.createSession(<span class="literal">false</span>, Session.AUTO_ACKNOWLEDGE);</span><br><span class="line">        <span class="type">Queue</span> <span class="variable">queue2</span> <span class="operator">=</span> session2.createQueue(<span class="string">&quot;TEST&quot;</span>);</span><br><span class="line">        <span class="type">MessageConsumer</span> <span class="variable">consumer2</span> <span class="operator">=</span> session2.createConsumer(queue2);</span><br><span class="line">        consumer2.setMessageListener(message -&gt; &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;consumer2: &quot;</span> + ((TextMessage) message).getText());</span><br><span class="line">            &#125; <span class="keyword">catch</span> (JMSException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        <span class="type">Queue</span> <span class="variable">queue3</span> <span class="operator">=</span> session2.createQueue(<span class="string">&quot;TEST&quot;</span>);</span><br><span class="line">        <span class="type">MessageConsumer</span> <span class="variable">consumer3</span> <span class="operator">=</span> session2.createConsumer(queue3);</span><br><span class="line">        consumer3.setMessageListener(message -&gt; &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;consumer3: &quot;</span> + ((TextMessage) message).getText());</span><br><span class="line">            &#125; <span class="keyword">catch</span> (JMSException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            TimeUnit.MINUTES.sleep(<span class="number">100</span>);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h2 id="多线程消费方式分析"><a href="#多线程消费方式分析" class="headerlink" title="多线程消费方式分析"></a>多线程消费方式分析</h2><p>可以看到，实际上有三种方式都可以实现多线程消费同一队列，分别是定义多个connection, session和consumer。这三种方式在应用层面的效果是一样的，都是会生成多个消费者，并行处理队列中的消息。但是在性能上，这三种方式会有较明显的差别。下边详细介绍一下。</p><p>首先，定于多个consumer只是一种伪并行，并没有真正并发消费。原因是在JMS协议列，一个session在同一时间点只能被一个线程使用，所以多consumer复用同一session时，只是这些consumer轮流使用这一个session。</p><p>而可以考虑开多个connection对应的是物理的tcp连接，一个producer可以新建多个session，这些session就是复用同一个tcp链接。多session和多可以考虑开多个connection都是真正的并发操作，区别只在于是开一个还是多个tcp连接。这个造成的影响很容易理解，多个tcp连接，可以处理更大的网络流量，当然在建立、维护连接时也会带来一定开销。</p><h2 id="总结与建议"><a href="#总结与建议" class="headerlink" title="总结与建议"></a>总结与建议</h2><p>总的来说，大部分情况下，我们可以定义多session来实现activemq的并行消费。在流量较大时，可以考虑开多个connection。而多个consumer, 目前没有想出有什么场景能用到。</p><h2 id="原文链接：https-lichuanyang-top-posts-20459"><a href="#原文链接：https-lichuanyang-top-posts-20459" class="headerlink" title="原文链接：https://lichuanyang.top/posts/20459/"></a>原文链接：<a href="https://lichuanyang.top/posts/20459/">https://lichuanyang.top/posts/20459/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/20459/</id>
    <link href="https://lichuanyang.top/posts/20459/"/>
    <published>2018-09-16T01:58:11.000Z</published>
    <summary>对比 ActiveMQ 消费端实现多线程的三种方式（多 connection、多 session、多 consumer），分析它们在性能上的差异。</summary>
    <title>activemq多线程消费的不同处理方式</title>
    <updated>2026-06-27T03:44:21.604Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="架构设计" scheme="https://lichuanyang.top/categories/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    <category term="设计" scheme="https://lichuanyang.top/tags/%E8%AE%BE%E8%AE%A1/"/>
    <content>
      <![CDATA[<p>前一段时间参加了优化一个老的计费系统，学习了一些高并发下做余额扣减的常用手段，也做了一些尝试，因此在这里总结记录一下, 在高并发下对。</p><span id="more"></span><h2 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h2><p>对于一个计费系统来说，并发问题事实上分为两类，一类是应用并发高，也就是纯粹的用户量大，访问量多，这类问题和一般的高并发问题没有区别，用分布式等手段就可以解决；另外一类问题则是一般分布式手段无法解决的用户高并发问题，也是本文要着重说的。<br>这类问题源自对某些高频账号，大量的并发访问，会导致瓶颈首先出现在某些数据库记录上，大量操作由于无法竞争到数据库的行锁而导致等待，这些等待中的操作又会占用其他资源，最终导致系统不可用。<br>针对这类问题，下边介绍一些常用的处理办法。</p><h2 id="不设置余额字段"><a href="#不设置余额字段" class="headerlink" title="不设置余额字段"></a>不设置余额字段</h2><p>由于对于一个稳定的计费来说，一定是会记录计费流水明细的，所以完全可以不设置余额字段，而采用根据流水明细计算的方式来获取余额。<br>不过这种方法不是万能的，比如拿广告业务的计费系统来说，频率非常高，而每次的金额很小，这时候想通过计算求和去算余额，显然是不现实的。</p><h2 id="合并与拆分"><a href="#合并与拆分" class="headerlink" title="合并与拆分"></a>合并与拆分</h2><p>这是两种方式，因为有一些相似之处，都是要降低对单条数据库记录的访问压力，所以也就放到一起说了。<br>合并，就是对单个账号的数次请求作合并处理，再往数据库写，这样就等于降低了数倍的压力。<br>拆分，则是把一个主账号拆分成数个子账号，然后把请求分配到各个子账号上，这样单个账号的压力就小了。然后再用其他手段把子账号的数据合并成主账号数据，返回给用户。</p><h2 id="减少行锁占用时间"><a href="#减少行锁占用时间" class="headerlink" title="减少行锁占用时间"></a>减少行锁占用时间</h2><p>这是个代码层面的优化，前面说了，高频账号之所以会导致系统的性能问题，就是因为要竞争行锁，所以，如果我们能减少每次请求占用行锁的时间，系统性能也就会大幅度提升了。<br>所以，首先，要尽量加快从获取行锁到事务提交这个阶段的运行速度，将不必要的操作，尤其是一些耗时的操作，放到其他地方执行，比如获取行锁之前或者事务以外。<br>然后，尽量避免用</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">select ... for update</span><br></pre></td></tr></table></figure><p> 的方式去获取行锁，二是采用下面这种方式：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">update xxx set amount=amount-<span class="number">1</span> where id=x and amount&gt;=<span class="number">1</span></span><br></pre></td></tr></table></figure><p>如果业务层面允许余额扣减成负数的话，就可以不使用where条件中对金额的校验；否则就需要在将要把余额扣成负数时不去更新数据库，并在程序中返回异常。</p><h2 id="限流"><a href="#限流" class="headerlink" title="限流"></a>限流</h2><p>既然高频账号是单账号的并发达到一定程度后才会导致系统的性能问题，所以我们就可以强制控制这个并发量，使它永远保持在系统可接受的范围内。</p><h2 id="缓存"><a href="#缓存" class="headerlink" title="缓存"></a>缓存</h2><p>缓存也是一个常用的解决高频账号的方法，在缓存中对余额作操作，然后定时向数据库内同步。</p><p>上边介绍了很多方法，当然每一种都会有它的适用条件，也会有其局限性。比如像合并啊，限流啊，实际上都会造成延迟扣费，而在延迟的这个时间段内，可能账户余额已经耗尽，所以如果是严格余额不能为负也不能丢弃记录的业务场景下，其实并不适合用这种方式。<br>所以说，最重要的还是根据业务场景选择合适的方案。</p><p>原文地址: <a href="https://lichuanyang.top/posts/56940/">https://lichuanyang.top/posts/56940/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/56940/</id>
    <link href="https://lichuanyang.top/posts/56940/"/>
    <published>2018-08-25T03:10:28.000Z</published>
    <summary>高并发场景下余额扣减的实战经验，解决用户并发操作同一账户导致的计费系统问题。</summary>
    <title>高并发下作余额扣减的一些经验</title>
    <updated>2026-06-08T08:17:05.772Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <content>
      <![CDATA[<h2 id="问题引入"><a href="#问题引入" class="headerlink" title="问题引入"></a>问题引入</h2><p>今天用findbugs扫代码时遇到一个很有意思的问题，有关三目运算符的，在这儿记录一下。</p><span id="more"></span><p>就是类似这么一行代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">boolean</span> <span class="variable">b</span> <span class="operator">=</span> <span class="literal">true</span>; </span><br><span class="line"><span class="type">Long</span> <span class="variable">a</span> <span class="operator">=</span> b ? <span class="number">0l</span> : Long.valueOf(<span class="number">2</span>); </span><br></pre></td></tr></table></figure><p>Findbugs给出了”Boxed value is unboxed and then immediately reboxed”的提示，意思就是有装箱的对象做了拆箱，然后又马上做了装箱。这个问题其实很常见，一开始也没注意，只是习惯性的把Long.valueOf 改成了Long.parseLong, 确实把这个警告消掉了，不过之后才意识到不对：明明valueOf返回的是Long类型，parseLong返回的是long类型，而需要的正是Long类型，为什么反而用valueOf的时候有问题呢。 </p><p>其实思考一下大概也能想明白，主要就在三元运算符的另一个分支，因为另一个分支返回的是一个未装箱的0,所以这个三元运算符的返回值就成了long,所以原本的Long类型就要经过一次拆箱才会被返回。要优化这个部分的话，保持两个分支的返回类型一致就可以了。 </p><p>然后把相关的细节查了一下，了解清楚。 </p><h2 id="自动装箱-拆箱"><a href="#自动装箱-拆箱" class="headerlink" title="自动装箱&#x2F;拆箱"></a>自动装箱&#x2F;拆箱</h2><p>从JDK1.5开始，java引入了自动装箱和拆箱，不需要做显式转换，提高了我们的开发效率。比如: </p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Double</span> <span class="variable">dWrap1</span> <span class="operator">=</span> <span class="number">10d</span>; </span><br><span class="line"></span><br><span class="line"><span class="type">double</span> <span class="variable">d1</span> <span class="operator">=</span> dWrap1; </span><br><span class="line"></span><br><span class="line"><span class="type">double</span> <span class="variable">d2</span> <span class="operator">=</span> d1 + dWrap1; </span><br><span class="line"></span><br><span class="line">DoubledWarp2 = d2 + dWrap1; </span><br></pre></td></tr></table></figure><p>这么一段代码就是可以正常运行的。 </p><p>另外一个要注意的地方就是，在一个运算，比如前边提到的三元运算符，涉及到类型转换时，编译器会优先选择基本类型，也就是说会优先把已装箱的对象拆箱。 </p><p>我们一开始的问题只是很细微的性能损耗 </p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Long</span> <span class="variable">B</span> <span class="operator">=</span> <span class="literal">null</span>; </span><br><span class="line"><span class="type">Long</span> <span class="variable">A</span> <span class="operator">=</span> (<span class="number">2</span>&gt;<span class="number">1</span>)?B:<span class="number">0l</span>; </span><br></pre></td></tr></table></figure><p>但是像这样的代码的话就是有bug了， 看似只是把一个null赋给Long类型的A, 但是这过程中会做一次向long的拆箱，所以肯定会报空指针。</p><p>所以说，我们在平常的开发中，还是应该尽量避免无意义的装&#x2F;拆箱和类型转换，不光是出于性能考虑，也是为了避免一些诡异的问题。</p><p>原文地址: <a href="https://lichuanyang.top/posts/53072/">https://lichuanyang.top/posts/53072/</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/53072/</id>
    <link href="https://lichuanyang.top/posts/53072/"/>
    <published>2018-05-10T13:22:36.000Z</published>
    <summary>通过 FindBugs 发现的问题，深入分析 Java 三目运算符中的自动拆箱陷阱和装箱开销。</summary>
    <title>java细节:三目运算符和自动拆箱</title>
    <updated>2026-06-08T07:50:49.072Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="开源项目" scheme="https://lichuanyang.top/tags/%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/"/>
    <category term="工具" scheme="https://lichuanyang.top/tags/%E5%B7%A5%E5%85%B7/"/>
    <content>
      <![CDATA[<h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>最近在找房子，因为想找一个去几个地方都相对方便的位置，自己去地图上看还挺麻烦的，所以想做个小工具，用来对北京地铁的路线做规划，本文就简单介绍一下实现过程。目前的功能还比较简单，主体方法就是根据一个输入的始发站，列出其他所有站点到这个地方的站数最少路线。</p><span id="more"></span><h2 id="数据获取"><a href="#数据获取" class="headerlink" title="数据获取"></a>数据获取</h2><p>从网上找了高德地图的接口数据: <a href="http://map.amap.com/service/subway?_1469083453978&srhdata=1100_drw_beijing.json">http://map.amap.com/service/subway?_1469083453978&amp;srhdata=1100_drw_beijing.json</a> . 对拿到的json串进行解析，其中包含每条地铁线路的信息，并依次列出线路上的每个站点。</p><p>通过解析数据，要得到的主要就是各个站点的信息，站点定义的数据结构如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> String id;</span><br><span class="line"><span class="keyword">private</span> String name;</span><br><span class="line"><span class="keyword">private</span> Set&lt;String&gt; lines = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;String&gt;(); <span class="comment">//所在线路</span></span><br><span class="line"><span class="keyword">private</span> String position;</span><br><span class="line"><span class="keyword">private</span> String pinyin;</span><br><span class="line"><span class="keyword">private</span> Set&lt;String&gt; nextStations = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;String&gt;(); <span class="comment">//相邻站点</span></span><br></pre></td></tr></table></figure><p>所有站点汇总后可以看做一张图，nextStations字段就是用来表示边的信息。</p><h2 id="路线规划"><a href="#路线规划" class="headerlink" title="路线规划"></a>路线规划</h2><p>路线规划算法可以参考Dijkstra算法，由于现有的表现形式下，只是一个无向无权重的图，实现起来还要简单一些。</p><p>实现过程描述如下：</p><ol><li>新建两个map, knownPath和waitingPath，分别用来表示已经确定了最短路径的站点列表，和待处理的站点列表。初始化时，knownPath中只包含指定的起点，waitingPath中包含其他所有站点，并将距离设置成无穷大(用MAX &#x3D; 20000表示)</li><li>指定起点为当前节点</li><li>依次处理当前的相邻节点，更新每个节点的最短距离</li><li>从waitingPath中找到距离最短的节点，作为当前节点，重复第3步，并从waitingPath转移到knownPath</li></ol><p>整个过程结束之后，会得到每个节点到起点的最短距离以及路线详情，然后可以根据这些数据计算路线详情的换乘情况。</p><p>对于换乘，主体判断逻辑是，如果一个站点的上一站和下一站所在路线不重合，就可以确定在这一站进行了换乘，比如灯市口-东四-朝阳门，灯市口在5号线上，朝阳门在2，6号线上，可以断定在东四肯定作了换乘。不过还有一些特殊情况要处理，比如说西直门到平安里，中间那站如果是车公庄（虽然现实中没人会这么干），实际就是作了换乘的，但是按刚才的逻辑就会判断成未换乘。</p><p>最终得到的路线信息中包含以下信息：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> String stationId;</span><br><span class="line"><span class="keyword">private</span> <span class="type">int</span> length;</span><br><span class="line"><span class="keyword">private</span> <span class="type">int</span> transferNum; <span class="comment">//换乘数</span></span><br><span class="line"><span class="keyword">private</span> List&lt;String&gt; detail = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;(); <span class="comment">//详细路径</span></span><br></pre></td></tr></table></figure><h2 id="用途"><a href="#用途" class="headerlink" title="用途"></a>用途</h2><p>主体功能在上面已经完成，后边就可以根据需要再去自定义处理了。比如，加入我现在需要找到”距奥林匹克公园站15站以内且距天安门东站7站以内的位置”，就可以分别输入奥林匹克公园和天安门东，然后在结果中作相应的过滤，再取交集。</p><h2 id="后续计划"><a href="#后续计划" class="headerlink" title="后续计划"></a>后续计划</h2><p>目前做的还比较简单，只是简单考虑站数，但是实际上，不同站点的距离、耗时相差会比较大，再一个不同地方的换乘开销差别也很大。所以后续会试试能不能找到换乘站的详情数据，还有两站之间耗时的数据，并应用到算法中。</p><p>再一个是要找一个合适的方法做个界面出来，因为一直做的是纯后台，还没考虑清楚用什么合适。</p><p>其他的，还在考虑爬一下房价信息。</p><h2 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h2><p><a href="https://github.com/lcy362/FoxSubway">https://github.com/lcy362/FoxSubway</a></p><p>欢迎提意见。</p><h2 id="原文地址-https-lichuanyang-top-posts-13793"><a href="#原文地址-https-lichuanyang-top-posts-13793" class="headerlink" title="原文地址: https://lichuanyang.top/posts/13793"></a>原文地址: <a href="https://lichuanyang.top/posts/13793">https://lichuanyang.top/posts/13793</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/13793/</id>
    <link href="https://lichuanyang.top/posts/13793/"/>
    <published>2018-03-23T06:33:52.000Z</published>
    <summary>用 Java 实现北京地铁路线规划工具，从高德地图接口获取数据，计算最少站数路线。</summary>
    <title>设计一个开源的北京地铁路线规划小工具 java版本</title>
    <updated>2026-06-27T03:46:06.798Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="redis系列" scheme="https://lichuanyang.top/categories/redis%E7%B3%BB%E5%88%97/"/>
    <category term="redis" scheme="https://lichuanyang.top/tags/redis/"/>
    <category term="redis-cluster" scheme="https://lichuanyang.top/tags/redis-cluster/"/>
    <content>
      <![CDATA[<p>在之前的一篇文章<a href="https://lichuanyang.top/posts/9329/">通过实际操作理解redis cluster原理</a>中，我们简单介绍过redis cluster的设计原理。redis cluster中的数据是根据一定规则分配到16384个slot中，这些slot又根据配置对应到不同的节点上。我们知道，在集群稳定运行后，仍然可以以slot为单位转移数据，不过对于具体的转移过程，包括转移过程中集群的可用性等问题，一直不是太确定，所以这次详细了解了一下。</p><span id="more"></span><h2 id="整体流程"><a href="#整体流程" class="headerlink" title="整体流程"></a>整体流程</h2><p>redis官方文档中提供的数据迁移办法是借助redis-trib脚本，其实严格来说，这个redis-trib并不是redis本体的一部分，它只是官方按照redis设计规范实现的一套脚本集合，帮助用户更方便的使用redis-cluster。 实际上，我们完全可以脱离这个脚本来使用cluster, 或者用其他方式实现这套逻辑，比如搜狐tv的redis运维工具cachecloud里，就用java实现了整套逻辑。</p><p>我们可以参考redis-trip或者cachecloud的代码来了解cluster数据迁移的流程，主要分为如下几部:</p><ol><li>设定迁移中的节点状态，比如要把slot x的数据从节点A迁移到节点B的话，需要把A设置成MIGRATING状态，B设置成IMPORTING状态。 <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CLUSTER SETSLOT &lt;slot&gt; IMPORTING &lt;node_id&gt;</span><br></pre></td></tr></table></figure> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CLUSTER SETSLOT &lt;slot&gt; MIGRATING &lt;node_id&gt;</span><br></pre></td></tr></table></figure></li><li>迁移数据，这一步首先使用CLUSTER GETKEYSINSLOT 命令获取该slot中所有的key, 然后每个key依次用MIGRATE命令转移数据。</li><li>数据转移完毕之后，正式将slot指派给新的节点B <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CLUSTER SETSLOT &lt;slot&gt; NODE &lt;node_id&gt;</span><br></pre></td></tr></table></figure></li></ol><h2 id="可用性"><a href="#可用性" class="headerlink" title="可用性"></a>可用性</h2><p>在整个迁移中，会出现对于单个key的阻塞情况，原因是MIGRATE命令是原子性的，在单个key的迁移过程中，对这个key的访问会被阻塞。但是，一般来说，一个key的数据不会特别大，所以绝大多数情况下瞬间都能完成，所以一般不会真正影响使用。而其他任何情况都不会造成集群的不可用，如果出现了，比如出现slot级的不可用，说明client端的处理存在某些问题。接下来，本文也会介绍一些client端使用的注意事项。</p><h2 id="ASK和MOVED转向"><a href="#ASK和MOVED转向" class="headerlink" title="ASK和MOVED转向"></a>ASK和MOVED转向</h2><p>前边说了，redis cluster的数据迁移基本不会影响集群使用，但是，在数据从节点A迁移到B的过程中，数据可能在A上，也可能在B上，redis是怎么知道要到哪个节点上去找的呢？这里就要先介绍一下ask和moved这两个转向信号了。顾名思义，出现这个信息就说明需要的数据并不在当前节点上，需要做一次转向处理，其中，MOVED是永久转向信号，ASK则表示只需要这一次操作做转向。</p><p>比如，在节点A向节点B的数据迁移过程中，各个key分散在节点A和节点B中,所以当客户端在A中没找到某个key时,就会得到一个ASK，然后再去B中查找，实际上就是多查一次。</p><p>需要注意的是，客户端查询B时，需要先发一条ASKING命令，否则这个针对带有IMPORTING状态的槽的命令请求将被节点B拒绝执行。</p><p>对于客户端，简单来说就是，收到MOVED时，需要更新slot映射信息，收到ASK时，则需要向新节点发ASKING命令并重新执行操作。</p><p>看了一下jedis代码，也正是按照这个逻辑实现的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">&#125; <span class="keyword">catch</span> (JedisRedirectionException jre) &#123;</span><br><span class="line">      <span class="comment">// if MOVED redirection occurred,</span></span><br><span class="line">      <span class="keyword">if</span> (jre <span class="keyword">instanceof</span> JedisMovedDataException) &#123;</span><br><span class="line">        <span class="comment">// it rebuilds cluster&#x27;s slot cache</span></span><br><span class="line">        <span class="comment">// recommended by Redis cluster specification</span></span><br><span class="line">        <span class="built_in">this</span>.connectionHandler.renewSlotCache(connection);</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// release current connection before recursion or renewing</span></span><br><span class="line">      releaseConnection(connection);</span><br><span class="line">      connection = <span class="literal">null</span>;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (jre <span class="keyword">instanceof</span> JedisAskDataException) &#123;</span><br><span class="line">        asking = <span class="literal">true</span>;</span><br><span class="line">        askConnection.set(<span class="built_in">this</span>.connectionHandler.getConnectionFromNode(jre.getTargetNode()));</span><br><span class="line">      &#125; <span class="keyword">else</span> <span class="keyword">if</span> (jre <span class="keyword">instanceof</span> JedisMovedDataException) &#123;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">JedisClusterOperationException</span>(jre);</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">return</span> runWithRetries(slot, attempts - <span class="number">1</span>, <span class="literal">false</span>, asking);</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br></pre></td></tr></table></figure><p>操作出现异常时，会分别判断MovedException和AskException,然后作相应处理。</p><h2 id="原文地址：https-lichuanyang-top-posts-37583"><a href="#原文地址：https-lichuanyang-top-posts-37583" class="headerlink" title="原文地址：https://lichuanyang.top/posts/37583/"></a>原文地址：<a href="https://lichuanyang.top/posts/37583/">https://lichuanyang.top/posts/37583/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/37583/</id>
    <link href="https://lichuanyang.top/posts/37583/"/>
    <published>2018-02-24T10:34:19.000Z</published>
    <summary>Redis Cluster 数据迁移的详细过程，包括 slot 转移机制和迁移过程中集群可用性分析。</summary>
    <title>redis cluster的数据迁移</title>
    <updated>2026-06-27T03:44:21.599Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="jstorm源码解析" scheme="https://lichuanyang.top/categories/jstorm%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/"/>
    <category term="jstorm" scheme="https://lichuanyang.top/tags/jstorm/"/>
    <content>
      <![CDATA[<h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>AsyncLoopThread是jstorm里自定义的一个循环执行任务的工具，实现不复杂，本来是不值当的专门开一篇文章介绍。不过这个在jstorm里应用实在太广泛了，诸如uspervisor&#x2F;nimbus心跳,获取新topology,更新worker状态等大量功能都是利用AsyncLoopThread实现的，所以还是介绍一下吧，也方便后续看其他部分代码。</p><span id="more"></span><h2 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h2><p>AsyncLoopThread其实完全可以拿出来用在我们自己的工程里，使用比较方便，可以参考 <a href="https://github.com/lcy362/Scenes/blob/4d8ec4ff166060cf5d491c33a96f5c86e8389333/src/main/java/com/mallow/jstormcode/AsyncLoop.java">https://github.com/lcy362/Scenes/blob/4d8ec4ff166060cf5d491c33a96f5c86e8389333/src/main/java/com/mallow/jstormcode/AsyncLoop.java</a></p><p>只需要先定义一个RunnableCallback类，RunnableCallback是jstorm里封装的一个线程类，对Runable接口做了一些增强，可以回调，可以主动关闭。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestThread</span> <span class="keyword">extends</span> <span class="title class_">RunnableCallback</span> &#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;thread runs &quot;</span> + <span class="keyword">new</span> <span class="title class_">Date</span>());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Object <span class="title function_">getResult</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">10</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里我们只实现了两个方法，run和getResult, 其中，run方法就是线程执行方法，和一般的线程一样的，getResult返回的是任务执行间隔时间，单位是秒。</p><p>之后我们只要把这个TestThread作为一个参数传给AsyncLoopThread，就可以实现循环执行TestThread的run方法了。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String args[])</span> &#123;</span><br><span class="line">        <span class="type">AsyncLoopThread</span> <span class="variable">loop</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AsyncLoopThread</span>(<span class="keyword">new</span> <span class="title class_">TestThread</span>());</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            TimeUnit.MINUTES.sleep(<span class="number">5</span>);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="设计"><a href="#设计" class="headerlink" title="设计"></a>设计</h2><p>接下来，我们在具体看一下AsyncLoopThread的实现。</p><p>核心代码就是这个init方法</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">(RunnableCallback afn, <span class="type">boolean</span> daemon, RunnableCallback kill_fn, <span class="type">int</span> priority, <span class="type">boolean</span> start)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (kill_fn == <span class="literal">null</span>) &#123;</span><br><span class="line">        kill_fn = <span class="keyword">new</span> <span class="title class_">AsyncLoopDefaultKill</span>();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="type">Runnable</span> <span class="variable">runnable</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AsyncLoopRunnable</span>(afn, kill_fn);</span><br><span class="line">    thread = <span class="keyword">new</span> <span class="title class_">Thread</span>(runnable);</span><br><span class="line">    <span class="type">String</span> <span class="variable">threadName</span> <span class="operator">=</span> afn.getThreadName();</span><br><span class="line">    <span class="keyword">if</span> (threadName == <span class="literal">null</span>) &#123;</span><br><span class="line">        threadName = afn.getClass().getSimpleName();</span><br><span class="line">    &#125;</span><br><span class="line">    thread.setName(threadName);</span><br><span class="line">    thread.setDaemon(daemon);</span><br><span class="line">    thread.setPriority(priority);</span><br><span class="line">    thread.setUncaughtExceptionHandler(<span class="keyword">new</span> <span class="title class_">UncaughtExceptionHandler</span>() &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">uncaughtException</span><span class="params">(Thread t, Throwable e)</span> &#123;</span><br><span class="line">            LOG.error(<span class="string">&quot;UncaughtException&quot;</span>, e);</span><br><span class="line">            JStormUtils.halt_process(<span class="number">1</span>, <span class="string">&quot;UncaughtException&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    <span class="built_in">this</span>.afn = afn;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (start) &#123;</span><br><span class="line">        thread.start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里主要就是新建了一个AsyncLoopRunnable类，更核心的代码在这里。此外，要注意的一个是kill_fn，在AsyncLoopRunnable里会用到，负责杀掉任务，另外就是会对线程做几个配置，优先级，异常处理之类的，此外这里的线程都被设置成了守护线程，所以上边的例子里，我们需要主动让main线程sleep才能看到运行效果。</p><p>接下来再看一下AsyncLoopRunnable.</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (fn == <span class="literal">null</span>) &#123;</span><br><span class="line">        LOG.error(<span class="string">&quot;fn==null&quot;</span>);</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">&quot;AsyncLoopRunnable no core function &quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    fn.preRun();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">while</span> (!shutdown.get()) &#123;</span><br><span class="line">            fn.run();</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span> (shutdown.get()) &#123;</span><br><span class="line">                shutdown();</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="type">Exception</span> <span class="variable">e</span> <span class="operator">=</span> fn.error();</span><br><span class="line">            <span class="keyword">if</span> (e != <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">throw</span> e;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="type">Object</span> <span class="variable">rtn</span> <span class="operator">=</span> fn.getResult();</span><br><span class="line">            <span class="keyword">if</span> (<span class="built_in">this</span>.needQuit(rtn)) &#123;</span><br><span class="line">                shutdown();</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Throwable e) &#123;</span><br><span class="line">        <span class="keyword">if</span> (shutdown.get()) &#123;</span><br><span class="line">            shutdown();</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            LOG.error(<span class="string">&quot;Async loop died!!!&quot;</span> + e.getMessage(), e);</span><br><span class="line">            killFn.execute(e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个代码也很简单，就是循环的去执行RunnableCallback的run方法，期间会有shutdown,needQuit(),异常几种情况导致任务中断，或者直接杀掉进程。其中,needQuit()方法里会根据前边说的getResult控制任务执行速度。</p><h2 id="原文地址：https-lichuanyang-top-posts-31761"><a href="#原文地址：https-lichuanyang-top-posts-31761" class="headerlink" title="原文地址：https://lichuanyang.top/posts/31761"></a>原文地址：<a href="https://lichuanyang.top/posts/31761">https://lichuanyang.top/posts/31761</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/31761/</id>
    <link href="https://lichuanyang.top/posts/31761/"/>
    <published>2018-02-08T08:17:37.000Z</published>
    <summary>JStorm 自定义循环任务工具 AsyncLoopThread 的源码解析，了解 supervisor/nimbus 心跳等功能的底层实现。</summary>
    <title>jstorm源码解析之循环任务AsyncLoopThread</title>
    <updated>2026-06-27T03:43:25.697Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="activemq系列文章" scheme="https://lichuanyang.top/categories/activemq%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0/"/>
    <category term="activemq" scheme="https://lichuanyang.top/tags/activemq/"/>
    <content>
      <![CDATA[<h2 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h2><p>数据的持久化是很多系统都会涉及到的一个问题，尤其是redis,activemq这些数据主要是存储在内存中的。既然存在内存中，就会面临宕机时数据丢失的风险。这一问题的解决方案就是通过某种方式将数据落到磁盘上，也就是所谓的持久化。</p><p>activemq提供了三种持久化方式，分别基于jdbc, kahadb和leveldb. 目前官方最推荐的是基于kahadb的持久化。<span id="more"></span> jdbc是activemq最早提供的一种持久化方式，但是用数据库去做持久化确实不合适，毕竟性能有瓶颈，而且只是需要简单的读写数据，不需要数据库各种强大的功能。现在去看文档的话，连基本的配置都被埋的很深，所以这种方式我们也就不细说了。</p><p>正是由于基于jdbc的方式存在的种种问题，activemq后来就接连提供了基于kahabd和leveldb的持久化方式.<a href="https://github.com/google/leveldb">leveldb</a> 是google开源的一个KV磁盘存储系统，应用很广，kahadb没找着源头，应该就是activemq团队开发的，也是一种基于磁盘的存储系统。按理说,leveldb的性能是要好一些的，之前无论是activemq的默认配置，还是文档里的推荐使用方法，都是首选leveldb. 但是有一天这个基于leveldb的持久化方式就突然被activemq废弃了，主要原因是leveldb是一个第三方系统，维护起来不如kahadb那么方便。到当前最新版本，leveldb持久化还存在不少严重问题，功能也不如kahadb完善。所以目前来说，最推荐的持久化方式就是kahadb. 接下来介绍一下基本配置。</p><h2 id="基本配置"><a href="#基本配置" class="headerlink" title="基本配置"></a>基本配置</h2><p>基本配置很简单，看默认配置里的就可以，打开activemq.xml。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">&lt;persistenceAdapter&gt;</span><br><span class="line">    &lt;kahaDB directory=&quot;$&#123;activemq.data&#125;/kahadb&quot;/&gt;</span><br><span class="line">&lt;/persistenceAdapter&gt;</span><br></pre></td></tr></table></figure><p>这样数据会自动同步到kahadb目录下。可以去目录下看一下kahadb的文件结构，文件存储的主体是一系列的.log文件，每条需要持久化的数据会一次写入，一个log文件到达一定大小后，会新建一个新的。当一个文件内的所有消息都已经被消费完毕后，这个文件会被删除。</p><h2 id="参数"><a href="#参数" class="headerlink" title="参数"></a>参数</h2><p>参数可以查阅文档：<a href="http://activemq.apache.org/kahadb.html">http://activemq.apache.org/kahadb.html</a>.</p><p>列几个比较有用的：</p><ul><li>cleanupInterval 定期检查哪些文件需要清理的时间间隔,默认30秒</li><li>journalMaxFileLength 每个log文件的最大大小，默认32m</li><li>journalDiskSyncInterval, journalDiskSyncStrategy, journalMaxFileLength: 异步写磁盘相关的一些参数。异步写磁盘能够提升效率，但是会有可能丢失数据</li></ul><h2 id="使用经验"><a href="#使用经验" class="headerlink" title="使用经验"></a>使用经验</h2><p>介绍几点比较有用的经验，一个是日志，可以把kahadb的trace级别日志打开,在log4j里添加org.apache.activemq.store.kahadb.MessageDatabase这个logger的配置就可以，例如</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">log4j.appender.kahadb=org.apache.log4j.RollingFileAppender</span><br><span class="line">log4j.appender.kahadb.file=$&#123;activemq.base&#125;/data/kahadb.log</span><br><span class="line">log4j.appender.kahadb.maxFileSize=1024KB</span><br><span class="line">log4j.appender.kahadb.maxBackupIndex=5</span><br><span class="line">log4j.appender.kahadb.append=true</span><br><span class="line">log4j.appender.kahadb.layout=org.apache.log4j.PatternLayout</span><br><span class="line">log4j.appender.kahadb.layout.ConversionPattern=%d [%-15.15t] %-5p %-30.30c&#123;1&#125; - %m%n</span><br><span class="line">log4j.logger.org.apache.activemq.store.kahadb.MessageDatabase=TRACE, kahadb</span><br></pre></td></tr></table></figure><p>有时候会遇到kahadb文件无法被删除的问题，直接看不一定看得出来是哪些队列的原因，这份日志里就会打印出清理了哪些文件，其他文件因为什么没被清理掉等关键信息。</p><p>另一个是可以为每个队列单独配置存储目录，</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">&lt;persistenceAdapter&gt;</span><br><span class="line">  &lt;mKahaDB directory=&quot;$&#123;activemq.base&#125;/data/kahadb&quot;&gt;</span><br><span class="line">    &lt;filteredPersistenceAdapters&gt;</span><br><span class="line">      &lt;!-- kahaDB per destinations --&gt;</span><br><span class="line">      &lt;filteredKahaDB perDestination=&quot;true&quot;&gt;</span><br><span class="line">        &lt;persistenceAdapter&gt;</span><br><span class="line">          &lt;kahaDB journalMaxFileLength=&quot;32mb&quot;/&gt;</span><br><span class="line">        &lt;/persistenceAdapter&gt;</span><br><span class="line">      &lt;/filteredKahaDB&gt;</span><br><span class="line">    &lt;/filteredPersistenceAdapters&gt;</span><br><span class="line">  &lt;/mKahaDB&gt;</span><br><span class="line"> &lt;/persistenceAdapter&gt;</span><br></pre></td></tr></table></figure><p>这样做的原因还是和存储空间有关，kahadb写文件时是按消息顺序依次写入的，删文件时则要等到这个文件内的所有消息被消费完毕。也就是说，即使这个文件里只有一条消息没被消费掉，也需要占用完整的空间。如果本身队列特别多，恰好有一个队列消费没跟上，可能它本身占用空间非常小，但是会占用大量磁盘空间无法释放。给每个队列分别配置的话就可以大大缓解这一情况。</p><h2 id="原文地址：https-lichuanyang-top-posts-31044"><a href="#原文地址：https-lichuanyang-top-posts-31044" class="headerlink" title="原文地址：https://lichuanyang.top/posts/31044/"></a>原文地址：<a href="https://lichuanyang.top/posts/31044/">https://lichuanyang.top/posts/31044/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/31044/</id>
    <link href="https://lichuanyang.top/posts/31044/"/>
    <published>2018-02-02T10:56:49.000Z</published>
    <summary>深入介绍 ActiveMQ 的三种持久化方式（JDBC、KahaDB、LevelDB），重点讲解官方推荐的 KahaDB 持久化机制。</summary>
    <title>activemq特性之持久化</title>
    <updated>2026-06-27T03:44:21.598Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="activemq系列文章" scheme="https://lichuanyang.top/categories/activemq%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0/"/>
    <category term="activemq" scheme="https://lichuanyang.top/tags/activemq/"/>
    <content>
      <![CDATA[<h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>从<a href="http://activemq.apache.org/download.html">官网</a>下载最新版本，解压，其他的前期准备只需要安装jdk。 从activemq 5.14开始，只支持jdk8。</p><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><p>核心配置文件是conf目录下的activemq.xml, 默认的配置无需修改即可使用,其他配置我们会在后续文章介绍activemq各种特性时详细介绍。</p><h2 id="启动"><a href="#启动" class="headerlink" title="启动"></a>启动</h2><p>bin目录下运行<code> activemq start</code> 即可 ，启动之后浏览器打开localhost:8161 可以打开web管理页面。</p><h2 id="基本api使用"><a href="#基本api使用" class="headerlink" title="基本api使用"></a>基本api使用</h2><p>这里只介绍java下的api使用，activemq默认的openwire协议是只支持java的，因为实现的JMS协议是java下的协议。如果有其他语言的访问需求，可以用stomp,amqp等协议。</p><h3 id="依赖包"><a href="#依赖包" class="headerlink" title="依赖包"></a>依赖包</h3><p>引入activemq官方的client包。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.activemq<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>activemq-client<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>actibemq收发消息的主要步骤类似，都需要先建立连接，然后建立会话，然后再新建需要的producer,consumer. </p><p>示例代码如下</p><h3 id="发送消息"><a href="#发送消息" class="headerlink" title="发送消息"></a>发送消息</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">produce</span><span class="params">()</span> <span class="keyword">throws</span> JMSException &#123;</span><br><span class="line">        <span class="type">ActiveMQConnectionFactory</span> <span class="variable">cf</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ActiveMQConnectionFactory</span>();</span><br><span class="line">        cf.setBrokerURL(<span class="string">&quot;tcp://localhost:61616&quot;</span>);</span><br><span class="line">        <span class="type">Connection</span> <span class="variable">connection</span> <span class="operator">=</span> cf.createConnection();</span><br><span class="line">        connection.start();</span><br><span class="line">        <span class="type">Session</span> <span class="variable">session</span> <span class="operator">=</span> connection.createSession(<span class="literal">false</span>, Session.AUTO_ACKNOWLEDGE);</span><br><span class="line">        <span class="type">Queue</span> <span class="variable">queue</span> <span class="operator">=</span> session.createQueue(<span class="string">&quot;TEST&quot;</span>);</span><br><span class="line">        <span class="type">MessageProducer</span> <span class="variable">producer</span> <span class="operator">=</span> session.createProducer(queue);</span><br><span class="line">        <span class="type">TextMessage</span> <span class="variable">message</span> <span class="operator">=</span> session.createTextMessage(<span class="string">&quot;123&quot;</span>);</span><br><span class="line">        producer.send(message);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h3 id="接收消息"><a href="#接收消息" class="headerlink" title="接收消息"></a>接收消息</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">consume</span><span class="params">()</span> <span class="keyword">throws</span> JMSException &#123;</span><br><span class="line">        <span class="type">ActiveMQConnectionFactory</span> <span class="variable">cf</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ActiveMQConnectionFactory</span>();</span><br><span class="line">        cf.setBrokerURL(<span class="string">&quot;tcp://localhost:61616&quot;</span>);</span><br><span class="line">        <span class="type">Connection</span> <span class="variable">connection</span> <span class="operator">=</span> cf.createConnection();</span><br><span class="line">        connection.start();</span><br><span class="line">        <span class="type">Session</span> <span class="variable">session</span> <span class="operator">=</span> connection.createSession(<span class="literal">false</span>, Session.AUTO_ACKNOWLEDGE);</span><br><span class="line">        <span class="type">Queue</span> <span class="variable">queue</span> <span class="operator">=</span> session.createQueue(<span class="string">&quot;TEST&quot;</span>);</span><br><span class="line">        <span class="type">MessageConsumer</span> <span class="variable">consumer</span> <span class="operator">=</span> session.createConsumer(queue);</span><br><span class="line"></span><br><span class="line"><span class="comment">//        Message message = consumer.receive();</span></span><br><span class="line"><span class="comment">//        TextMessage textMessage = (TextMessage) message;</span></span><br><span class="line"><span class="comment">//        System.out.println(textMessage.getText());</span></span><br><span class="line"></span><br><span class="line">        consumer.setMessageListener(message -&gt; &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;consumer1: &quot;</span> + ((TextMessage) message).getText());</span><br><span class="line">            &#125; <span class="keyword">catch</span> (JMSException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        <span class="type">Session</span> <span class="variable">session2</span> <span class="operator">=</span> connection.createSession(<span class="literal">false</span>, Session.AUTO_ACKNOWLEDGE);</span><br><span class="line">        <span class="type">Queue</span> <span class="variable">queue2</span> <span class="operator">=</span> session2.createQueue(<span class="string">&quot;TEST&quot;</span>);</span><br><span class="line">        <span class="type">MessageConsumer</span> <span class="variable">consumer2</span> <span class="operator">=</span> session2.createConsumer(queue2);</span><br><span class="line">        consumer2.setMessageListener(message -&gt; &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;consumer2: &quot;</span> + ((TextMessage) message).getText());</span><br><span class="line">            &#125; <span class="keyword">catch</span> (JMSException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        <span class="type">Queue</span> <span class="variable">queue3</span> <span class="operator">=</span> session2.createQueue(<span class="string">&quot;TEST&quot;</span>);</span><br><span class="line">        <span class="type">MessageConsumer</span> <span class="variable">consumer3</span> <span class="operator">=</span> session2.createConsumer(queue3);</span><br><span class="line">        consumer3.setMessageListener(message -&gt; &#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                System.out.println(<span class="string">&quot;consumer3: &quot;</span> + ((TextMessage) message).getText());</span><br><span class="line">            &#125; <span class="keyword">catch</span> (JMSException e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            TimeUnit.MINUTES.sleep(<span class="number">100</span>);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>connection,session,consumer都可以创建多个，实现并行消费。一般来说需要使用多connection或者多session,因为只是多consumer的话，共用一个session,只是多个consumer轮流执行而已，不是真正的并行消费。至于connection和session的选择，一般看具体的流量需求。一个connection对应的是一个物理的tcp连接。</p><p>producer其实也类似，只是一般并行发消息意义不大，所以就不贴代码了。</p><h3 id="浏览消息"><a href="#浏览消息" class="headerlink" title="浏览消息"></a>浏览消息</h3><p>activemq提供了一个有用的功能，brower, 作用与consumer类似，只是不会真正将消息消费掉，只是预览消息内容。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">brower</span><span class="params">()</span> <span class="keyword">throws</span> Exception&#123;</span><br><span class="line">        <span class="type">ActiveMQConnectionFactory</span> <span class="variable">cf</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ActiveMQConnectionFactory</span>();</span><br><span class="line">        cf.setBrokerURL(<span class="string">&quot;tcp://localhost:61616&quot;</span>);</span><br><span class="line">        <span class="type">Connection</span> <span class="variable">connection</span> <span class="operator">=</span> cf.createConnection();</span><br><span class="line">        connection.start();</span><br><span class="line">        <span class="type">Session</span> <span class="variable">session</span> <span class="operator">=</span> connection.createSession(<span class="literal">false</span>, Session.AUTO_ACKNOWLEDGE);</span><br><span class="line">        <span class="type">Queue</span> <span class="variable">queue</span> <span class="operator">=</span> session.createQueue(<span class="string">&quot;TEST&quot;</span>);</span><br><span class="line">        <span class="type">QueueBrowser</span> <span class="variable">browser</span> <span class="operator">=</span> session.createBrowser(queue);</span><br><span class="line">        Enumeration&lt;?&gt; enumeration = browser.getEnumeration();</span><br><span class="line">        <span class="keyword">while</span> (enumeration.hasMoreElements()) &#123;</span><br><span class="line">            <span class="type">TextMessage</span> <span class="variable">message</span> <span class="operator">=</span> (TextMessage) enumeration.nextElement();</span><br><span class="line">            System.out.println(<span class="string">&quot;Browsing: &quot;</span> + message.getText());</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h2 id="原文地址：-https-lichuanyang-top-posts-48216"><a href="#原文地址：-https-lichuanyang-top-posts-48216" class="headerlink" title="原文地址： https://lichuanyang.top/posts/48216/"></a>原文地址： <a href="https://lichuanyang.top/posts/48216/">https://lichuanyang.top/posts/48216/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/48216/</id>
    <link href="https://lichuanyang.top/posts/48216/"/>
    <published>2018-01-30T00:46:07.000Z</published>
    <summary>ActiveMQ 的安装部署、核心配置文件说明，以及基本 API 使用教程，适合入门学习。</summary>
    <title>activemq的安装及基本使用</title>
    <updated>2026-06-27T02:31:54.496Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="activemq系列文章" scheme="https://lichuanyang.top/categories/activemq%E7%B3%BB%E5%88%97%E6%96%87%E7%AB%A0/"/>
    <category term="activemq" scheme="https://lichuanyang.top/tags/activemq/"/>
    <content>
      <![CDATA[<p>ActiveMQ 是 Apache 旗下的开源消息中间件（Message-Oriented Middleware，MOM），也是 JMS（Java Message Service）1.1 规范的完整实现。本文是 ActiveMQ 系列文章的开篇，先讲清楚消息中间件是什么、解决什么问题，再介绍 JMS 的核心概念，为后续深入学习 ActiveMQ 的具体特性打好基础。</p><span id="more"></span><h2 id="消息中间件解决什么问题？"><a href="#消息中间件解决什么问题？" class="headerlink" title="消息中间件解决什么问题？"></a>消息中间件解决什么问题？</h2><p>消息中间件是分布式系统中非常常见的组件。它的核心定位是作为应用程序之间的中介——各个应用不直接互相通信，而是通过消息中间件来传递消息。</p><p>两个最核心的价值：<strong>解耦</strong>和<strong>削峰</strong>。</p><h3 id="解耦"><a href="#解耦" class="headerlink" title="解耦"></a>解耦</h3><p>想象一个用户登录的场景：登录成功后，可能会有多个下游系统需要响应——给用户发欢迎消息、更新推荐算法、同步用户数据到数据仓库、触发风控检查……这些操作跟登录本身没有直接关系，如果把逻辑全部写在登录模块里，不仅代码会变成一锅粥，登录的响应时间也会变得不可接受。</p><p>引入消息中间件后，登录模块只需要在验证成功后发一条”用户已登录”的消息，其他系统各自监听这个消息，独立执行自己的业务逻辑。登录模块不再需要知道下游有哪些系统，新增一个消费者也无需修改登录代码。</p><h3 id="削峰"><a href="#削峰" class="headerlink" title="削峰"></a>削峰</h3><p>系统的资源是按日常流量分配的，不可能按秒杀级别的峰值去配置。当瞬时流量远超系统处理能力时，与其让请求直接打到数据库把整个系统拖垮，不如让消息先在中间件里排队，下游系统按照自己的能力慢慢消费。</p><p>举个例子：秒杀场景下，1 秒内涌入 10 万个下单请求，但数据库每秒只能处理 1000 个。用消息中间件作为缓冲，请求先写入消息队列，后台服务按 1000 QPS 匀速处理，保证系统不被冲垮。</p><h2 id="消息传递的两种模式"><a href="#消息传递的两种模式" class="headerlink" title="消息传递的两种模式"></a>消息传递的两种模式</h2><p>消息中间件一般支持两种消息传递模式：</p><h3 id="点对点模式（P2P-Queue）"><a href="#点对点模式（P2P-Queue）" class="headerlink" title="点对点模式（P2P &#x2F; Queue）"></a>点对点模式（P2P &#x2F; Queue）</h3><p>在 JMS 中对应 <strong>Queue（队列）</strong>，有三个特点：</p><ol><li><strong>一条消息一个消费者</strong>：多个消费者可以监听同一个 Queue，但每条消息只会被其中一个消费</li><li><strong>时间解耦</strong>：发送者发消息时，接收者不需要在线。消息会持久化在队列中，等待接收者上线后消费</li><li><strong>需要确认</strong>：消费者成功处理消息后，需要向队列发送应答（ACK），消息才会被标记为已消费</li></ol><p>典型场景：订单处理、异步任务分发。</p><h3 id="发布-订阅模式（Pub-Sub-Topic）"><a href="#发布-订阅模式（Pub-Sub-Topic）" class="headerlink" title="发布-订阅模式（Pub&#x2F;Sub &#x2F; Topic）"></a>发布-订阅模式（Pub&#x2F;Sub &#x2F; Topic）</h3><p>在 JMS 中对应 <strong>Topic（主题）</strong>：</p><ol><li><strong>一条消息多个消费者</strong>：发布者发出消息后，所有订阅了该 Topic 的消费者都会收到</li><li><strong>时间耦合</strong>：订阅者必须保持在线才能收到消息（除非使用持久订阅，Durable Subscription）</li><li><strong>广播特性</strong>：适合一对多通知的场景</li></ol><p>典型场景：配置变更通知、实时推送、事件广播。</p><h3 id="Queue-vs-Topic-对比"><a href="#Queue-vs-Topic-对比" class="headerlink" title="Queue vs Topic 对比"></a>Queue vs Topic 对比</h3><table><thead><tr><th>特性</th><th>Queue</th><th>Topic</th></tr></thead><tbody><tr><td>消费模式</td><td>一条消息一个消费者</td><td>一条消息广播给所有订阅者</td></tr><tr><td>时间依赖</td><td>无（消息持久化）</td><td>有（订阅者需在线，持久订阅除外）</td></tr><tr><td>消息确认</td><td>需要 ACK</td><td>发布即完成</td></tr><tr><td>典型场景</td><td>异步任务、削峰</td><td>事件通知、实时推送</td></tr></tbody></table><h2 id="JMS-是什么？"><a href="#JMS-是什么？" class="headerlink" title="JMS 是什么？"></a>JMS 是什么？</h2><p>JMS（Java Message Service）是 Sun 定义的一套消息传送 API 规范。JMS 本身不是一个具体的消息系统——它只是定义了消息客户端和消息系统之间通信所需的接口和类的抽象，类似于 JDBC 定义数据库访问接口、JNDI 定义命名和目录服务接口。</p><p>JMS 规范主要定义了：</p><ul><li><strong>消息结构</strong>：消息头（Header）、属性（Properties）、消息体（Body）</li><li><strong>连接工厂</strong>：<code>ConnectionFactory</code>，用于创建与消息中间件的连接</li><li><strong>连接与会话</strong>：<code>Connection</code> 和 <code>Session</code>，管理消息的发送和接收</li><li><strong>目的地</strong>：<code>Queue</code> 和 <code>Topic</code></li><li><strong>生产者与消费者</strong>：<code>MessageProducer</code> 和 <code>MessageConsumer</code></li></ul><p>ActiveMQ 就是 JMS 1.1 规范的一个完整实现。</p><h2 id="ActiveMQ-的定位"><a href="#ActiveMQ-的定位" class="headerlink" title="ActiveMQ 的定位"></a>ActiveMQ 的定位</h2><p>在消息中间件的生态中，ActiveMQ 的独特优势在于：</p><ul><li><strong>JMS 兼容性最好</strong>：完整支持 JMS 1.1，是学习 JMS 规范的最佳实践参考</li><li><strong>多协议支持</strong>：支持 OpenWire、AMQP、MQTT、STOMP 等多种传输协议</li><li><strong>灵活的持久化</strong>：支持 JDBC、KahaDB、LevelDB 等多种持久化方式</li><li><strong>丰富的集群方案</strong>：Master-Slave、Network of Brokers 等</li></ul><p>在后续系列文章中，我们会深入介绍 ActiveMQ 的安装配置、持久化机制、集群部署、插件开发等主题。</p><h2 id="系列文章导航"><a href="#系列文章导航" class="headerlink" title="系列文章导航"></a>系列文章导航</h2><blockquote><p>这是 ActiveMQ 系列的第 1 篇。后续文章包括：</p><ul><li><a href="">ActiveMQ 安装与基本配置</a></li><li><a href="">ActiveMQ 持久化机制详解</a></li><li><a href="">ActiveMQ 多线程消费模型</a></li><li><a href="/posts/61645/">ActiveMQ 插件开发指南</a></li><li><a href="/posts/32479/">ActiveMQ Web Console 权限配置</a></li></ul></blockquote><h2 id="欢迎保持关注。"><a href="#欢迎保持关注。" class="headerlink" title="欢迎保持关注。"></a>欢迎保持关注。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/12035/</id>
    <link href="https://lichuanyang.top/posts/12035/"/>
    <published>2018-01-22T10:33:02.000Z</published>
    <summary>ActiveMQ 消息中间件系列开篇，介绍消息中间件的核心作用（解耦和平峰）以及 JMS 协议的基本概念。</summary>
    <title>activemq系列-概述</title>
    <updated>2026-06-27T02:15:49.471Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="hexo" scheme="https://lichuanyang.top/tags/hexo/"/>
    <category term="博客搭建" scheme="https://lichuanyang.top/tags/%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA/"/>
    <content>
      <![CDATA[<p>原文地址： <a href="https://lichuanyang.top/posts/19890/">https://lichuanyang.top/posts/19890/</a></p><p>Hexo是一款开源的博客系统。对于一个后端程序员来说，不想折腾前端的东西，但是csdn,博客园之类的用起来还是不太方便，自己搭博客又麻烦，做出来还丑。偶然间看到了hexo,这个对后端程序员来说可以说是非常友好了。所以也写篇文章记录一下hexo安装,一些关键配置，以及部署到github的过程。</p><h2 id="安装及初始化"><a href="#安装及初始化" class="headerlink" title="安装及初始化"></a>安装及初始化</h2><p>参考<a href="https://hexo.io/docs/index.html">官方文档</a> 就可以了。hexo是基于node.js的，用过node的自然没有任何问题，没用过也没关系，照着说明文档做就可以了。</p><p>hexo支持直接向github的page发布，只需要配置好自己的github信息就可以。</p><h2 id="主题"><a href="#主题" class="headerlink" title="主题"></a>主题</h2><p>hexo有很多定制<a href="https://hexo.io/themes/">主题</a>, 按个人喜好使用吧， 我用的是<a href="https://github.com/theme-next/hexo-theme-next">next</a> , 这款主题功能非常多，统计、搜索之类的都是一条配置都搞定了。不过这款的一些基础配置和其他主题似乎是有些区别的，所以用了以后如果以后想换别的可能会有点困难。</p><h2 id="内容迁移"><a href="#内容迁移" class="headerlink" title="内容迁移"></a>内容迁移</h2><p>hexo提供了多个从其他博客迁移数据的插件，rss,blogger等等都可以。</p><p>以博客园为例，博客园的博客可以导出一个rss文件，然后我们用hexo-migrator-rss就可以生成hexo格式的文件了，不过有可能需要做一些微调。</p><h2 id="插件及第三方服务"><a href="#插件及第三方服务" class="headerlink" title="插件及第三方服务"></a>插件及第三方服务</h2><p>前边说了，hexo配合next主题，很多工具用起来会非常方便。</p><p>主要参考next的<a href="http://theme-next.iissnan.com/third-party-services.html">文档</a>就可以了，不过这份文档有些老了，具体的还要参考next的主题配置文件，里边的说明也比较详细。</p><p>推荐一些比较有用的：</p><ul><li>百度、google等的统计工具: 在next里是把自己的id配上就可以，就不用去加js代码了</li><li>gitment: 基于github issue的评论系统， 用github账号登录以后就可以发评论了。毕竟看技术博客的人github账号大家都有，这样比其他评论系统方便些。</li><li>hexo-generator-searchdb: 一个本地搜索工具，使用之后在博客首页加个搜索框</li><li>hexo-generator-feed: 生成rss文件，以支持订阅</li><li>hexo-abbrlink：默认的文章地址是带文章标题的，特别是中文标题真的是反人类，这个会给每篇文章生成一个id，然后用id做地址</li><li>hexo-generator-robotstxt， hexo-generator-sitemap: 生成sitemap,robots.txt, 帮助搜索引擎爬数据，不多说了</li><li>leancloud: 统计每篇文章访问量，并且在页面上显示</li></ul><p>包括部署到github， 也有现成的配置可以用，简单配置一下就好了。</p><p>弄完之后，就是大家现在看到的样子了。</p><hr><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><h3 id="Step-1-安装Hexo"><a href="#Step-1-安装Hexo" class="headerlink" title="Step 1: 安装Hexo"></a>Step 1: 安装Hexo</h3><p>确保已安装 Node.js，执行 <code>npm install -g hexo-cli</code> 安装 Hexo 命令行工具。然后 <code>hexo init blog</code> 初始化博客项目，<code>cd blog &amp;&amp; npm install</code> 安装依赖。</p><h3 id="Step-2-选择主题"><a href="#Step-2-选择主题" class="headerlink" title="Step 2: 选择主题"></a>Step 2: 选择主题</h3><p>在 Hexo 主题市场中选择喜欢的主题（如 Next），下载到 <code>themes/</code> 目录，在 <code>_config.yml</code> 中修改 <code>theme</code> 字段为主题名称。</p><h3 id="Step-3-配置站点"><a href="#Step-3-配置站点" class="headerlink" title="Step 3: 配置站点"></a>Step 3: 配置站点</h3><p>编辑 <code>_config.yml</code> 配置站点标题、描述、作者等基本信息。根据需要配置第三方服务：统计工具、评论系统、搜索功能等。</p><h3 id="Step-4-部署到GitHub"><a href="#Step-4-部署到GitHub" class="headerlink" title="Step 4: 部署到GitHub"></a>Step 4: 部署到GitHub</h3><p>安装 <code>hexo-deployer-git</code> 插件，在 <code>_config.yml</code> 中配置 <code>deploy</code> 字段填入 GitHub 仓库地址。执行 <code>hexo generate &amp;&amp; hexo deploy</code> 即可部署。</p><h3 id="Step-5-绑定域名"><a href="#Step-5-绑定域名" class="headerlink" title="Step 5: 绑定域名"></a>Step 5: 绑定域名</h3><p>在 <code>source/</code> 目录下创建 CNAME 文件写入自定义域名，在域名 DNS 管理中添加 CNAME 记录指向 GitHub Pages 地址。</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/19890/</id>
    <link href="https://lichuanyang.top/posts/19890/"/>
    <published>2018-01-19T10:56:07.000Z</published>
    <summary>Hexo 博客系统入门教程，从安装初始化到部署 GitHub Pages，适合后端程序员快速搭建个人博客。</summary>
    <title>hexo教程：博客系统搭建及部署到github</title>
    <updated>2026-06-27T02:15:49.475Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="大数据" scheme="https://lichuanyang.top/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <category term="监控" scheme="https://lichuanyang.top/tags/%E7%9B%91%E6%8E%A7/"/>
    <category term="jstorm" scheme="https://lichuanyang.top/tags/jstorm/"/>
    <category term="storm" scheme="https://lichuanyang.top/tags/storm/"/>
    <content>
      <![CDATA[<h2 id="JStorm-Metrics-体系"><a href="#JStorm-Metrics-体系" class="headerlink" title="JStorm Metrics 体系"></a>JStorm Metrics 体系</h2><p>Jstorm的UI中提供了大量非常详细的监控参数，对于我们排查问题帮助非常大，关于UI，可以参考我之前的另一篇文章： <a href="https://lichuanyang.top/posts/31996/">https://lichuanyang.top/posts/31996/</a> 。 不过，UI这种方式用起来有时可能会不太方便，比如需要查历史数据的时候。所以我们希望将监控数据输出到别的存储介质中，方便后续查询、分析。</p><p>由于jstorm的监控相比于apache-storm进行了完全的重写，所以网上查到的storm的监控输出方式并不适用于jstorm. 而jstorm除了官方文档以外实在缺少资料，官方文档又太简略，给的只是一些线索性的东西，具体还要结合这些线索去翻阅源码。所以我整理了一个jstorm监控数据输出的例子。</p><h2 id="自定义-Metrics-上报"><a href="#自定义-Metrics-上报" class="headerlink" title="自定义 Metrics 上报"></a>自定义 Metrics 上报</h2><p>首先需要实现MetricUploader这个接口，不过其实我们并不会实际使用这个接口里的哪个方法，主要是要去用它的TopologyMetricsRunnable这个参数，然后用这个参数去取监控信息。所以理论上只要拿到TopologyMetricsRunnable就行，并不一定非要实现MetricUploader接口。我的做法是实现MetricUploader，然后自己起一个定时的线程池，定时去取监控数据。</p><p>jstorm的metric数据存在rocksdb里，这里取的数据实质上是用jstorm封装好的接口去查询rocksdb。</p><h2 id="核心代码实现"><a href="#核心代码实现" class="headerlink" title="核心代码实现"></a>核心代码实现</h2><p>具体代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ClusterSummary</span> <span class="variable">clusterInfo</span> <span class="operator">=</span> client.getClient().getClusterInfo();</span><br><span class="line"><span class="comment">//get list of topologies in this cluster</span></span><br><span class="line">List&lt;TopologySummary&gt; topologies = clusterInfo.get_topologies();</span><br><span class="line"><span class="keyword">for</span> (TopologySummary topology : topologies) &#123;</span><br><span class="line">    <span class="comment">//get topology id and name</span></span><br><span class="line">    <span class="comment">//the id is used for query, name for human reading</span></span><br><span class="line">    logger.info(<span class="string">&quot;topology info &quot;</span> + topology.get_id() + <span class="string">&quot; &quot;</span> + topology.get_name());</span><br><span class="line">    <span class="type">TopologyMetric</span> <span class="variable">metric</span> <span class="operator">=</span> metricsRunnable.getTopologyMetric(topology.get_id());</span><br><span class="line">    <span class="comment">//get data of &quot;component metrics&quot; page in jstorm UI</span></span><br><span class="line">    <span class="type">MetricInfo</span> <span class="variable">componentMetric</span> <span class="operator">=</span> metric.get_componentMetric();</span><br><span class="line">    Map&lt;String, Map&lt;Integer, MetricSnapshot&gt;&gt; metrics = componentMetric.get_metrics();</span><br><span class="line">    <span class="keyword">for</span> (Map.Entry&lt;String, Map&lt;Integer, MetricSnapshot&gt;&gt; oneMetric : metrics.entrySet()) &#123;</span><br><span class="line">        String[] key = oneMetric.getKey().split(<span class="string">&quot;@&quot;</span>);</span><br><span class="line">        <span class="type">String</span> <span class="variable">metricKey</span> <span class="operator">=</span> key[<span class="number">1</span>] + <span class="string">&quot;@&quot;</span> + key[<span class="number">2</span>] + <span class="string">&quot;@&quot;</span> + key[<span class="number">6</span>];</span><br><span class="line">        <span class="comment">//get(60) to get data in 1 min, also can get(600) for 10min, and so on</span></span><br><span class="line">        logger.info(<span class="string">&quot;metric one minute data for &quot;</span> + metricKey + <span class="string">&quot; &quot;</span> + oneMetric.getValue().get(<span class="number">60</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="数据解析与输出"><a href="#数据解析与输出" class="headerlink" title="数据解析与输出"></a>数据解析与输出</h2><p>整个流程比较清晰，首先需要去查询集群中topology的列表，然后使用每一个topology id去查询metric信息，得到一个TopologyMetric类， TopologyMetric里包含topologyMetric，componentMetric，workerMetric等属性，这个分别与UI页面里对应。</p><p>以componentMetric为例， 可以使用componentMetric.get_metrics(); 拿到具体的监控metric数据, 一个metric是一个Map&lt;String, Map&lt;Integer, MetricSnapshot&gt;&gt;， 其中key是一个@符分隔的字符串，里边包含topology名,component名，数据项等关键的key信息，value里这个map的key是一个时间，单位为秒，对应UI上1分钟，2分钟那几页，value就是具体的监控数据，这个数据其实比UI展示出来的更丰富，除了均值外，还有诸如95线，99线等。</p><p>在这个例子里，我只是用打日志的方式，将部分数据输出。具体用的时候，可以根据需求使用hbase, redis,mysql等存储介质。</p><p>具体代码可以查看 <a href="https://github.com/lcy362/StormTrooper/blob/master/src/main/java/com/trooper/storm/monitor/MetricUploaderTest.java">https://github.com/lcy362/StormTrooper/blob/master/src/main/java/com/trooper/storm/monitor/MetricUploaderTest.java</a></p><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><ol><li><strong>实现 MetricUploader 接口</strong>：创建自定义类实现 <code>MetricUploader</code> 接口，通过构造函数获取 <code>TopologyMetricsRunnable</code> 对象，用它来查询 RocksDB 中的 metrics 数据。</li><li><strong>编写 metrics 采集逻辑</strong>：调用 <code>metricsRunnable.getTopologyMetric(topologyId)</code> 获取 <code>TopologyMetric</code>，分别读取 <code>componentMetric</code>、<code>workerMetric</code>、<code>topologyMetric</code> 等维度的数据，解析 <code>@</code> 分隔的 metric key 并存储。</li><li><strong>配置 JStorm</strong>：在 JStorm 配置文件中将自定义的 MetricUploader 实现类注册进去，确保拓扑启动后自动启用 metrics 上报。</li><li><strong>测试验证</strong>：启动拓扑，检查目标存储介质（HBase&#x2F;Redis&#x2F;MySQL 等）中是否有 metrics 数据写入，对照 JStorm UI 确认数据一致性。</li><li><strong>对接 Grafana</strong>：将存储介质配置为 Grafana 数据源，编写查询语句创建监控面板，实现历史数据的可视化查询和告警。</li></ol>]]>
    </content>
    <id>https://lichuanyang.top/posts/13749/</id>
    <link href="https://lichuanyang.top/posts/13749/"/>
    <published>2017-09-06T12:18:00.000Z</published>
    <summary>将 JStorm 监控 metrics 数据输出到第三方存储介质的方案，方便历史数据查询和分析。</summary>
    <title>jstorm的监控metrics数据输出到第三方存储介质</title>
    <updated>2026-06-27T02:31:54.493Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="消息队列" scheme="https://lichuanyang.top/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    <category term="camel" scheme="https://lichuanyang.top/tags/camel/"/>
    <content>
      <![CDATA[<p>Apache Camel 是一个强大的企业集成框架，基于 EIP（Enterprise Integration Patterns）实现了 200 多种组件，可以轻松连接各种数据源和中间件。但它的强大也带来一个问题：高度封装让底层细节变得不透明，一旦路由出问题，排查起来非常困难。</p><p>好在 Camel 官方提供了一个 Debugger 工具，可以像调试普通 Java 程序一样在路由节点上打断点、查看消息内容。本文在<a href="http://camel.apache.org/debugger.html">官方文档</a>的基础上，补充一些实用的调试技巧。</p><span id="more"></span><h2 id="为什么要用-Debugger？"><a href="#为什么要用-Debugger？" class="headerlink" title="为什么要用 Debugger？"></a>为什么要用 Debugger？</h2><p>Camel 的路由定义通常非常声明式，比如：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">from(<span class="string">&quot;file:inbox&quot;</span>)</span><br><span class="line">    .unmarshal().json()</span><br><span class="line">    .bean(SomeTransformer.class)</span><br><span class="line">    .to(<span class="string">&quot;activemq:queue:orders&quot;</span>);</span><br></pre></td></tr></table></figure><p>看起来清晰简洁，但如果消息在某个环节被处理错了，你怎么知道是 <code>unmarshal</code> 出了问题还是 <code>SomeTransformer</code> 的 bug？这个时候 Debugger 就派上用场了。</p><h2 id="快速上手"><a href="#快速上手" class="headerlink" title="快速上手"></a>快速上手</h2><h3 id="1-引入依赖"><a href="#1-引入依赖" class="headerlink" title="1. 引入依赖"></a>1. 引入依赖</h3><p>首先引入 <code>camel-test</code> 包：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.camel<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>camel-test<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.16.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><h3 id="2-创建-Debugger-类"><a href="#2-创建-Debugger-类" class="headerlink" title="2. 创建 Debugger 类"></a>2. 创建 Debugger 类</h3><p>继承 <code>CamelTestSupport</code>，这是 Camel 提供的一个方便的测试基类：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CamelDebugger</span> <span class="keyword">extends</span> <span class="title class_">CamelTestSupport</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> RoutesBuilder <span class="title function_">createRouteBuilder</span><span class="params">()</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">RouteBuilder</span>() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">configure</span><span class="params">()</span> &#123;</span><br><span class="line">                from(<span class="string">&quot;direct:start&quot;</span>)</span><br><span class="line">                    .log(<span class="string">&quot;Received: $&#123;body&#125;&quot;</span>)</span><br><span class="line">                    .bean(SomeTransformer.class)</span><br><span class="line">                    .to(<span class="string">&quot;mock:result&quot;</span>);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">debugBefore</span><span class="params">(Exchange exchange, Processor processor, </span></span><br><span class="line"><span class="params">                                 ProcessorDefinition&lt;?&gt; definition, String id, String label)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;Before &#123;&#125;: body=&#123;&#125;, headers=&#123;&#125;&quot;</span>, </span><br><span class="line">            id, exchange.getIn().getBody(), exchange.getIn().getHeaders());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">debugAfter</span><span class="params">(Exchange exchange, Processor processor, </span></span><br><span class="line"><span class="params">                                ProcessorDefinition&lt;?&gt; definition, String id, String label, <span class="type">long</span> timeTaken)</span> &#123;</span><br><span class="line">        log.info(<span class="string">&quot;After &#123;&#125;: body=&#123;&#125;, took &#123;&#125;ms&quot;</span>, </span><br><span class="line">            id, exchange.getIn().getBody(), timeTaken);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="关键方法详解"><a href="#关键方法详解" class="headerlink" title="关键方法详解"></a>关键方法详解</h2><p><code>CamelTestSupport</code> 中与调试相关的几个关键方法：</p><h3 id="createCamelContext"><a href="#createCamelContext" class="headerlink" title="createCamelContext()"></a><code>createCamelContext()</code></h3><p>自定义 CamelContext，可以配置组件、注册 bean 等。适用于需要精确控制测试环境的场景。</p><h3 id="createRouteBuilder"><a href="#createRouteBuilder" class="headerlink" title="createRouteBuilder()"></a><code>createRouteBuilder()</code></h3><p>使用默认 CamelContext，只添加自己的路由。这是最简单的方式，适合大多数调试场景。</p><h3 id="debugBefore-和-debugAfter"><a href="#debugBefore-和-debugAfter" class="headerlink" title="debugBefore() 和 debugAfter()"></a><code>debugBefore()</code> 和 <code>debugAfter()</code></h3><p>这两个方法是 Debugger 的核心：</p><ul><li><p><strong><code>debugBefore</code></strong>：在消息被每个 processor 处理<strong>之前</strong>调用。参数包括当前的 Exchange（含消息体、消息头、属性）、即将执行的 Processor、路由节点 ID 和 label。适合用来<strong>确认”进入这个节点时消息是什么状态”</strong>。</p></li><li><p><strong><code>debugAfter</code></strong>：在消息被处理<strong>之后</strong>调用。多了 <code>timeTaken</code> 参数，表示这个 processor 的执行耗时。适合用来<strong>对比处理前后的变化</strong>，以及<strong>定位性能瓶颈</strong>。</p></li></ul><h2 id="实战示例：排查消息丢失"><a href="#实战示例：排查消息丢失" class="headerlink" title="实战示例：排查消息丢失"></a>实战示例：排查消息丢失</h2><p>假设你的路由中有一条消息在某个节点之后消失了，你可以这样 debug：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">debugAfter</span><span class="params">(Exchange exchange, Processor processor,</span></span><br><span class="line"><span class="params">                            ProcessorDefinition&lt;?&gt; definition, String id, String label, <span class="type">long</span> timeTaken)</span> &#123;</span><br><span class="line">    <span class="type">Object</span> <span class="variable">body</span> <span class="operator">=</span> exchange.getIn().getBody();</span><br><span class="line">    <span class="keyword">if</span> (body == <span class="literal">null</span>) &#123;</span><br><span class="line">        log.error(<span class="string">&quot;⚠️ Message body is null after processor: &#123;&#125; (id=&#123;&#125;)&quot;</span>, label, id);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 记录每个节点的耗时，超过 1 秒的告警</span></span><br><span class="line">    <span class="keyword">if</span> (timeTaken &gt; <span class="number">1000</span>) &#123;</span><br><span class="line">        log.warn(<span class="string">&quot;🐢 Slow processor: &#123;&#125; took &#123;&#125;ms&quot;</span>, label, timeTaken);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="替代方案"><a href="#替代方案" class="headerlink" title="替代方案"></a>替代方案</h2><p>Debugger 不是唯一的选择，Camel 还提供了其他排查手段：</p><table><thead><tr><th>方式</th><th>适用场景</th><th>优劣</th></tr></thead><tbody><tr><td><strong>Debugger</strong></td><td>本地开发调试</td><td>信息最全面，但需要启动测试</td></tr><tr><td><strong>Tracer Interceptor</strong></td><td>生产环境排查</td><td>可以全局开启，但对性能有影响</td></tr><tr><td><strong>Log EIP（<code>log:</code> 组件）</strong></td><td>路由中插入临时日志</td><td>最简单，但需要改路由代码</td></tr><tr><td><strong>Hawtio 管理控制台</strong></td><td>可视化监控</td><td>可以看到运行状态，但调试粒度较粗</td></tr></tbody></table><h2 id="完整代码"><a href="#完整代码" class="headerlink" title="完整代码"></a>完整代码</h2><p>完整的可运行示例在这里：<br><a href="https://github.com/lcy362/CamelDemo/blob/7aef2cc7661236499896022f6976c160b73b68e7/src/main/java/com/mallow/demo/camel/debugger/CamelDebugger.java">https://github.com/lcy362/CamelDemo</a></p><blockquote><p>这是 Camel 系列文章之一。如果你在使用 Camel 做数据路由或系统集成，欢迎关注后续文章，或者<a href="https://github.com/lcy362/CamelDemo">到 GitHub 上交流</a>。</p></blockquote><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-什么时候用-Debugger-而不是-Tracer？"><a href="#Q-什么时候用-Debugger-而不是-Tracer？" class="headerlink" title="Q: 什么时候用 Debugger 而不是 Tracer？"></a>Q: 什么时候用 Debugger 而不是 Tracer？</h3><p>Debugger 适合<strong>本地开发调试</strong>，信息最全面，可以逐节点打断点、查看消息体、对比处理前后的变化。Tracer 适合<strong>线上生产环境排查</strong>，全局开启后可追踪每一条消息经过的完整路由路径，但会对性能有一定影响。简单选择就是：开发环境用 Debugger 精确定位问题，生产环境用 Tracer 快速了解消息流向。</p><h3 id="Q-Camel-调试有哪些替代工具？"><a href="#Q-Camel-调试有哪些替代工具？" class="headerlink" title="Q: Camel 调试有哪些替代工具？"></a>Q: Camel 调试有哪些替代工具？</h3><p>除了 Debugger，Camel 还提供了 Tracer Interceptor（全局消息追踪）、Log EIP（在路由中插入临时日志）、以及 Hawtio 管理控制台（可视化监控）。此外，也可以在路由中临时添加 <code>log:</code> 或 <code>wireTap:</code> 组件来做即时排查。</p><h3 id="Q-Debugger-会影响性能吗？"><a href="#Q-Debugger-会影响性能吗？" class="headerlink" title="Q: Debugger 会影响性能吗？"></a>Q: Debugger 会影响性能吗？</h3><p>会有影响。Debugger 在每个 processor 执行前后都会触发回调，如果回调中做了大量日志输出或序列化操作，路由吞吐量会明显下降。因此 Debugger 设计上只用于本地开发和测试，不建议在生产环境开启。</p><h3 id="Q-Debugger-只能用在测试环境吗？"><a href="#Q-Debugger-只能用在测试环境吗？" class="headerlink" title="Q: Debugger 只能用在测试环境吗？"></a>Q: Debugger 只能用在测试环境吗？</h3><p>严格来说是的。Camel 的 Debugger 依托于 <code>CamelTestSupport</code> 基类，本身就设计为测试工具。如果需要在运行中的路由上做调试，应使用 Tracer、Hawtio 控制台或 JMX 监控等替代方案。</p><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><ol><li><strong>引入依赖</strong>：在 <code>pom.xml</code> 中添加 <code>camel-test</code> 依赖，版本与项目中的 Camel 版本保持一致。</li><li><strong>创建 Debugger 类</strong>：继承 <code>CamelTestSupport</code>，重写 <code>createRouteBuilder()</code> 定义待调试路由，重写 <code>debugBefore()</code> 和 <code>debugAfter()</code> 打印消息内容和处理耗时。</li><li><strong>配置调试路由</strong>：在 <code>createRouteBuilder</code> 中通过 <code>from().to()</code> 或 <code>from().bean().to()</code> 等方式定义完整的路由链路。</li><li><strong>运行测试</strong>：直接运行 Debugger 类，观察控制台输出，通过 <code>debugBefore</code> 确认消息进入状态，通过 <code>debugAfter</code> 对比处理变化和定位慢节点。</li></ol>]]>
    </content>
    <id>https://lichuanyang.top/posts/1120/</id>
    <link href="https://lichuanyang.top/posts/1120/"/>
    <published>2017-08-03T11:56:00.000Z</published>
    <summary>Apache Camel 调试工具 debugger 的使用指南，帮助在 Camel 高度封装的数据路由框架中高效排查问题。</summary>
    <title>camel系列之camel debugger的使用</title>
    <updated>2026-06-27T02:31:54.478Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="jstorm源码解析" scheme="https://lichuanyang.top/categories/jstorm%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/"/>
    <category term="jstorm" scheme="https://lichuanyang.top/tags/jstorm/"/>
    <category term="源码阅读" scheme="https://lichuanyang.top/tags/%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/"/>
    <content>
      <![CDATA[<h2 id="问题"><a href="#问题" class="headerlink" title="问题"></a>问题</h2><p>用过storm或者jstorm的都知道，如果在bolt代码中发生了没被catch住的异常，所在worker进程会退出。本文就从源码角度分析一下具体设计，其实并不是“有异常然后进程崩了”这么简单。</p><span id="more"></span><h2 id="实质"><a href="#实质" class="headerlink" title="实质"></a>实质</h2><p>我们先看BasicBoltExecutor的源码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">execute</span><span class="params">(Tuple input)</span> &#123;</span><br><span class="line">    _collector.setContext(input);</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        _bolt.execute(input, _collector);</span><br><span class="line">        _collector.getOutputter().ack(input);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (FailedException e) &#123;</span><br><span class="line">        <span class="keyword">if</span> (e <span class="keyword">instanceof</span> ReportedFailedException) &#123;</span><br><span class="line">            _collector.reportError(e);</span><br><span class="line">        &#125;</span><br><span class="line">        _collector.getOutputter().fail(input);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>_bolt.execute(input, _collector) 就是执行我们自己编写的bolt里的excute方法。可以看到，在这里，只会catch storm自己定义的FailedException，并且发送fail消息，标记tuple处理失败， 其余异常则会被放过。</p><p>再外层是BoltExecutors的processTupleEvent方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">try</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (!isSystemBolt &amp;&amp; tuple.getSourceStreamId().equals(Common.TOPOLOGY_MASTER_CONTROL_STREAM_ID)) &#123;</span><br><span class="line">        backpressureTrigger.handle(tuple);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        bolt.execute(tuple);</span><br><span class="line">    &#125;</span><br><span class="line">&#125; <span class="keyword">catch</span> (Throwable e) &#123;</span><br><span class="line">    error = e;</span><br><span class="line">    LOG.error(<span class="string">&quot;bolt execute error &quot;</span>, e);</span><br><span class="line">    report_error.report(e);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这里，所有异常都会被catch住，但是只会进行report_error，并不会发fail消息，相关tuple只能等超时才能被标记为失败。</p><p>再来看report_error.report(e) 的具体实现，通过看构造函数，可以看到report_error是一个TaskReportErrorAndDie类，</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">report</span><span class="params">(Throwable error)</span> &#123;</span><br><span class="line">    <span class="built_in">this</span>.reporterror.report(error);</span><br><span class="line">    <span class="built_in">this</span>.haltfn.run();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这里，reporterror是一个AsyncLoopDefaultKill类</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> &#123;</span><br><span class="line">    JStormUtils.halt_process(<span class="number">1</span>, <span class="string">&quot;Async loop died!&quot;</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里就是整个过程的最终步骤了， JStormUtils.halt_process()方法会打印一条”Async loop died!”的日志后将worker进程杀死。</p><h2 id="思考"><a href="#思考" class="headerlink" title="思考"></a>思考</h2><p>通过代码可以出来，对于jstorm，“异常后worker退出”是一个故意设计出的特性，并非程序不健壮。猜测这一块的设计理念就是对于已知异常，开发人员自己捕获并重新抛出FailedException，使相应消息失败；未知异常则强制使进程直接失败退出，避免过度的catch导致问题被掩盖。</p><p>不过虽然话是这么说，对这个设计还是持保留意见，毕竟storm和普通的java程序不一样，storm的worker进程在退出后是会自动被重启的，所以这种异常处理方式并不能起到failfast的效果。</p><p>相反，worker的持续重启，还会带来一些其他问题。再一个，不主动将消息标为失败，而是等超时，如果设置的超时时间过长(当然超时时间太长也不合理)，也会引入一些问题。比如说kafkaSpout, 一条消息没被ack之前是不会继续取后边的数据的，这样如果有一条数据需要等超时，同分区下的数据在这一个超时周期内，就都无法被处理了。</p><p>从另一方面来说，如果像FailedException一样处理其他所有异常，由于异常之后可以看到有数据fail,也并不会掩盖问题。</p><p>所以说，这一块的处理逻辑，个人感觉还是需要斟酌一下。—</p>]]>
    </content>
    <id>https://lichuanyang.top/posts/15594/</id>
    <link href="https://lichuanyang.top/posts/15594/"/>
    <published>2017-08-03T11:29:00.000Z</published>
    <summary>从源码角度分析 JStorm bolt 中未捕获异常导致 worker 进程退出的机制，揭示背后的完整设计。</summary>
    <title>jstorm源码解析之bolt异常处理</title>
    <updated>2026-06-27T02:31:54.502Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="日志" scheme="https://lichuanyang.top/tags/%E6%97%A5%E5%BF%97/"/>
    <content>
      <![CDATA[<p>大多数 Java 开发者习惯通过配置文件（<code>log4j.properties</code> 或 <code>log4j.xml</code>）来管理日志输出。但在某些运维场景下，运行时动态修改日志配置会更加灵活——比如线上排查问题时临时开 DEBUG 级别，或者动态将日志推送到 Kafka 做集中分析。</p><p>Log4j 1.x 的 API 完全支持运行时代码修改配置。本文以 KafkaAppender 为例，演示如何动态添加 appender。</p><span id="more"></span><h2 id="静态配置-vs-动态配置"><a href="#静态配置-vs-动态配置" class="headerlink" title="静态配置 vs 动态配置"></a>静态配置 vs 动态配置</h2><table><thead><tr><th>方式</th><th>适用场景</th><th>优缺点</th></tr></thead><tbody><tr><td><strong>配置文件</strong>（静态）</td><td>日常开发、常规部署</td><td>简单、标准化；但修改需要重启</td></tr><tr><td><strong>API 编程</strong>（动态）</td><td>线上排查、运维自动化</td><td>灵活，不需要重启；但需要对 API 熟悉</td></tr></tbody></table><h2 id="基本示例：动态添加-KafkaAppender"><a href="#基本示例：动态添加-KafkaAppender" class="headerlink" title="基本示例：动态添加 KafkaAppender"></a>基本示例：动态添加 KafkaAppender</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.apache.log4j.Logger;</span><br><span class="line"><span class="keyword">import</span> org.apache.log4j.PatternLayout;</span><br><span class="line"><span class="keyword">import</span> org.apache.log4j.Level;</span><br><span class="line"><span class="keyword">import</span> org.apache.log4j.net.SocketAppender; <span class="comment">// 或其他</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 一个工具方法示例</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Log4jConfigurator</span> &#123;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">addKafkaAppender</span><span class="params">(String loggerName, String broker, </span></span><br><span class="line"><span class="params">                                          String topic, String layout)</span> &#123;</span><br><span class="line">        <span class="type">Logger</span> <span class="variable">logger</span> <span class="operator">=</span> Logger.getLogger(loggerName);</span><br><span class="line">        </span><br><span class="line">        <span class="type">KafkaLog4jAppender</span> <span class="variable">kafkaAppender</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">KafkaLog4jAppender</span>();</span><br><span class="line">        kafkaAppender.setBrokerList(broker);</span><br><span class="line">        kafkaAppender.setTopic(topic);</span><br><span class="line">        kafkaAppender.setCompressionType(<span class="string">&quot;gzip&quot;</span>);</span><br><span class="line">        kafkaAppender.setSyncSend(<span class="literal">false</span>);</span><br><span class="line">        kafkaAppender.setLayout(<span class="keyword">new</span> <span class="title class_">PatternLayout</span>(layout));</span><br><span class="line">        kafkaAppender.activateOptions();</span><br><span class="line">        </span><br><span class="line">        logger.addAppender(kafkaAppender);</span><br><span class="line">        logger.setLevel(Level.INFO);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>关键步骤</strong>：</p><ol><li><strong>创建 Appender 实例</strong>，设置目标地址、格式等参数</li><li><strong>调用 <code>activateOptions()</code></strong>——激活配置，很多 Appender 在这一步初始化连接</li><li><strong><code>logger.addAppender()</code></strong> 添加到目标 Logger</li><li><strong>设置日志级别</strong>——只有等于或高于该级别的日志才会被 Appender 处理</li></ol><h2 id="三个实战场景"><a href="#三个实战场景" class="headerlink" title="三个实战场景"></a>三个实战场景</h2><h3 id="场景一：线上临时开-DEBUG"><a href="#场景一：线上临时开-DEBUG" class="headerlink" title="场景一：线上临时开 DEBUG"></a>场景一：线上临时开 DEBUG</h3><p>线上出问题时，需要看到更详细的日志，但全局改 INFO 为 DEBUG 会产生海量日志。可以只对特定 Logger 动态调整：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 只对 com.example.payment 这个包临时开 DEBUG</span></span><br><span class="line"><span class="type">Logger</span> <span class="variable">paymentLogger</span> <span class="operator">=</span> Logger.getLogger(<span class="string">&quot;com.example.payment&quot;</span>);</span><br><span class="line">paymentLogger.setLevel(Level.DEBUG);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 排查完毕后恢复</span></span><br><span class="line">paymentLogger.setLevel(Level.INFO);</span><br></pre></td></tr></table></figure><p>配合一个 HTTP 端点暴露这个能力，就可以做到”浏览器点一下，DEBUG 就开了”。</p><h3 id="场景二：动态路由日志到-Kafka"><a href="#场景二：动态路由日志到-Kafka" class="headerlink" title="场景二：动态路由日志到 Kafka"></a>场景二：动态路由日志到 Kafka</h3><p>这是上面的主示例。适合需要实时日志分析的场景——把应用日志通过 Kafka 推到 ELK 或 Flink 等分析平台。</p><h3 id="场景三：动态切换日志文件"><a href="#场景三：动态切换日志文件" class="headerlink" title="场景三：动态切换日志文件"></a>场景三：动态切换日志文件</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 从当前文件切换到新的滚动文件</span></span><br><span class="line"><span class="type">Logger</span> <span class="variable">rootLogger</span> <span class="operator">=</span> Logger.getRootLogger();</span><br><span class="line">rootLogger.removeAllAppenders();</span><br><span class="line"></span><br><span class="line"><span class="type">DailyRollingFileAppender</span> <span class="variable">newAppender</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DailyRollingFileAppender</span>();</span><br><span class="line">newAppender.setFile(<span class="string">&quot;/var/log/myapp/app.log&quot;</span>);</span><br><span class="line">newAppender.setDatePattern(<span class="string">&quot;&#x27;.&#x27;yyyy-MM-dd&quot;</span>);</span><br><span class="line">newAppender.setLayout(<span class="keyword">new</span> <span class="title class_">PatternLayout</span>(<span class="string">&quot;%d [%t] %-5p %c - %m%n&quot;</span>));</span><br><span class="line">newAppender.activateOptions();</span><br><span class="line"></span><br><span class="line">rootLogger.addAppender(newAppender);</span><br></pre></td></tr></table></figure><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><ul><li><strong>线程安全</strong>：Log4j 1.x 不是完全线程安全的。在多线程环境下动态修改配置，建议加同步锁或通过 JMX 管理</li><li><strong>内存泄漏</strong>：动态添加的 Appender 记得在不需要时 <code>removeAppender()</code> 释放，特别是带有网络连接的 Appender（Kafka&#x2F;Socket）</li><li><strong>Log4j 2 更友好</strong>：Log4j 2 提供了原生的 <code>Configurator</code> API，线程安全且支持异步 Logger，是新项目更好的选择</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Log4j 2 的动态配置方式（更现代）</span></span><br><span class="line">Configurator.setLevel(<span class="string">&quot;com.example&quot;</span>, Level.DEBUG);</span><br></pre></td></tr></table></figure><p>如果你还在用 Log4j 1.x，上面的动态 API 能帮你应对很多运维场景。如果是新项目，建议直接上 Log4j 2 或 SLF4J + Logback。</p><hr><h2 id="快速上手步骤"><a href="#快速上手步骤" class="headerlink" title="快速上手步骤"></a>快速上手步骤</h2><h3 id="Step-1-识别需求"><a href="#Step-1-识别需求" class="headerlink" title="Step 1: 识别需求"></a>Step 1: 识别需求</h3><p>明确需要动态添加 Appender 的场景：是临时开启 DEBUG 日志、将日志推送至 Kafka，还是动态切换日志文件。确定目标 Logger 名称和所需的 Appender 类型。</p><h3 id="Step-2-创建Appender实例"><a href="#Step-2-创建Appender实例" class="headerlink" title="Step 2: 创建Appender实例"></a>Step 2: 创建Appender实例</h3><p>根据需求创建对应的 Appender 实例，如 <code>KafkaLog4jAppender</code>、<code>DailyRollingFileAppender</code> 等。使用 <code>Logger.getLogger(name)</code> 获取目标 Logger 对象。</p><h3 id="Step-3-配置参数"><a href="#Step-3-配置参数" class="headerlink" title="Step 3: 配置参数"></a>Step 3: 配置参数</h3><p>设置 Appender 的必要参数，如 Kafka 的 Broker 地址和 Topic、文件路径和滚动策略、日志格式 <code>PatternLayout</code> 等。</p><h3 id="Step-4-激活并添加到Logger"><a href="#Step-4-激活并添加到Logger" class="headerlink" title="Step 4: 激活并添加到Logger"></a>Step 4: 激活并添加到Logger</h3><p>调用 <code>appender.activateOptions()</code> 激活配置（建立连接等初始化操作），然后通过 <code>logger.addAppender(appender)</code> 将 Appender 添加到目标 Logger，必要时调整日志级别。</p><h3 id="Step-5-验证日志输出"><a href="#Step-5-验证日志输出" class="headerlink" title="Step 5: 验证日志输出"></a>Step 5: 验证日志输出</h3><p>触发业务逻辑产生日志，确认日志已按预期输出到目标位置（Kafka 消费者能收到消息、文件中有新日志写入等）。排查完毕后记得调用 <code>removeAppender</code> 释放资源。</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/42764/</id>
    <link href="https://lichuanyang.top/posts/42764/"/>
    <published>2017-06-30T11:30:00.000Z</published>
    <summary>通过代码动态修改 Log4j 配置的实战示例，以 KafkaAppender 为例演示运行时添加 appender。</summary>
    <title>log4j动态添加appender</title>
    <updated>2026-06-27T03:43:25.703Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <category term="测试" scheme="https://lichuanyang.top/tags/%E6%B5%8B%E8%AF%95/"/>
    <content>
      <![CDATA[<h2 id="为什么需要基准测试"><a href="#为什么需要基准测试" class="headerlink" title="为什么需要基准测试"></a>为什么需要基准测试</h2><p>有时候我们需要写一些简单的性能测试代码，恰好在stackoverflow上看到一篇经验之谈，<a href="https://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java%EF%BC%8C">https://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java，</a> 怎样写基准测试来尽量屏蔽掉环境的影响。</p><p>翻译出来贴在这儿：</p><h2 id="编写可靠的基准测试"><a href="#编写可靠的基准测试" class="headerlink" title="编写可靠的基准测试"></a>编写可靠的基准测试</h2><p>来自Java HotSpot作者的撰写微基准的提示：</p><p>规则0：阅读有关JVM和微型基准测试的好论文。比如<a href="https://www.ibm.com/developerworks/java/library/j-jtp02225/%E3%80%82%E4%B8%8D%E8%A6%81%E5%AF%B9%E8%BF%99%E7%A7%8D%E6%B5%8B%E8%AF%95%E6%9C%89%E5%A4%AA%E9%AB%98%E7%9A%84%E6%9C%9F%E6%9C%9B;%E5%AE%83%E4%BB%AC%E5%AF%B9JVM%E6%80%A7%E8%83%BD%E7%9A%84%E6%B5%8B%E8%AF%95%E4%BB%85%E8%83%BD%E8%B5%B7%E5%88%B0%E6%9C%89%E9%99%90%E6%95%88%E6%9E%9C%E3%80%82">https://www.ibm.com/developerworks/java/library/j-jtp02225/。不要对这种测试有太高的期望;它们对JVM性能的测试仅能起到有限效果。</a></p><p>规则1：始终包含一个预热阶段，它一直运行，直到触发所有的初始化和编译。 （预热阶段的迭代次数可以减少，经验法则是几万次循环。）</p><p>规则2：始终使用-XX:+PrintCompilation -verbose:gc 等参数来运行，这样可以确定编译阶段和JVM的其他部分在计时时是否进行了一些意外的工作。</p><p>规则2.1：在计时和预热阶段的开始和结束打印消息，这样可以确定计时时是否有规则2的输出。</p><p>规则3：了解-client与-server之间的区别，还有OSR和常规汇编之间的区别。 -server优于-client, 常规编译优于OSR</p><h2 id="常见陷阱"><a href="#常见陷阱" class="headerlink" title="常见陷阱"></a>常见陷阱</h2><p>规则4：注意初始化的影响，第一次计时不要打印结果，除非是在测试类加载的过程，规则2是你对抗这种效果的第一道防线。</p><p>规则5：注意编译器优化和重新编译的效果。计时时不要使用任何代码路径，因为编译器可能会基于一些乐观假设进行优化，导致根本不会使用该路径，从而可以对代码进行垃圾和重新编译。规则2是你对抗这种效果的第一道防线。</p><p>规则6：使用适当的工具读取编译器的工作过程，并对它产生一些令人惊讶的代码做好。在形成关于什么使事情更快或更慢的理论之前自己检查代码。</p><p>规则7：减少测量中的噪音。在一台安静的机器上运行基准测试，并运行几次，抛弃异常值。使用-Xbatch将编译器与应用程序串行化，并考虑设置 -XX：CICompilerCount &#x3D; 1以防止编译器与其自身并行运行。</p><h2 id="JMH-入门"><a href="#JMH-入门" class="headerlink" title="JMH 入门"></a>JMH 入门</h2><h2 id="规则8：使用一些库用来做基准测试，因为它可能更有效率。比如JMH，Caliper，UCSD-Benchmarks-for-Java等。"><a href="#规则8：使用一些库用来做基准测试，因为它可能更有效率。比如JMH，Caliper，UCSD-Benchmarks-for-Java等。" class="headerlink" title="规则8：使用一些库用来做基准测试，因为它可能更有效率。比如JMH，Caliper，UCSD Benchmarks for Java等。"></a>规则8：使用一些库用来做基准测试，因为它可能更有效率。比如JMH，Caliper，UCSD Benchmarks for Java等。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/4987/</id>
    <link href="https://lichuanyang.top/posts/4987/"/>
    <published>2017-06-05T12:39:00.000Z</published>
    <summary>翻译文章：Java 基准测试编写经验，介绍 JMH 使用方法和常见的基准测试陷阱。</summary>
    <title>【翻译】java里编写基准测试的一些经验</title>
    <updated>2026-06-27T00:58:58.267Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <category term="多线程" scheme="https://lichuanyang.top/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    <content>
      <![CDATA[<p>Java 的线程池用起来很方便——<code>Executors.newFixedThreadPool(n)</code> 一行代码搞定。但如果任务提交速度远快于执行速度，任务会在无界队列中无限堆积，最终导致 OOM。今天分享一个真实案例和几种解决方案。</p><span id="more"></span><h2 id="问题场景"><a href="#问题场景" class="headerlink" title="问题场景"></a>问题场景</h2><p>假设你有一个批量处理任务：遍历一个目录下的 100 万个文件，每个文件提交一个任务去处理。每个任务需要加载一些数据到内存。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ExecutorService</span> <span class="variable">executor</span> <span class="operator">=</span> Executors.newFixedThreadPool(<span class="number">10</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (File file : files) &#123;</span><br><span class="line">    executor.submit(() -&gt; &#123;</span><br><span class="line">        <span class="comment">// 这个任务占用大量内存</span></span><br><span class="line">        <span class="type">byte</span>[] data = loadFileContent(file);</span><br><span class="line">        process(data);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>问题在于：<code>Executors.newFixedThreadPool</code> 使用的是<strong>无界队列</strong>（<code>LinkedBlockingQueue</code>），任务提交速度远大于执行速度时，队列中会堆积几十万个未执行的任务，每个任务内部又持有大量数据——内存很快爆炸。</p><h2 id="方案一：手动轮询（不推荐）"><a href="#方案一：手动轮询（不推荐）" class="headerlink" title="方案一：手动轮询（不推荐）"></a>方案一：手动轮询（不推荐）</h2><p>当时应急写了一个土办法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">int</span> <span class="variable">threadCount</span> <span class="operator">=</span> ((ThreadPoolExecutor) executor).getActiveCount();</span><br><span class="line"><span class="keyword">while</span> (threadCount == POOL_SIZE) &#123;</span><br><span class="line">    TimeUnit.MILLISECONDS.sleep(<span class="number">1</span>);</span><br><span class="line">    threadCount = ((ThreadPoolExecutor) executor).getActiveCount();</span><br><span class="line">&#125;</span><br><span class="line">executor.execute(task);</span><br></pre></td></tr></table></figure><p>在提交之前轮询 activeCount，等有空闲线程了再提交。<strong>能跑，但问题很多</strong>：</p><ul><li><strong>Busy-waiting</strong>：浪费 CPU</li><li><strong>没有超时</strong>：万一某个线程永远不释放，提交线程就永远阻塞</li><li><strong><code>getActiveCount()</code> 是近似值</strong>：Java 文档明确说这个方法返回的是近似值，不一定准确</li></ul><h2 id="方案二：有界队列-拒绝策略（推荐）"><a href="#方案二：有界队列-拒绝策略（推荐）" class="headerlink" title="方案二：有界队列 + 拒绝策略（推荐）"></a>方案二：有界队列 + 拒绝策略（推荐）</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ThreadPoolExecutor</span> <span class="variable">executor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ThreadPoolExecutor</span>(</span><br><span class="line">    <span class="number">10</span>,                        <span class="comment">// corePoolSize</span></span><br><span class="line">    <span class="number">10</span>,                        <span class="comment">// maximumPoolSize</span></span><br><span class="line">    <span class="number">60L</span>, TimeUnit.SECONDS,</span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">LinkedBlockingQueue</span>&lt;&gt;(<span class="number">100</span>),  <span class="comment">// 有界队列：最多 100 个等待任务</span></span><br><span class="line">    <span class="keyword">new</span> <span class="title class_">ThreadPoolExecutor</span>.CallerRunsPolicy()  <span class="comment">// 队列满时由提交线程自己执行</span></span><br><span class="line">);</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (File file : files) &#123;</span><br><span class="line">    executor.submit(() -&gt; process(file));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>CallerRunsPolicy</code> 的行为：当线程池和队列都满了，<code>submit()</code> 的调用者自己执行这个任务——相当于天然的背压（backpressure），提交速度自动和消费速度对齐。</p><p>其他拒绝策略：</p><table><thead><tr><th>策略</th><th>行为</th><th>适用场景</th></tr></thead><tbody><tr><td><code>AbortPolicy</code></td><td>抛异常</td><td>需要感知到压力</td></tr><tr><td><code>CallerRunsPolicy</code></td><td>提交者自己执行</td><td>自然背压，推荐</td></tr><tr><td><code>DiscardPolicy</code></td><td>静默丢弃</td><td>不重要的任务</td></tr><tr><td><code>DiscardOldestPolicy</code></td><td>丢弃最老的任务</td><td>优先新任务</td></tr></tbody></table><h2 id="方案三：Semaphore-限流（更精细控制）"><a href="#方案三：Semaphore-限流（更精细控制）" class="headerlink" title="方案三：Semaphore 限流（更精细控制）"></a>方案三：Semaphore 限流（更精细控制）</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Semaphore</span> <span class="variable">semaphore</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Semaphore</span>(<span class="number">10</span> + <span class="number">100</span>);  <span class="comment">// 线程数 + 队列容量</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (File file : files) &#123;</span><br><span class="line">    semaphore.acquire();  <span class="comment">// 超过限制时阻塞</span></span><br><span class="line">    executor.submit(() -&gt; &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            process(file);</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            semaphore.release();  <span class="comment">// 任务完成后释放信号量</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Semaphore</code> 比 <code>CallerRunsPolicy</code> 更灵活——你可以独立控制限流的大小，不受线程池队列长度约束。</p><h2 id="方案四：Guava-RateLimiter（平滑限流）"><a href="#方案四：Guava-RateLimiter（平滑限流）" class="headerlink" title="方案四：Guava RateLimiter（平滑限流）"></a>方案四：Guava RateLimiter（平滑限流）</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">RateLimiter</span> <span class="variable">limiter</span> <span class="operator">=</span> RateLimiter.create(<span class="number">50.0</span>);  <span class="comment">// 每秒最多提交 50 个任务</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (File file : files) &#123;</span><br><span class="line">    limiter.acquire();  <span class="comment">// 匀速等待</span></span><br><span class="line">    executor.submit(() -&gt; process(file));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>RateLimiter</code> 是 Guava 提供的令牌桶实现，可以平滑控制提交速率。适合需要精确控制 QPS 的场景。</p><h2 id="总结：背压策略怎么选？"><a href="#总结：背压策略怎么选？" class="headerlink" title="总结：背压策略怎么选？"></a>总结：背压策略怎么选？</h2><ul><li><strong>简单场景</strong>：直接用 <code>CallerRunsPolicy</code> + 有界队列，零额外代码</li><li><strong>需要精细控制</strong>：<code>Semaphore</code> 限流</li><li><strong>需要平滑速率</strong>：Guava <code>RateLimiter</code></li><li><strong>永远不要</strong>：无界队列 + busy-waiting 轮询 <code>getActiveCount()</code></li></ul><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/5707/</id>
    <link href="https://lichuanyang.top/posts/5707/"/>
    <published>2017-05-26T12:36:00.000Z</published>
    <summary>解决线程池中任务提交速度远快于执行速度导致内存爆满的问题，介绍在提交前检查运行线程数的方法。</summary>
    <title>java线程池：获取运行线程数并控制线程启动速度</title>
    <updated>2026-06-27T02:15:49.472Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="maven" scheme="https://lichuanyang.top/tags/maven/"/>
    <content>
      <![CDATA[<p>maven里的mirror和repository是两个比较容易混淆的概念，它们的作用都是配置远程maven仓库的地址。顾名思义，repository就是直接配置站点地址，mirror则是作为站点的镜像，代理某个或某几个站点的请求，实现对repository的完全代替。</p><h2 id="repository"><a href="#repository" class="headerlink" title="repository"></a>repository</h2><p>有两种形式可以配置多个repository, 配置多个profile或者在同一个profile中配置多个repository.配置多profile时，还需要配置activeProfiles使配置生效。</p><p>配置示例：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br></pre></td><td class="code"><pre><span class="line">      &lt;profiles&gt;</span><br><span class="line">&lt;/profile&gt;</span><br><span class="line">&lt;profile&gt;</span><br><span class="line">&lt;id&gt;central&lt;/id&gt;</span><br><span class="line">&lt;repositories&gt;</span><br><span class="line">&lt;repository&gt;</span><br><span class="line">&lt;id&gt;central&lt;/id&gt;</span><br><span class="line">&lt;url&gt;http://search.maven.org/&lt;/url&gt;</span><br><span class="line">&lt;!-- &lt;releases&gt; &lt;enabled&gt;true&lt;/enabled&gt; &lt;/releases&gt; &lt;snapshots&gt; &lt;enabled&gt;true&lt;/enabled&gt; </span><br><span class="line">&lt;/snapshots&gt; --&gt;</span><br><span class="line">&lt;releases&gt;</span><br><span class="line">&lt;enabled&gt;true&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/releases&gt;</span><br><span class="line">&lt;snapshots&gt;</span><br><span class="line">&lt;enabled&gt;false&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/snapshots&gt;</span><br><span class="line">&lt;/repository&gt;</span><br><span class="line">&lt;/repositories&gt;</span><br><span class="line">&lt;pluginRepositories&gt;</span><br><span class="line">&lt;pluginRepository&gt;</span><br><span class="line">&lt;id&gt;central&lt;/id&gt;</span><br><span class="line">&lt;url&gt;http://search.maven.org/&lt;/url&gt;</span><br><span class="line">&lt;releases&gt;</span><br><span class="line">&lt;enabled&gt;false&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/releases&gt;</span><br><span class="line">&lt;snapshots&gt;</span><br><span class="line">&lt;enabled&gt;true&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/snapshots&gt;</span><br><span class="line">&lt;/pluginRepository&gt;</span><br><span class="line">&lt;/pluginRepositories&gt;</span><br><span class="line">&lt;/profile&gt;</span><br><span class="line">&lt;profile&gt;</span><br><span class="line">&lt;id&gt;aliyun&lt;/id&gt;</span><br><span class="line">&lt;repositories&gt;</span><br><span class="line">&lt;repository&gt;</span><br><span class="line">&lt;id&gt;aliyun&lt;/id&gt;</span><br><span class="line">&lt;url&gt;http://maven.aliyun.com/nexus/content/groups/public&lt;/url&gt;</span><br><span class="line">&lt;!-- &lt;releases&gt; &lt;enabled&gt;true&lt;/enabled&gt; &lt;/releases&gt; &lt;snapshots&gt; &lt;enabled&gt;true&lt;/enabled&gt; </span><br><span class="line">&lt;/snapshots&gt; --&gt;</span><br><span class="line">&lt;releases&gt;</span><br><span class="line">&lt;enabled&gt;true&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/releases&gt;</span><br><span class="line">&lt;snapshots&gt;</span><br><span class="line">&lt;enabled&gt;true&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/snapshots&gt;</span><br><span class="line">&lt;/repository&gt;</span><br><span class="line">&lt;/repositories&gt;</span><br><span class="line">&lt;pluginRepositories&gt;</span><br><span class="line">&lt;pluginRepository&gt;</span><br><span class="line">&lt;id&gt;aliyun&lt;/id&gt;</span><br><span class="line">&lt;url&gt;http://maven.aliyun.com/nexus/content/groups/public&lt;/url&gt;</span><br><span class="line">&lt;releases&gt;</span><br><span class="line">&lt;enabled&gt;true&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/releases&gt;</span><br><span class="line">&lt;snapshots&gt;</span><br><span class="line">&lt;enabled&gt;true&lt;/enabled&gt;</span><br><span class="line">&lt;updatePolicy&gt;always&lt;/updatePolicy&gt;</span><br><span class="line">&lt;/snapshots&gt;</span><br><span class="line">&lt;/pluginRepository&gt;</span><br><span class="line">&lt;/pluginRepositories&gt;</span><br><span class="line">&lt;/profile&gt;</span><br><span class="line">          &lt;/profiles&gt;</span><br><span class="line"></span><br><span class="line">          &lt;activeProfiles&gt;</span><br><span class="line">&lt;activeProfile&gt;aliyun&lt;/activeProfile&gt;</span><br><span class="line">&lt;activeProfile&gt;central&lt;/activeProfile&gt;</span><br><span class="line">  &lt;/activeProfiles&gt;</span><br></pre></td></tr></table></figure><p>单profile,多repository的配置也类似。</p><p>这样就实现了多站点的配置。下载依赖时，maven会按照配置从上到下的顺序，依次尝试从各个地址下载，成功下载为止。</p><h2 id="mirror"><a href="#mirror" class="headerlink" title="mirror"></a>mirror</h2><p>个人感觉mirror的存在有些鸡肋，如果不想用repository里配的地址，完全可以直接改掉，而不用再加一条mirror配置。</p><p>如果setting.xml和pom里都配置了repository, 配置的mirror是可以对两个配置文件都生效的，这可能是mirror存在的唯一意义。</p><p>mirror的配置示例：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">mirror</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">id</span>&gt;</span>nexus-aliyun<span class="tag">&lt;/<span class="name">id</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">mirrorOf</span>&gt;</span>*<span class="tag">&lt;/<span class="name">mirrorOf</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">name</span>&gt;</span>Nexus aliyun<span class="tag">&lt;/<span class="name">name</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">url</span>&gt;</span>http://maven.aliyun.com/nexus/content/groups/public<span class="tag">&lt;/<span class="name">url</span>&gt;</span></span><br><span class="line">       <span class="tag">&lt;/<span class="name">mirror</span>&gt;</span></span><br></pre></td></tr></table></figure><p>使用mirrorOf指定这个镜像是针对哪个repository的，配置成*就表示要代理所有repository的请求。</p><p>需要注意的是，与repository不同，配置到同一个repository的多个mirror时，相互之间是备份的关系，只有在仓库连不上的时候才会切换另一个，而如果在能连上的情况下找不到包，是不会尝试下一个地址的。</p><h2 id="所以，一般情况下，无论是配置国内的maven仓库，还是配置nexus之类私服，都可以直接配置成repository，-这样即使配置的这些仓库有些问题导致一些包下不下来，也可以继续用别的仓库尝试。"><a href="#所以，一般情况下，无论是配置国内的maven仓库，还是配置nexus之类私服，都可以直接配置成repository，-这样即使配置的这些仓库有些问题导致一些包下不下来，也可以继续用别的仓库尝试。" class="headerlink" title="所以，一般情况下，无论是配置国内的maven仓库，还是配置nexus之类私服，都可以直接配置成repository， 这样即使配置的这些仓库有些问题导致一些包下不下来，也可以继续用别的仓库尝试。"></a>所以，一般情况下，无论是配置国内的maven仓库，还是配置nexus之类私服，都可以直接配置成repository， 这样即使配置的这些仓库有些问题导致一些包下不下来，也可以继续用别的仓库尝试。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/32793/</id>
    <link href="https://lichuanyang.top/posts/32793/"/>
    <published>2017-05-19T11:40:00.000Z</published>
    <summary>厘清 Maven 中 mirror 和 repository 的区别，介绍配置多仓库和镜像代理的正确方式。</summary>
    <title>maven里的mirror和repository: 配置多repository</title>
    <updated>2026-06-27T03:43:25.705Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <category term="maven" scheme="https://lichuanyang.top/tags/maven/"/>
    <content>
      <![CDATA[<h2 id="Fat-Jar-是什么"><a href="#Fat-Jar-是什么" class="headerlink" title="Fat Jar 是什么"></a>Fat Jar 是什么</h2><p>eclipse里有一个功能叫做“打可执行(runnable) jar包”, 用这个功能可以把一个工程自身和所有依赖包打成一个fat jar，并且指定Main方法，这样直接使用java jar xxx.jar就可以运行代码了。</p><h2 id="Maven-Shade-Plugin-配置"><a href="#Maven-Shade-Plugin-配置" class="headerlink" title="Maven Shade Plugin 配置"></a>Maven Shade Plugin 配置</h2><p>但是在不使用eclipse的时候呢？其实，借助maven，我们很容易实现同样功能。maven提供了一个shade plugin,可以用来打fat jar, 同时也提供了指定main方法的功能。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">project</span>&gt;</span></span><br><span class="line">  ...</span><br><span class="line">  <span class="tag">&lt;<span class="name">build</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">plugins</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;<span class="name">plugin</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.maven.plugins<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>maven-shade-plugin<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">version</span>&gt;</span>3.0.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">executions</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;<span class="name">execution</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">phase</span>&gt;</span>package<span class="tag">&lt;/<span class="name">phase</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">goals</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">goal</span>&gt;</span>shade<span class="tag">&lt;/<span class="name">goal</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">goals</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;<span class="name">transformers</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">transformer</span> <span class="attr">implementation</span>=<span class="string">&quot;org.apache.maven.plugins.shade.resource.ManifestResourceTransformer&quot;</span>&gt;</span></span><br><span class="line">                  <span class="tag">&lt;<span class="name">mainClass</span>&gt;</span>org.sonatype.haven.HavenCli<span class="tag">&lt;/<span class="name">mainClass</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="name">transformer</span>&gt;</span></span><br><span class="line">              <span class="tag">&lt;/<span class="name">transformers</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line">          <span class="tag">&lt;/<span class="name">execution</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">executions</span>&gt;</span></span><br><span class="line">      <span class="tag">&lt;/<span class="name">plugin</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">plugins</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;/<span class="name">build</span>&gt;</span></span><br><span class="line">  ...</span><br><span class="line"><span class="tag">&lt;/<span class="name">project</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h2 id="然后在用maven打包的时候就可以打出直接可运行的包了。"><a href="#然后在用maven打包的时候就可以打出直接可运行的包了。" class="headerlink" title="然后在用maven打包的时候就可以打出直接可运行的包了。"></a>然后在用maven打包的时候就可以打出直接可运行的包了。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/3945/</id>
    <link href="https://lichuanyang.top/posts/3945/"/>
    <published>2017-05-14T01:32:00.000Z</published>
    <summary>使用 Maven Shade Plugin 打可执行 Fat Jar，替代 Eclipse 的 Export 功能，指定 Main 方法一键运行。</summary>
    <title>使用maven shade plugin 打可执行Jar包</title>
    <updated>2026-06-27T01:00:36.675Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="消息队列" scheme="https://lichuanyang.top/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    <category term="activemq" scheme="https://lichuanyang.top/tags/activemq/"/>
    <content>
      <![CDATA[<p>activemq提供了一种插件(plugin)开发的机制(<a href="http://activemq.apache.org/developing-plugins.html">http://activemq.apache.org/developing-plugins.html</a>), 可以方便的添加各种自定义功能。其效果类似于直接去修改activemq的源码，无论从方便性还是风险程度上，使用plugin都要比去修改源码要好很多。通过这种方式，理论上我们可以实现几乎所有能想到的功能。</p><h2 id="开发指南"><a href="#开发指南" class="headerlink" title="开发指南"></a>开发指南</h2><p>首先需要在工程中添加activemq的依赖，为了方便，可以直接用activemq-all包。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.activemq<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>activemq-all<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>5.13.2<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>Actviemq plugin的开发非常方便，只需要实现两个类。</p><p>一个是plugin类， 需要implements BrokerPlugin这个接口，然后将接口的installPlugin方法实现了即可。</p><p>这个方法的唯一作用就是指定一个broker类，代码如下：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Broker <span class="title function_">installPlugin</span><span class="params">(Broker broker)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">LimitQueueSIzeBroker</span>(broker);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>接下来就是在实现一个broker类，一般情况下可以extends BrokerFilter 这个类，然后选择需要的方法重写，构造方法和重写的每个方法执行完毕后，执行以下super类的对应方法，这样就不会对activemq的实际运行造成影响。</p><p>例如：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="title function_">FoxBroker</span><span class="params">(Broker next)</span> &#123;</span><br><span class="line">    <span class="built_in">super</span>(next);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">send</span><span class="params">(ProducerBrokerExchange producerExchange, Message messageSend)</span> <span class="keyword">throws</span> Exception &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">ip</span> <span class="operator">=</span> producerExchange.getConnectionContext().getConnection().getRemoteAddress();</span><br><span class="line">        <span class="type">String</span> <span class="variable">destinationName</span> <span class="operator">=</span> messageSend.getDestination().getPhysicalName();</span><br><span class="line">        logger.info(<span class="string">&quot;send_&quot;</span> + destinationName + <span class="string">&quot; &quot;</span>  + ip);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">        logger.error(<span class="string">&quot;activemq send log error: &quot;</span> + e, e);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="built_in">super</span>.send(producerExchange, messageSend);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里我们重写了activemq的send方法, 并且增加了一些日志。</p><p>然后打包，将jar包放到activemq的lib目录下。再在activemq.xml下增加plugins这个模块，并把需要的插件依次列在里边。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">plugins</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">xmlns</span>=<span class="string">&quot;http://www.springframework.org/schema/beans&quot;</span> <span class="attr">id</span>=<span class="string">&quot;testPlugin&quot;</span> <span class="attr">class</span>=<span class="string">&quot;com.mallow.activemq.FoxBrokerPlugin&quot;</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">xmlns</span>=<span class="string">&quot;http://www.springframework.org/schema/beans&quot;</span> <span class="attr">id</span>=<span class="string">&quot;purgePlugin&quot;</span> <span class="attr">class</span>=<span class="string">&quot;com.mallow.activemq.LimitQueueSizePlugin&quot;</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">plugins</span>&gt;</span></span><br></pre></td></tr></table></figure><p>重启activemq,再收发消息的时候就能看到效果了。</p><h2 id="例子"><a href="#例子" class="headerlink" title="例子"></a>例子</h2><p>可以参考我在github上放的代码： <a href="https://github.com/lcy362/FoxActivemqPlugin">https://github.com/lcy362/FoxActivemqPlugin</a></p><p>里边提供了两个非常简单的插件示例。FoxBrokerPlugin会在收发消息时记录生产者、消费者的ip; LimitQueueSizePlugin则可以控制队列大小，队列中积压1000条消息以后新的消息就会被丢弃，在测试环境下会比较有用。</p><h2 id="此外，activemq自己也提供了几个常用的插件，包括LoggingBrokerPlugin，-StatisticsBrokerPlugin-等，-也可以参考他们的实现。"><a href="#此外，activemq自己也提供了几个常用的插件，包括LoggingBrokerPlugin，-StatisticsBrokerPlugin-等，-也可以参考他们的实现。" class="headerlink" title="此外，activemq自己也提供了几个常用的插件，包括LoggingBrokerPlugin， StatisticsBrokerPlugin, 等， 也可以参考他们的实现。"></a>此外，activemq自己也提供了几个常用的插件，包括LoggingBrokerPlugin， StatisticsBrokerPlugin, 等， 也可以参考他们的实现。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/61645/</id>
    <link href="https://lichuanyang.top/posts/61645/"/>
    <published>2017-05-05T08:58:00.000Z</published>
    <summary>ActiveMQ 插件(plugin)开发的完整指南，介绍如何通过插件机制方便地添加自定义功能，比直接修改源码更安全高效。</summary>
    <title>activemq plugin开发指南及示例</title>
    <updated>2026-06-27T02:31:54.501Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <content>
      <![CDATA[<p>在命令行直接运行 Java 程序时，如果需要引入第三方 jar 包，通常的做法是用 Maven&#x2F;Gradle 打一个 fat jar（把依赖全部打包进去）。但在某些轻量场景下——比如写了一个小脚本、做快速原型验证、或者在 CI 环境中临时跑一段 Java 代码——手动设置 classpath 反而更直接。</p><span id="more"></span><h2 id="Classpath-是什么？"><a href="#Classpath-是什么？" class="headerlink" title="Classpath 是什么？"></a>Classpath 是什么？</h2><p>Classpath 是 JVM 查找类文件的搜索路径。当你写 <code>java com.example.Main</code> 的时候，JVM 会在 classpath 中逐条路径搜索 <code>com/example/Main.class</code>。</p><p>默认情况下，classpath 只包含当前目录（<code>.</code>）。如果程序依赖了其他 jar 包，就需要手动把它们加到 classpath 中。</p><h2 id="方法一：Shell-脚本拼接"><a href="#方法一：Shell-脚本拼接" class="headerlink" title="方法一：Shell 脚本拼接"></a>方法一：Shell 脚本拼接</h2><p>写一个简单的启动脚本，手动拼接所有依赖 jar 的路径：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 定义 CLASSPATH</span></span><br><span class="line">CLASSPATH=your-main.jar:lib/dep1.jar:lib/dep2.jar:<span class="string">&quot;<span class="variable">$CLASSPATH</span>&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 运行</span></span><br><span class="line">java -classpath <span class="string">&quot;<span class="variable">$&#123;CLASSPATH&#125;</span>&quot;</span> com.example.Main</span><br></pre></td></tr></table></figure><p><strong>几个注意点</strong>：</p><ul><li><strong>自己的主类所在的 jar 也要包含进去</strong>，这点容易忘</li><li><strong>Linux&#x2F;macOS 用 <code>:</code> 分隔</strong>，Windows 用 <code>;</code>：<figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Linux</span></span><br><span class="line">java -classpath <span class="string">&quot;a.jar:b.jar&quot;</span> Main</span><br><span class="line"></span><br><span class="line"><span class="comment"># Windows</span></span><br><span class="line">java -classpath <span class="string">&quot;a.jar;b.jar&quot;</span> Main</span><br></pre></td></tr></table></figure></li><li><strong>路径中有空格要加引号</strong></li></ul><h2 id="方法二：通配符批量引入"><a href="#方法二：通配符批量引入" class="headerlink" title="方法二：通配符批量引入"></a>方法二：通配符批量引入</h2><p>如果 lib 目录下 jar 很多，一个一个写太麻烦。Java 6 开始支持通配符：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -classpath <span class="string">&quot;your-main.jar:lib/*&quot;</span> com.example.Main</span><br></pre></td></tr></table></figure><p><code>lib/*</code> 会自动匹配 <code>lib/</code> 下的所有 <code>.jar</code> 文件。<strong>注意</strong>：通配符只匹配 jar，不匹配 class 文件或子目录。</p><h2 id="方法三：自动生成启动脚本"><a href="#方法三：自动生成启动脚本" class="headerlink" title="方法三：自动生成启动脚本"></a>方法三：自动生成启动脚本</h2><p>如果你的项目用 Maven，可以用 <code>maven-dependency-plugin</code> 自动生成 classpath：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">plugin</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.maven.plugins<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>maven-dependency-plugin<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">executions</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">execution</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">goals</span>&gt;</span><span class="tag">&lt;<span class="name">goal</span>&gt;</span>build-classpath<span class="tag">&lt;/<span class="name">goal</span>&gt;</span><span class="tag">&lt;/<span class="name">goals</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">configuration</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">outputFile</span>&gt;</span>classpath.txt<span class="tag">&lt;/<span class="name">outputFile</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">execution</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">executions</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">plugin</span>&gt;</span></span><br></pre></td></tr></table></figure><p>然后在脚本里读取：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">CLASSPATH=$(<span class="built_in">cat</span> target/classpath.txt):target/your-app.jar</span><br><span class="line">java -classpath <span class="string">&quot;<span class="variable">$&#123;CLASSPATH&#125;</span>&quot;</span> com.example.Main</span><br></pre></td></tr></table></figure><h2 id="什么时候用手动-classpath，什么时候用-Fat-Jar？"><a href="#什么时候用手动-classpath，什么时候用-Fat-Jar？" class="headerlink" title="什么时候用手动 classpath，什么时候用 Fat Jar？"></a>什么时候用手动 classpath，什么时候用 Fat Jar？</h2><table><thead><tr><th>方案</th><th>适用场景</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>手动 classpath</td><td>快速原型、CI 脚本、小工具</td><td>简单直接，不需额外构建</td><td>容易写错路径</td></tr><tr><td>Fat Jar（Shade&#x2F;Assembly）</td><td>生产部署</td><td>单文件，到处可跑</td><td>构建慢，文件大</td></tr><tr><td>Docker 镜像</td><td>现代标准部署</td><td>环境一致性最好</td><td>需要 Docker 环境</td></tr></tbody></table><p>如果你只是想在服务器上快速跑一段 Java 代码验证一个想法，手动拼 classpath 是最快的方式。但如果是生产环境，还是用 fat jar 或 Docker 镜像更规范。</p><h2 id="另外，如果你需要更复杂的依赖管理，可以参考我之前写的-Maven-Shade-Plugin-打可执行-Jar-包-一文。"><a href="#另外，如果你需要更复杂的依赖管理，可以参考我之前写的-Maven-Shade-Plugin-打可执行-Jar-包-一文。" class="headerlink" title="另外，如果你需要更复杂的依赖管理，可以参考我之前写的 Maven Shade Plugin 打可执行 Jar 包 一文。"></a>另外，如果你需要更复杂的依赖管理，可以参考我之前写的 <a href="/posts/3945/">Maven Shade Plugin 打可执行 Jar 包</a> 一文。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/65262/</id>
    <link href="https://lichuanyang.top/posts/65262/"/>
    <published>2017-05-03T10:42:00.000Z</published>
    <summary>通过 Shell 脚本设置 CLASSPATH，实现命令行运行 Java 程序时引入第三方依赖。</summary>
    <title>通过加入classpath的形式实现命令行运行java程序时引入第三方jar包</title>
    <updated>2026-06-27T03:46:06.799Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <content>
      <![CDATA[<h2 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a>问题现象</h2><p>最近试图运行一个fatjar的时候报错：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">Exception in thread &quot;main&quot; java.lang.SecurityException: Invalid signature file digest for Manifest main attributes</span><br><span class="line">at sun.security.util.SignatureFileVerifier.processImpl(SignatureFileVerifier.java:287)</span><br><span class="line">at sun.security.util.SignatureFileVerifier.process(SignatureFileVerifier.java:240)</span><br><span class="line">at java.util.jar.JarVerifier.processEntry(JarVerifier.java:317)</span><br><span class="line">at java.util.jar.JarVerifier.update(JarVerifier.java:228)</span><br><span class="line">at java.util.jar.JarFile.initializeVerifier(JarFile.java:348)</span><br><span class="line">at java.util.jar.JarFile.getInputStream(JarFile.java:415)</span><br><span class="line">at sun.misc.URLClassPath$JarLoader$2.getInputStream(URLClassPath.java:775)</span><br><span class="line">at sun.misc.Resource.cachedInputStream(Resource.java:77)</span><br><span class="line">at sun.misc.Resource.getByteBuffer(Resource.java:160)</span><br><span class="line">at java.net.URLClassLoader.defineClass(URLClassLoader.java:436)</span><br><span class="line">at java.net.URLClassLoader.access$100(URLClassLoader.java:71)</span><br><span class="line">at java.net.URLClassLoader$1.run(URLClassLoader.java:361)</span><br><span class="line">at java.net.URLClassLoader$1.run(URLClassLoader.java:355)</span><br><span class="line">at java.security.AccessController.doPrivileged(Native Method)</span><br><span class="line">at java.net.URLClassLoader.findClass(URLClassLoader.java:354)</span><br><span class="line">at java.lang.ClassLoader.loadClass(ClassLoader.java:425)</span><br><span class="line">at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)</span><br><span class="line">at java.lang.ClassLoader.loadClass(ClassLoader.java:358)</span><br><span class="line">at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:482)</span><br></pre></td></tr></table></figure><h2 id="根因：签名文件冲突"><a href="#根因：签名文件冲突" class="headerlink" title="根因：签名文件冲突"></a>根因：签名文件冲突</h2><p>查了一下，跟jar包的签名有关。关于签名可以参考： <a href="http://www.cnblogs.com/jackofhearts/p/jar_signing.html">http://www.cnblogs.com/jackofhearts/p/jar_signing.html</a></p><p>有些Jar包会在metainf里包含一个.SF：包含原Jar包内的class文件和资源文件的Hash， 用来校验文件的完整度等验证。</p><h2 id="Maven-Shade-引发的问题"><a href="#Maven-Shade-引发的问题" class="headerlink" title="Maven Shade 引发的问题"></a>Maven Shade 引发的问题</h2><p>但是在打fat-jar的时候，我们是把很多jar包合成了一个，这样fatjar下就会存在各个jar包中的签名文件，但是他们显然无法跟最终的fatjar作校验。</p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>解决方法就是打包时把签名文件全都去掉，如果是使用maven的话，可以使用shade插件：</p><h2 id="Maven-配置示例"><a href="#Maven-配置示例" class="headerlink" title="Maven 配置示例"></a>Maven 配置示例</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">&lt;plugin&gt;</span><br><span class="line">     &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;</span><br><span class="line">     &lt;artifactId&gt;maven-shade-plugin&lt;/artifactId&gt;</span><br><span class="line">     &lt;version&gt;1.7.1&lt;/version&gt;</span><br><span class="line">     &lt;executions&gt;</span><br><span class="line">         &lt;execution&gt;</span><br><span class="line">             &lt;phase&gt;package&lt;/phase&gt;</span><br><span class="line">             &lt;goals&gt;</span><br><span class="line">                 &lt;goal&gt;shade&lt;/goal&gt;</span><br><span class="line">             &lt;/goals&gt;</span><br><span class="line">             &lt;configuration&gt;</span><br><span class="line">                 &lt;filters&gt;</span><br><span class="line">                     &lt;filter&gt;</span><br><span class="line">                         &lt;artifact&gt;*:*&lt;/artifact&gt;</span><br><span class="line">                         &lt;excludes&gt;</span><br><span class="line">                             &lt;exclude&gt;META-INF/*.SF&lt;/exclude&gt;</span><br><span class="line">                             &lt;exclude&gt;META-INF/*.DSA&lt;/exclude&gt;</span><br><span class="line">                             &lt;exclude&gt;META-INF/*.RSA&lt;/exclude&gt;</span><br><span class="line">                         &lt;/excludes&gt;</span><br><span class="line">                     &lt;/filter&gt;</span><br><span class="line">                 &lt;/filters&gt;</span><br><span class="line">             &lt;/configuration&gt;</span><br><span class="line">         &lt;/execution&gt;</span><br><span class="line">     &lt;/executions&gt;</span><br><span class="line"> &lt;/plugin&gt;</span><br></pre></td></tr></table></figure><h2 id="预防措施"><a href="#预防措施" class="headerlink" title="预防措施"></a>预防措施</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/2478/</id>
    <link href="https://lichuanyang.top/posts/2478/"/>
    <published>2017-04-27T06:16:00.000Z</published>
    <summary>解决 Fat Jar 合并时因签名校验导致的 SecurityException，理解 META-INF 签名机制及排除签名文件的方法。</summary>
    <title>解决fatjar的 &quot;java.lang.SecurityException: Invalid signature file digest for Manifest main attributes&quot; 问题</title>
    <updated>2026-06-27T03:44:21.601Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="redis系列" scheme="https://lichuanyang.top/categories/redis%E7%B3%BB%E5%88%97/"/>
    <category term="redis" scheme="https://lichuanyang.top/tags/redis/"/>
    <category term="redis-cluster" scheme="https://lichuanyang.top/tags/redis-cluster/"/>
    <content>
      <![CDATA[<p>Redis 集群是redis官方提供的一种集群方案，从3.0开始提供稳定版，应用也已经比较广泛，也经受住了时间考验，个人感觉完全可以取代codis,tweemproxy等集群方案。</p><h2 id="cluster原理介绍"><a href="#cluster原理介绍" class="headerlink" title="cluster原理介绍"></a>cluster原理介绍</h2><p>cluster是使用数据分片的形式实现的，一个 Redis cluster集群包含 16384 个哈希槽, 任意一个key都可以通过 CRC16(key) % 16384 这个公式计算出应当属于哪个槽。每个槽应当落在哪个节点上，也是事先定好。这样，进行任一操作时，首先会根据key计算出对应的节点，然后操作相应的节点就可以了。</p><p>所以说，其实cluster跟单点相比，只是多了一个给key计算sharding值的过程，并没有增加多少复杂度，完全可以放心使用。</p><p>在这样的设计下，一些对节点的操作也很方便，操作过程中对client端也不会有影响。<br>1. 增加节点：将原有节点的某些slot转移到新节点上<br>2. 删除节点: 将节点上的槽转移到其他节点上以后，移除空节点<br>3. 节点故障：落到故障节点上的操作会失败，而集群其他部分可以正常访问，这样就不会出现一致性哈希方案的“雪崩”问题。单点的故障也可以通过配置slave解决，节点故障时可以由对应的slave顶替<br>4. 节点重启: 正常重启即可，节点会自动读之前的配置然后加入集群中。</p><h2 id="安装操作流程"><a href="#安装操作流程" class="headerlink" title="安装操作流程"></a>安装操作流程</h2><p>然后，我们可以实际安装一次，由于只是测试，装个两三个节点就可以，而如果是生产使用的话，至少需要3个master节点，也推荐同时至少有3个slave节点才能使用。</p><h3 id="安装redis"><a href="#安装redis" class="headerlink" title="安装redis"></a>安装redis</h3><p>到redis官网(<a href="https://redis.io/download)%E4%B8%8A%E4%B8%8B%E8%BD%BD%E6%9C%80%E6%96%B0%E7%9A%84stable%E7%89%88%E6%9C%AC%E3%80%82">https://redis.io/download)上下载最新的stable版本。</a></p><p>以3.2.8为例，下载完以后执行以下命令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">$ tar xzf redis-3.2.8.tar.gz</span><br><span class="line">$ cd redis-3.2.8</span><br><span class="line">$ make</span><br></pre></td></tr></table></figure><p>然后解压缩，make就可以了.</p><h3 id="redis配置与启动"><a href="#redis配置与启动" class="headerlink" title="redis配置与启动"></a>redis配置与启动</h3><p>make之后会在redis的src目录下多出几个命令，包括redis-server, redis-cli等，可以把他们拷到&#x2F;usr&#x2F;local&#x2F;bin下，这样就可以在任意路径下执行。也可以自己指到src目录下进行操作。</p><p>启动之前需要先修改一下配置，redis的根目录下有一个redis.conf文件，可以直接改它，也可以拷到别的路径下进行修改。</p><p>主要就是把cluster模式打开，然后配一下cluster配置文件。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">cluster-enabled yes</span><br><span class="line">cluster-config-file nodes.conf</span><br></pre></td></tr></table></figure><p>其他的配置，port,bind,protect-mode之类的，可以根据需求改一下，不改也没关系。如果是想在一台机器上启动多个实例的话,需要给每个实例一个配置文件， port, cluster-config-file这些配置也需要区分一下。</p><p>然后执行以下命令，分别指定不同的配置文件启动。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">redis-server redis-6379.conf &amp;</span><br><span class="line">redis-server redis-6380.conf &amp;</span><br></pre></td></tr></table></figure><h3 id="开启cluster"><a href="#开启cluster" class="headerlink" title="开启cluster"></a>开启cluster</h3><p>需要的节点都启动以后，就可以开始配置cluster了。推荐的办法是使用官方提供的redis-trib.rb工具包，这个具体操作可以参考官方文档。我们在这里试一下手动操作，这样其实可以加深对cluster原理的理解。</p><p>首先通过redis-cli命令进入任意一个redis实例中，然后一次对其他几个节点执行meet命令。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Cluster meet xxx:6380</span><br></pre></td></tr></table></figure><p>然后执行cluster nodes命令就可以看到节点被加入到集群中了。</p><p>这之后还有一步是分配slot, 可以使用cluster setslot命令，不过由于slot比较多，分配起来很麻烦。还有另外一种方式是修改nodes.conf这个配置文件。</p><p>Nodes.conf这个配置包含了cluster相关的几乎所有信息，一般情况下无需自己去操作，redis会自动生成，可以打开看一下，里边每个节点都对应一行，一行是类似这种格式：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">8301106a1a0b7472650f22503abea23024df17fb 127.0.0.1:6379 myself,master - 0 0 1 connected</span><br></pre></td></tr></table></figure><p>然后slot的分配情况是加在最后</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">8301106a1a0b7472650f22503abea23024df17fb 127.0.0.1:6379 myself,master - 0 0 1 connected 0-8000</span><br></pre></td></tr></table></figure><p>需要注意的是，全部16384(0-16383)个slot都要分配出去，不能留空；集群中每个节点都对应一个Nodes.conf文件，每个文件里的slot配置要一致。</p><p>改完以后重启所有节点，在redis-cli中执行cluster info 命令，可以看到一些关键信息</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">cluster_state:ok</span><br><span class="line">cluster_slots_assigned:16384</span><br><span class="line">cluster_slots_ok:16384</span><br><span class="line">cluster_slots_pfail:0</span><br><span class="line">cluster_slots_fail:0</span><br><span class="line">cluster_known_nodes:2</span><br><span class="line">cluster_size:1</span><br></pre></td></tr></table></figure><p>所有slot已经分配出去，Cluster stat也变成了ok. 这样一个cluster就算搭建完毕了。</p><h2 id="原文地址：https-lichuanyang-top-posts-9329"><a href="#原文地址：https-lichuanyang-top-posts-9329" class="headerlink" title="原文地址：https://lichuanyang.top/posts/9329/"></a>原文地址：<a href="https://lichuanyang.top/posts/9329/">https://lichuanyang.top/posts/9329/</a></h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/9329/</id>
    <link href="https://lichuanyang.top/posts/9329/"/>
    <published>2017-04-14T12:02:00.000Z</published>
    <summary>通过实际操作理解 Redis Cluster 的数据分片机制、16384 个哈希槽的设计和节点路由原理。</summary>
    <title>通过实际操作理解redis cluster原理</title>
    <updated>2026-06-27T02:31:54.497Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="大数据" scheme="https://lichuanyang.top/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <category term="hbase" scheme="https://lichuanyang.top/tags/hbase/"/>
    <content>
      <![CDATA[<p>HBase 的存储模型是简单的键值对（rowkey → column family → qualifier → value），不像 Redis 那样原生支持 Set、List、Hash 等数据结构。但实际业务中，我们经常需要存储集合类型的数据——比如用户的标签集合、商品的属性集合、社交关系中的好友列表。</p><p>如何在 HBase 中优雅地实现一个 Set？本文介绍一种利用 HBase 列（qualifier）特性实现的高效方案。</p><span id="more"></span><h2 id="需求定义"><a href="#需求定义" class="headerlink" title="需求定义"></a>需求定义</h2><p>一个合格的 Set 存储方案需要满足：</p><ol><li><strong>元素自动去重</strong>——Set 的基本语义</li><li><strong>原子操作单个元素</strong>——增删查改不应影响其他元素</li><li><strong>方便查询</strong>——能快速获取整个 Set，也能快速判断某个元素是否存在</li></ol><h2 id="方案对比"><a href="#方案对比" class="headerlink" title="方案对比"></a>方案对比</h2><h3 id="方案-A：整体序列化"><a href="#方案-A：整体序列化" class="headerlink" title="方案 A：整体序列化"></a>方案 A：整体序列化</h3><p>把整个 Set 序列化为一个字节数组，存到 HBase 的某个 cell 中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 写</span></span><br><span class="line">Set&lt;String&gt; set = <span class="keyword">new</span> <span class="title class_">HashSet</span>&lt;&gt;(Arrays.asList(<span class="string">&quot;a&quot;</span>, <span class="string">&quot;b&quot;</span>, <span class="string">&quot;c&quot;</span>));</span><br><span class="line"><span class="type">byte</span>[] bytes = serialize(set);</span><br><span class="line"><span class="type">Put</span> <span class="variable">put</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Put</span>(Bytes.toBytes(<span class="string">&quot;row1&quot;</span>));</span><br><span class="line">put.addColumn(Bytes.toBytes(<span class="string">&quot;cf&quot;</span>), Bytes.toBytes(<span class="string">&quot;set&quot;</span>), bytes);</span><br><span class="line">table.put(put);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 读 → 修改 → 覆盖</span></span><br><span class="line"><span class="type">Get</span> <span class="variable">get</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Get</span>(Bytes.toBytes(<span class="string">&quot;row1&quot;</span>));</span><br><span class="line"><span class="type">Result</span> <span class="variable">result</span> <span class="operator">=</span> table.get(get);</span><br><span class="line">Set&lt;String&gt; set = deserialize(result.getValue(Bytes.toBytes(<span class="string">&quot;cf&quot;</span>), Bytes.toBytes(<span class="string">&quot;set&quot;</span>)));</span><br><span class="line">set.add(<span class="string">&quot;d&quot;</span>);  <span class="comment">// 修改</span></span><br><span class="line"><span class="comment">// 再整体写回去...</span></span><br></pre></td></tr></table></figure><p><strong>缺点显而易见</strong>：每次增删改都需要”读-改-写”三个步骤，不是原子操作，并发场景下会丢数据。</p><h3 id="方案-B：一行一个元素"><a href="#方案-B：一行一个元素" class="headerlink" title="方案 B：一行一个元素"></a>方案 B：一行一个元素</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">rowkey     cf:qualifier     value</span><br><span class="line">user_a     cf:element       a</span><br><span class="line">user_b     cf:element       b</span><br><span class="line">user_c     cf:element       c</span><br></pre></td></tr></table></figure><p>每个 Set 元素存成独立的一行，rowkey 设计为 <code>set_id + element</code>。Scan 时用前缀匹配获取整个 Set。</p><p><strong>缺点</strong>：要获取整个 Set 需要 scan 多行，效率比方案 C 低。</p><h3 id="方案-C：Qualifier-方案（推荐）"><a href="#方案-C：Qualifier-方案（推荐）" class="headerlink" title="方案 C：Qualifier 方案（推荐）"></a>方案 C：Qualifier 方案（推荐）</h3><p>将 Set 的元素值存在 <strong>qualifier</strong> 中，value 随便填一个值（如 <code>&quot;1&quot;</code>）：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">rowkey     cf:qualifier    value</span><br><span class="line">set_123    cf:a            1</span><br><span class="line">set_123    cf:b            1</span><br><span class="line">set_123    cf:c            1</span><br></pre></td></tr></table></figure><p><strong>核心优势</strong>：</p><table><thead><tr><th>操作</th><th>HBase 命令</th><th>复杂度</th></tr></thead><tbody><tr><td>添加元素</td><td><code>put.addColumn(cf, &quot;d&quot;, &quot;1&quot;)</code></td><td>O(1)</td></tr><tr><td>删除元素</td><td><code>delete.addColumn(cf, &quot;d&quot;)</code></td><td>O(1)</td></tr><tr><td>判断存在</td><td><code>get.addColumn(cf, &quot;d&quot;)</code> → 判断 result 是否为空</td><td>O(1)</td></tr><tr><td>获取全量</td><td><code>get.addFamily(cf)</code> → 遍历 qualifier</td><td>O(n)</td></tr></tbody></table><p>由于 HBase 的 qualifier 在同一行同一列族下是<strong>唯一的</strong>，天然实现了去重。</p><p>示例代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 添加元素</span></span><br><span class="line"><span class="type">Put</span> <span class="variable">put</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Put</span>(Bytes.toBytes(<span class="string">&quot;set:user:1001&quot;</span>));</span><br><span class="line">put.addColumn(Bytes.toBytes(<span class="string">&quot;tags&quot;</span>), Bytes.toBytes(<span class="string">&quot;vip&quot;</span>), Bytes.toBytes(<span class="string">&quot;1&quot;</span>));</span><br><span class="line">put.addColumn(Bytes.toBytes(<span class="string">&quot;tags&quot;</span>), Bytes.toBytes(<span class="string">&quot;premium&quot;</span>), Bytes.toBytes(<span class="string">&quot;1&quot;</span>));</span><br><span class="line">table.put(put);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 判断元素是否存在</span></span><br><span class="line"><span class="type">Get</span> <span class="variable">get</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Get</span>(Bytes.toBytes(<span class="string">&quot;set:user:1001&quot;</span>));</span><br><span class="line">get.addColumn(Bytes.toBytes(<span class="string">&quot;tags&quot;</span>), Bytes.toBytes(<span class="string">&quot;vip&quot;</span>));</span><br><span class="line"><span class="type">boolean</span> <span class="variable">exists</span> <span class="operator">=</span> table.exists(get);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取全量</span></span><br><span class="line"><span class="type">Get</span> <span class="variable">getAll</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Get</span>(Bytes.toBytes(<span class="string">&quot;set:user:1001&quot;</span>));</span><br><span class="line">getAll.addFamily(Bytes.toBytes(<span class="string">&quot;tags&quot;</span>));</span><br><span class="line"><span class="type">Result</span> <span class="variable">result</span> <span class="operator">=</span> table.get(getAll);</span><br><span class="line">NavigableMap&lt;<span class="type">byte</span>[], <span class="type">byte</span>[]&gt; familyMap = result.getFamilyMap(Bytes.toBytes(<span class="string">&quot;tags&quot;</span>));</span><br><span class="line">Set&lt;String&gt; elements = familyMap.keySet().stream()</span><br><span class="line">    .map(Bytes::toString)</span><br><span class="line">    .collect(Collectors.toSet());</span><br></pre></td></tr></table></figure><h2 id="局限性"><a href="#局限性" class="headerlink" title="局限性"></a>局限性</h2><ul><li><strong>Qualifier 大小限制</strong>：单个 qualifier 不应太大（HBase 是按行存储，qualifier 过大影响性能）</li><li><strong>单行列数上限</strong>：一行中的列数不宜过多（百万级会有性能问题），如果 Set 元素数量非常大，需要考虑分区</li><li><strong>不支持排序</strong>：qualifier 按字典序排列，如果需要有序 Set，需要自定义排序逻辑</li></ul><h2 id="扩展：带权重的-Set"><a href="#扩展：带权重的-Set" class="headerlink" title="扩展：带权重的 Set"></a>扩展：带权重的 Set</h2><p>如果需要类似 Redis Sorted Set 的功能（每个元素有 score），可以把 score 放在 value 中：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">rowkey     cf:qualifier    value</span><br><span class="line">rank       cf:Alice        95.5</span><br><span class="line">rank       cf:Bob          87.0</span><br><span class="line">rank       cf:Carol        92.3</span><br></pre></td></tr></table></figure><p>获取时扫描所有 qualifier，在应用层按 value 排序即可。</p><h2 id="如果你在-HBase-数据建模中遇到过类似的结构化存储需求，这个方案可以作为一个简洁的参考。"><a href="#如果你在-HBase-数据建模中遇到过类似的结构化存储需求，这个方案可以作为一个简洁的参考。" class="headerlink" title="如果你在 HBase 数据建模中遇到过类似的结构化存储需求，这个方案可以作为一个简洁的参考。"></a>如果你在 HBase 数据建模中遇到过类似的结构化存储需求，这个方案可以作为一个简洁的参考。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/46290/</id>
    <link href="https://lichuanyang.top/posts/46290/"/>
    <published>2017-04-10T12:42:00.000Z</published>
    <summary>在 HBase 中实现 Set 数据结构的存储方案，解决 HBase 原生不支持集合类型的限制。</summary>
    <title>一种实现在hbase中存储set的思路</title>
    <updated>2026-06-27T03:44:21.591Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="日志" scheme="https://lichuanyang.top/tags/%E6%97%A5%E5%BF%97/"/>
    <content>
      <![CDATA[<h2 id="完整配置示例"><a href="#完整配置示例" class="headerlink" title="完整配置示例"></a>完整配置示例</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span> encoding=<span class="string">&quot;UTF-8&quot;</span> ?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">configuration</span> <span class="attr">status</span>=<span class="string">&quot;warn&quot;</span>&gt;</span></span><br><span class="line"></span><br><span class="line">    <span class="tag">&lt;<span class="name">Appenders</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Console</span> <span class="attr">name</span>=<span class="string">&quot;Console&quot;</span> <span class="attr">target</span>=<span class="string">&quot;SYSTEM_OUT&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">PatternLayout</span> <span class="attr">pattern</span>=<span class="string">&quot;[%p] %d %c %l - %m%n&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">Console</span>&gt;</span></span><br><span class="line"></span><br><span class="line">        <span class="tag">&lt;<span class="name">RollingFile</span> <span class="attr">name</span>=<span class="string">&quot;activity&quot;</span> <span class="attr">fileName</span>=<span class="string">&quot;/opt/fox.log&quot;</span></span></span><br><span class="line"><span class="tag">                     <span class="attr">filePattern</span>=<span class="string">&quot;/opt/fox.log.%d&#123;yyyy-MM-dd&#125;.gz&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">Policies</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">TimeBasedTriggeringPolicy</span> <span class="attr">interval</span>=<span class="string">&quot;1&quot;</span> <span class="attr">modulate</span>=<span class="string">&quot;true&quot;</span>/&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">Policies</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">PatternLayout</span> <span class="attr">pattern</span>=<span class="string">&quot;[%p] %d - %m%n&quot;</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">RollingFile</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">RollingFile</span> <span class="attr">name</span>=<span class="string">&quot;fox_err&quot;</span> <span class="attr">fileName</span>=<span class="string">&quot;/opt/fox_err.log&quot;</span></span></span><br><span class="line"><span class="tag">                     <span class="attr">filePattern</span>=<span class="string">&quot;/opt/fox_err..log.%d&#123;yyyy-MM-dd&#125;.gz&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">Policies</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">TimeBasedTriggeringPolicy</span> <span class="attr">interval</span>=<span class="string">&quot;1&quot;</span> <span class="attr">modulate</span>=<span class="string">&quot;true&quot;</span>/&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">Policies</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">PatternLayout</span> <span class="attr">pattern</span>=<span class="string">&quot;[%p] %d %l - %m%n&quot;</span> <span class="attr">charset</span>=<span class="string">&quot;UTF-8&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">RollingFile</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">Appenders</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">Loggers</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">logger</span> <span class="attr">name</span>=<span class="string">&quot;com.fox&quot;</span> <span class="attr">additivity</span>=<span class="string">&quot;false&quot;</span> <span class="attr">level</span>=<span class="string">&quot;info&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">appender-ref</span> <span class="attr">ref</span>=<span class="string">&quot;activity&quot;</span> /&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">appender-ref</span> <span class="attr">ref</span>=<span class="string">&quot;activity_err&quot;</span> <span class="attr">level</span>=<span class="string">&quot;error&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">logger</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">Root</span> <span class="attr">level</span>=<span class="string">&quot;error&quot;</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">AppenderRef</span> <span class="attr">ref</span>=<span class="string">&quot;Console&quot;</span>/&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">Root</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">Loggers</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">configuration</span>&gt;</span></span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="与-Log4j-1-x-的主要区别"><a href="#与-Log4j-1-x-的主要区别" class="headerlink" title="与 Log4j 1.x 的主要区别"></a>与 Log4j 1.x 的主要区别</h2><p>和log4j相比，主要有这么一些变化，</p><p>首先整体结构上变化很大，appender、logger都被集中到了各自的一个根节点下。</p><p>xml各节点的名称也采用了新设计，名称直接就是有用信息，不再是之前appender xxx&#x3D;”xxx”, param xxx&#x3D;”xxx”的形式。</p><p>然后一些属性，包括fileName等，只能作为节点属性配置，不能像log4j那样配置成子节点。</p><p>此外，log4j2归档时支持压缩，在RollingFile节点的filePattern属性里将文件名后缀写成gz,zip等压缩格式，log4j2会自动选择相应压缩算法进行压缩。</p><h2 id="引入依赖与迁移"><a href="#引入依赖与迁移" class="headerlink" title="引入依赖与迁移"></a>引入依赖与迁移</h2><h2 id="现在发现的就这些，引入这个xml配置，再引用log4j-core，-log4j-api包，就可以使用log4j2了。此外，如果有需要，可以用log4j-slf4j-impl，log4j-jcl，log4j-1-2-api分别实现对slf4j-jcl-log4j的兼容。"><a href="#现在发现的就这些，引入这个xml配置，再引用log4j-core，-log4j-api包，就可以使用log4j2了。此外，如果有需要，可以用log4j-slf4j-impl，log4j-jcl，log4j-1-2-api分别实现对slf4j-jcl-log4j的兼容。" class="headerlink" title="现在发现的就这些，引入这个xml配置，再引用log4j-core， log4j-api包，就可以使用log4j2了。此外，如果有需要，可以用log4j-slf4j-impl，log4j-jcl，log4j-1.2-api分别实现对slf4j, jcl,log4j的兼容。"></a>现在发现的就这些，引入这个xml配置，再引用log4j-core， log4j-api包，就可以使用log4j2了。此外，如果有需要，可以用log4j-slf4j-impl，log4j-jcl，log4j-1.2-api分别实现对slf4j, jcl,log4j的兼容。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/41673/</id>
    <link href="https://lichuanyang.top/posts/41673/"/>
    <published>2017-04-10T12:23:00.000Z</published>
    <summary>Log4j2 XML 配置实战，对比 Log4j 的主要变化：新节点命名、属性配置方式和归档压缩支持。</summary>
    <title>log4j2.xml配置示例及与log4j的区别</title>
    <updated>2026-06-27T02:15:49.479Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <content>
      <![CDATA[<h2 id="问题的提出"><a href="#问题的提出" class="headerlink" title="问题的提出"></a>问题的提出</h2><p>java里比较两个list的值是否一致，不考虑顺序，有多种方法，比如排序后直接用equals比较，相互之间执行两次containsAll等，这些办法都需要我们给list的元素类实现equals和hashcode方法。但是有一种特殊情况，如果我们并不方便去实习类的equals方法，例如是一个古老的第三方jar包，改代码会带来很多未知问题，这时候该怎么办呢。</p><h2 id="不实现-equals-的比较方案"><a href="#不实现-equals-的比较方案" class="headerlink" title="不实现 equals 的比较方案"></a>不实现 equals 的比较方案</h2><p>其实很简单，万能的apache-commons早就想到了这一点，所以在commons-collections4中增加了外部输入equals和hashcode的方法，甚至equals和hashcode方法本身也不需要我们自己写代码，可以用comons-lang包实现，具体代码如下</p><h2 id="使用第三方工具"><a href="#使用第三方工具" class="headerlink" title="使用第三方工具"></a>使用第三方工具</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;commons-collections4&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;4.1&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;org.apache.commons&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;commons-lang3&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;3.5&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line"></span><br></pre></td></tr></table></figure><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">public static &lt;T&gt; boolean isEqualCollection(Collection&lt;T&gt; l1, Collection&lt;T&gt; l2, final String... exludedFields) &#123;</span><br><span class="line">    Equator&lt;T&gt; equator = generateEquator(exludedFields);</span><br><span class="line">    return CollectionUtils.isEqualCollection(l1, l2, equator);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">private static  &lt;T&gt; Equator&lt;T&gt; generateEquator(final String... exludedFields) &#123;</span><br><span class="line">    Equator&lt;T&gt; equator = new Equator&lt;T&gt;() &#123;</span><br><span class="line">        @Override</span><br><span class="line">        public boolean equate(T o1, T o2) &#123;</span><br><span class="line">            if (o1 == null &amp;&amp; o2 == null) &#123;</span><br><span class="line">                return true;</span><br><span class="line">            &#125;</span><br><span class="line">            if (o1 == null || o2 == null) &#123;</span><br><span class="line">                return false;</span><br><span class="line">            &#125;</span><br><span class="line">            if (o1.getClass() != o2.getClass()) &#123;</span><br><span class="line">                return false;</span><br><span class="line">            &#125;</span><br><span class="line">            return EqualsBuilder.reflectionEquals(o1, o2, exludedFields);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        @Override</span><br><span class="line">        public int hash(T o) &#123;</span><br><span class="line">            return HashCodeBuilder.reflectionHashCode(o, exludedFields);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;;</span><br><span class="line">    return equator;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/28720/</id>
    <link href="https://lichuanyang.top/posts/28720/"/>
    <published>2017-04-10T11:57:00.000Z</published>
    <summary>在不修改第三方类的 equals 方法前提下，利用 Apache Commons 的工具方法比较两个 List 的值。</summary>
    <title>不实现equals方法的情况下比较java list</title>
    <updated>2026-06-27T03:46:06.803Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="数据库" scheme="https://lichuanyang.top/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <category term="redis" scheme="https://lichuanyang.top/tags/redis/"/>
    <category term="lua" scheme="https://lichuanyang.top/tags/lua/"/>
    <content>
      <![CDATA[<p>Redis 的 Hash 操作中，<code>HSETNX</code> 可以保证只在 field 不存在时才写入，但 <code>HMSET</code>（批量设置多个 field）却没有对应的 <code>HMSETNX</code> 命令。如果你需要批量初始化缓存且不覆盖已有数据，每次发 N 个 <code>HSETNX</code> 命令会产生 N 次网络往返，这在批量场景下性能很差。</p><p>用 Lua 脚本可以完美解决这个问题——把逻辑放到 Redis 服务端执行，只需要一次网络往返。</p><span id="more"></span><h2 id="问题背景"><a href="#问题背景" class="headerlink" title="问题背景"></a>问题背景</h2><p>假设你有一个用户配置的缓存场景：新用户首次访问时，需要初始化一组默认配置到 Redis Hash 中。但如果用户已经有了部分配置，不应该覆盖。</p><p>用 <code>HSETNX</code> 逐个设置当然可以，但如果有 20 个配置项，就是 20 次 Redis 往返。在高并发下，这会成为瓶颈。</p><p>而 <code>HMSET</code> 一次可以设多个 field，但它会<strong>无条件覆盖</strong>已有值——这不符合需求。</p><h2 id="Lua-脚本方案"><a href="#Lua-脚本方案" class="headerlink" title="Lua 脚本方案"></a>Lua 脚本方案</h2><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">local</span> key</span><br><span class="line"><span class="keyword">for</span> i, j <span class="keyword">in</span> <span class="built_in">ipairs</span>(ARGV)</span><br><span class="line"><span class="keyword">do</span></span><br><span class="line">    <span class="keyword">if</span> i % <span class="number">2</span> == <span class="number">0</span> <span class="keyword">then</span></span><br><span class="line">        redis.call(<span class="string">&#x27;hsetnx&#x27;</span>, KEYS[<span class="number">1</span>], key, j)</span><br><span class="line">    <span class="keyword">else</span></span><br><span class="line">        key = j</span><br><span class="line">    <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span></span><br></pre></td></tr></table></figure><p><strong>脚本逻辑</strong>：</p><p><code>ARGV</code> 的参数格式与 <code>HMSET</code> 完全一致：<code>[field1, value1, field2, value2, ...]</code>。脚本遍历参数列表，奇数位置（1, 3, 5…）存为 <code>key</code>，偶数位置（2, 4, 6…）执行一次 <code>HSETNX</code>。</p><p>由于 Redis 执行 Lua 脚本是原子的，整个过程不会被其他命令打断。</p><h2 id="Java-调用（Jedis）"><a href="#Java-调用（Jedis）" class="headerlink" title="Java 调用（Jedis）"></a>Java 调用（Jedis）</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 脚本字符串（可以放在常量中）</span></span><br><span class="line"><span class="type">String</span> <span class="variable">HMSETNX_SCRIPT</span> <span class="operator">=</span> </span><br><span class="line">    <span class="string">&quot;local key\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;for i, j in ipairs(ARGV)\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;do\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;    if i % 2 == 0 then\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;        redis.call(&#x27;hsetnx&#x27;, KEYS[1], key, j)\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;    else\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;        key = j\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;    end\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;end\n&quot;</span> +</span><br><span class="line">    <span class="string">&quot;return 1&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 调用</span></span><br><span class="line"><span class="type">Jedis</span> <span class="variable">jedis</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Jedis</span>(<span class="string">&quot;localhost&quot;</span>, <span class="number">6379</span>);</span><br><span class="line">List&lt;String&gt; keys = Collections.singletonList(<span class="string">&quot;user:config:1001&quot;</span>);</span><br><span class="line">List&lt;String&gt; args = Arrays.asList(<span class="string">&quot;theme&quot;</span>, <span class="string">&quot;dark&quot;</span>, <span class="string">&quot;lang&quot;</span>, <span class="string">&quot;zh&quot;</span>, <span class="string">&quot;timezone&quot;</span>, <span class="string">&quot;UTC+8&quot;</span>);</span><br><span class="line"></span><br><span class="line">jedis.eval(HMSETNX_SCRIPT, keys, args);</span><br></pre></td></tr></table></figure><p><strong>参数说明</strong>：</p><ul><li><code>keys</code>：只放一个元素——Hash 表本身的 key</li><li><code>args</code>：按 <code>field1, value1, field2, value2...</code> 的顺序排列</li><li><code>eval()</code> 的两个重载：<code>String</code> 版本和 <code>byte[]</code> 版本，区别是是否需要序列化参数</li></ul><h2 id="性能优化：EVALSHA"><a href="#性能优化：EVALSHA" class="headerlink" title="性能优化：EVALSHA"></a>性能优化：EVALSHA</h2><p>每次 <code>EVAL</code> 都需要传输完整的脚本内容。对于固定脚本，用 <code>EVALSHA</code> 可以大幅减少网络开销：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 计算脚本的 SHA1</span></span><br><span class="line"><span class="type">String</span> <span class="variable">sha</span> <span class="operator">=</span> jedis.scriptLoad(HMSETNX_SCRIPT);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 后续调用直接使用 SHA1</span></span><br><span class="line">jedis.evalsha(sha, keys, args);</span><br></pre></td></tr></table></figure><p><code>EVALSHA</code> 只传输几十字节的 SHA1 值，脚本只在首次 <code>SCRIPT LOAD</code> 时传输一次。</p><h2 id="Hmsetnx-vs-逐条-Hsetnx：性能对比"><a href="#Hmsetnx-vs-逐条-Hsetnx：性能对比" class="headerlink" title="Hmsetnx vs 逐条 Hsetnx：性能对比"></a>Hmsetnx vs 逐条 Hsetnx：性能对比</h2><table><thead><tr><th>方案</th><th>网络往返</th><th>原子性</th><th>复杂度</th></tr></thead><tbody><tr><td>逐条 <code>HSETNX</code></td><td>N 次</td><td>否（非事务）</td><td>最低</td></tr><tr><td>Lua 脚本 <code>EVAL</code></td><td>1 次</td><td>是（Lua 原子执行）</td><td>中</td></tr><tr><td>Lua 脚本 <code>EVALSHA</code></td><td>1 次</td><td>是</td><td>中</td></tr></tbody></table><p>对于 20 个 field 的场景，Lua 方案可以减少 19 次网络往返，在高并发下效果非常显著。</p><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><ul><li><strong>Cluster 模式</strong>：如果 Redis 是 Cluster 模式，需要确保 <code>KEYS[1]</code>（Hash 的 key）落在同一个 slot。可以用 hash tag 来保证（如 <code>{user:1001}:config</code>）</li><li><strong>Big Key</strong>：如果 Hash 中 field 数量巨大，<code>HSETNX</code> 本身时间复杂度是 O(1)，但要注意总 key 大小</li><li><strong>脚本长度</strong>：Redis 对 Lua 脚本的执行有时间限制（<code>lua-time-limit</code>），但这里只是循环执行 O(n) 个 <code>HSETNX</code>，不会触发超时</li></ul><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/63756/</id>
    <link href="https://lichuanyang.top/posts/63756/"/>
    <published>2017-04-06T09:54:00.000Z</published>
    <summary>用 Lua 脚本和 Jedis 实现 Redis 缺失的 hmsetnx 命令，在批量操作 Hash 时不覆盖已有数据。</summary>
    <title>使用lua脚本和jedis实现redis的hmsetnx命令，操作hash表时不覆盖原有数据</title>
    <updated>2026-06-27T03:45:23.484Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="开发环境" scheme="https://lichuanyang.top/tags/%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83/"/>
    <content>
      <![CDATA[<p>为了方便，最近用vitualbox搭了一个centos7的虚拟机，整个过程比较简单，在这里记录一下。</p><h2 id="下载vitualbox"><a href="#下载vitualbox" class="headerlink" title="下载vitualbox"></a>下载vitualbox</h2><p>直接去官网（<a href="https://www.virtualbox.org/wiki/Downloads%EF%BC%89%E4%B8%8B%E8%BD%BD%E5%8D%B3%E5%8F%AF">https://www.virtualbox.org/wiki/Downloads）下载即可</a></p><h2 id="下载centos安装包"><a href="#下载centos安装包" class="headerlink" title="下载centos安装包"></a>下载centos安装包</h2><p>同样官网下载（<a href="https://www.centos.org/download/%EF%BC%89%EF%BC%8C%E6%88%91%E4%B8%8B%E8%BD%BD%E7%9A%84%E6%98%AFminimal">https://www.centos.org/download/），我下载的是minimal</a> iso</p><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><p>安装过程很简单，一路默认点下去就可以，中间内存、分区什么的可以根据需要调一下</p><h2 id="配置本机ssh访问"><a href="#配置本机ssh访问" class="headerlink" title="配置本机ssh访问"></a>配置本机ssh访问</h2><p>vitualbox默认的分辨率非常低，可以通过安装增强工具进行优化。不过由于我们不需要图形化界面，其实可以通过其他方式解决这一问题，就是用xshell或者putty通过ssh远程登陆到虚拟机上。</p><h3 id="打开ssh服务"><a href="#打开ssh服务" class="headerlink" title="打开ssh服务"></a>打开ssh服务</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">service sshd start</span><br><span class="line">chkconfig sshd on</span><br></pre></td></tr></table></figure><p>分别启动ssh服务，并将ssh设定为自启动</p><h3 id="关闭防火墙"><a href="#关闭防火墙" class="headerlink" title="关闭防火墙"></a>关闭防火墙</h3><p>由于只是弄着玩的，直接把防火墙关掉，方便。</p><p>centos7的防火墙操作和之前版本区别很大：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl stop firewalld.service</span><br><span class="line">sudo systemctl disable firewalld.service</span><br></pre></td></tr></table></figure><p>关闭防火墙和自动启动</p><h3 id="配置端口转发"><a href="#配置端口转发" class="headerlink" title="配置端口转发"></a>配置端口转发</h3><p>在VitualBox下配置端口转发：设置-网络-高级-端口转发，将22端口转发到主机的端口上，可以同样是22，也可以配置成其他端口.</p><p>如果需要在主机上访问虚拟机的其他端口，例如tomcat的8080，activemq的61616，8161,也可以在这儿一并配了。</p><h3 id="查看虚拟机ip"><a href="#查看虚拟机ip" class="headerlink" title="查看虚拟机ip"></a>查看虚拟机ip</h3><p>在主机上执行ipconfig,找sudo systemctl disable firewalld.service对应的ip, 然后就可以在xshell中配置对应的ip和端口，访问虚拟机了。</p><h2 id="配置自启动服务"><a href="#配置自启动服务" class="headerlink" title="配置自启动服务"></a>配置自启动服务</h2><p>centos下配置自启动的方式很多，我们在这里提供一种最简单的方式。</p><h3 id="写一个脚本"><a href="#写一个脚本" class="headerlink" title="写一个脚本"></a>写一个脚本</h3><p>例如vim &#x2F;opt&#x2F;app&#x2F;service.sh</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">#!/bin/bash</span><br><span class="line">export JAVA_HOME=/opt/app/jdk1.8.0_121</span><br><span class="line">sh /opt/app/apache-activemq-5.14.4/bin/activemq start &gt;/opt/app/start.log</span><br></pre></td></tr></table></figure><p>把需要自启动的脚本全都放这儿，以后想增加自启动服务的时候，也只需要操作这个脚本。</p><h3 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h3><p>chmod +x &#x2F;opt&#x2F;app&#x2F;service.sh</p><p>centos7下&#x2F;etc&#x2F;rc.d&#x2F;rc.local也需要自己去加执行权限：</p><p>chmod +x &#x2F;etc&#x2F;rc.d&#x2F;rc.local</p><p>然后打开&#x2F;etc&#x2F;rc.d&#x2F;rc.local，在最后把自己写的脚本加上：</p><p>&#x2F;opt&#x2F;app&#x2F;service.sh</p><p>保存，就完成自启动服务的配置了。</p><h2 id="之后，我们可以通过vitualbox的无界面方式启动，然后在xshell中自由操作。"><a href="#之后，我们可以通过vitualbox的无界面方式启动，然后在xshell中自由操作。" class="headerlink" title="之后，我们可以通过vitualbox的无界面方式启动，然后在xshell中自由操作。"></a>之后，我们可以通过vitualbox的无界面方式启动，然后在xshell中自由操作。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/62625/</id>
    <link href="https://lichuanyang.top/posts/62625/"/>
    <published>2017-04-02T04:45:00.000Z</published>
    <summary>VirtualBox 安装 CentOS 7 的完整教程，包括 SSH 访问配置和虚拟机自启动设置。</summary>
    <title>vitualbox虚拟机安装centos 7 及ssh访问、自启动等配置</title>
    <updated>2026-06-08T07:50:49.062Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <category term="监控" scheme="https://lichuanyang.top/tags/%E7%9B%91%E6%8E%A7/"/>
    <category term="hawtio" scheme="https://lichuanyang.top/tags/hawtio/"/>
    <content>
      <![CDATA[<p>hawtio(hawt.io)是一个开源的监控系统，它提供了多种启动方式，可以运行单独的jar包、war包，然后远程连接其他应用进行监控，也可以将它直接嵌到我们自己的应用中。</p><p>本文会介绍在一个单独的java进程(java standalone application)中嵌入hawtio，对应官方文档（<a href="http://hawt.io/getstarted/index.html%EF%BC%89%E7%9A%84">http://hawt.io/getstarted/index.html）的</a> “Using hawtio inside a stand alone Java application”,不过这一节文档问题是比较多的，如果你只看这段，会遇到各种问题。</p><p>下面介绍具体步骤</p><h2 id="引入jar包"><a href="#引入jar包" class="headerlink" title="引入jar包"></a>引入jar包</h2><p>除了官方文档里说的hawtio-embedded外，hawtio-insight-log，hawtio-core，hawtio-web这几个包都是必需的，我们都引入当前的最新版本.</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;io.hawt&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;hawtio-embedded&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;2.0.0&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;io.hawt&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;hawtio-insight-log&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;1.5.0&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;io.hawt&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;hawtio-core&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;2.0.0&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br><span class="line">&lt;dependency&gt;</span><br><span class="line">    &lt;groupId&gt;io.hawt&lt;/groupId&gt;</span><br><span class="line">    &lt;artifactId&gt;hawtio-web&lt;/artifactId&gt;</span><br><span class="line">    &lt;version&gt;2.0.0&lt;/version&gt;</span><br><span class="line">&lt;/dependency&gt;</span><br></pre></td></tr></table></figure><h2 id="下载war包"><a href="#下载war包" class="headerlink" title="下载war包"></a>下载war包</h2><p>同样在官方start文档（<a href="http://hawt.io/getstarted/index.html%EF%BC%89%E4%B8%AD%E4%B8%8B%E8%BD%BDhawtio-default.war%E5%8C%85%EF%BC%8C%E6%94%BE%E5%88%B0%E4%BB%BB%E6%84%8F%E4%BD%8D%E7%BD%AE%EF%BC%8Cwar%E5%8C%85%E7%9A%84%E5%90%8D%E5%AD%97%E4%B9%9F%E5%8F%AF%E4%BB%A5%E9%9A%8F%E4%BE%BF%E6%94%B9">http://hawt.io/getstarted/index.html）中下载hawtio-default.war包，放到任意位置，war包的名字也可以随便改</a></p><h2 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h2><p>基本的代码，如果不需要其他配置，非常简单</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Main main = new Main();</span><br><span class="line">main.setWar(&quot;hawtio.war&quot;);</span><br><span class="line">main.run();</span><br></pre></td></tr></table></figure><p>使用main.setWar配置的是war包具体路径，可以在工程内，也可以在工程外，但是并非官方文档所说的“包含war包的目录路径(somePathOrDirectoryContainingHawtioWar)”<br>之后运行main.run就可以启动hawtio了.</p><h2 id="权限"><a href="#权限" class="headerlink" title="权限"></a>权限</h2><p>新版本的hawtio默认是要密码的，如果想简单，可以配置一条jvm参数： -Dhawtio.authenticationEnabled&#x3D;false， 关掉权限验证。</p><h2 id="示例代码可以在github-https-github-com-lcy362-CamelDemo-blob-master-src-main-java-com-mallow-demo-camel-MainWithHawtio-java-上看，可以直接下载运行，不过需要在本地启动一个activemq-这里的例子是一个使用hawtio监控apache-camel的简单例子，启动后，在hawtio页面上栏可以直接看到camel的标签，使用非常方便。"><a href="#示例代码可以在github-https-github-com-lcy362-CamelDemo-blob-master-src-main-java-com-mallow-demo-camel-MainWithHawtio-java-上看，可以直接下载运行，不过需要在本地启动一个activemq-这里的例子是一个使用hawtio监控apache-camel的简单例子，启动后，在hawtio页面上栏可以直接看到camel的标签，使用非常方便。" class="headerlink" title="示例代码可以在github (https://github.com/lcy362/CamelDemo/blob/master/src/main/java/com/mallow/demo/camel/MainWithHawtio.java) 上看，可以直接下载运行，不过需要在本地启动一个activemq. 这里的例子是一个使用hawtio监控apache-camel的简单例子，启动后，在hawtio页面上栏可以直接看到camel的标签，使用非常方便。"></a>示例代码可以在github (<a href="https://github.com/lcy362/CamelDemo/blob/master/src/main/java/com/mallow/demo/camel/MainWithHawtio.java">https://github.com/lcy362/CamelDemo/blob/master/src/main/java/com/mallow/demo/camel/MainWithHawtio.java</a>) 上看，可以直接下载运行，不过需要在本地启动一个activemq. 这里的例子是一个使用hawtio监控apache-camel的简单例子，启动后，在hawtio页面上栏可以直接看到camel的标签，使用非常方便。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/11299/</id>
    <link href="https://lichuanyang.top/posts/11299/"/>
    <published>2017-04-01T11:42:00.000Z</published>
    <summary>在 Java 独立应用中嵌入 Hawtio 监控系统的完整指南，实现应用内直接查看 JMX 等监控数据。</summary>
    <title>在java独立进程(standalone app)中嵌入hawtio监控</title>
    <updated>2026-06-27T03:46:06.804Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <category term="日志" scheme="https://lichuanyang.top/tags/%E6%97%A5%E5%BF%97/"/>
    <content>
      <![CDATA[<p>Java 的日志框架生态可能是所有编程语言中最复杂的——不是因为它功能多复杂，而是历史原因导致了多套体系的并存和交叉。本文从一个真实的线上排查案例出发，理清 Java 日志框架的体系结构。</p><span id="more"></span><h2 id="Java-日志体系架构"><a href="#Java-日志体系架构" class="headerlink" title="Java 日志体系架构"></a>Java 日志体系架构</h2><p>目前 Java 下最主流的两套日志体系是 <strong>Log4j</strong> 和 <strong>SLF4J + Logback</strong>。</p><h3 id="日志门面-vs-日志实现"><a href="#日志门面-vs-日志实现" class="headerlink" title="日志门面 vs 日志实现"></a>日志门面 vs 日志实现</h3><p>理解这个区分是搞懂 Java 日志生态的关键：</p><table><thead><tr><th>类型</th><th>角色</th><th>例子</th></tr></thead><tbody><tr><td><strong>日志门面（Facade）</strong></td><td>只定义接口，不提供具体实现</td><td>SLF4J、Commons Logging（JCL）</td></tr><tr><td><strong>日志实现</strong></td><td>真正的日志写入逻辑</td><td>Logback、Log4j 1.x、Log4j 2、java.util.logging（JUL）</td></tr></tbody></table><p><strong>SLF4J + Logback</strong> 是典型的门面+实现分离架构：SLF4J 只包含接口（<code>LoggerFactory</code>、<code>Logger</code>），Logback 是具体实现。分离的好处是代码只依赖 SLF4J 接口，实现可以随时替换。</p><p><strong>Log4j 1.x</strong> 则同时包含接口和实现——代码直接用 <code>org.apache.log4j.Logger</code>。</p><h3 id="桥接器"><a href="#桥接器" class="headerlink" title="桥接器"></a>桥接器</h3><p>正是因为有两套体系，才出现了桥接器（Bridge）——让一套体系的接口打出的日志能被另一套体系的实现处理：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SLF4J 接口 ──→ slf4j-log4j12 ──→ Log4j 实现    （用 Log4j 输出 SLF4J 日志）</span><br><span class="line">Log4j 接口 ──→ log4j-over-slf4j ──→ SLF4J → Logback  （用 Logback 输出 Log4j 日志）</span><br></pre></td></tr></table></figure><p><strong>重要</strong>：<code>slf4j-log4j12</code> 和 <code>log4j-over-slf4j</code> 方向相反，不能同时使用，否则会造成无限循环。</p><h2 id="一个真实的排查故事"><a href="#一个真实的排查故事" class="headerlink" title="一个真实的排查故事"></a>一个真实的排查故事</h2><h3 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a>问题现象</h3><p>部署在 Tomcat 上的一个 Web 应用，磁盘空间持续下降，日志文件异常庞大。我们明明在 war 包里把 Log4j 的日志级别配成了 <code>INFO</code>，但 Tomcat 的 <code>catalina.out</code> 里却在疯狂打印 <code>DEBUG</code> 级别的日志。</p><h3 id="排查过程"><a href="#排查过程" class="headerlink" title="排查过程"></a>排查过程</h3><ol><li>起初以为是 Tomcat 配置问题，花了不少时间调整 Tomcat 日志级别，无果</li><li>磁盘告急，只好先 <code>jstack</code> 看看哪些线程在跑，发现大量线程阻塞在 <strong>Logback 的写日志代码</strong>上</li><li>这个线索完全出乎意料——我们根本没配置 Logback！</li></ol><h3 id="真相"><a href="#真相" class="headerlink" title="真相"></a>真相</h3><p>逐一梳理 war 包的依赖关系后，破案了：</p><ol><li>业务代码使用 <strong>SLF4J 接口</strong>写日志</li><li>war 包里同时存在 <strong>logback-core</strong>、<strong>log4j</strong>、<strong>slf4j-log4j12</strong> 三个包</li><li>Classpath 中有 Logback 和 Log4j 两个 SLF4J 的实现——SLF4J 会随机（或者说”不确定地”）选择其中一个</li><li>这次它选了 <strong>Logback</strong></li><li>我们只配了 Log4j 的配置文件（<code>log4j.xml</code>），Logback 找不到配置，按默认行为——<strong>所有级别都输出</strong></li><li>于是磁盘被 DEBUG 日志撑爆了</li></ol><h3 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h3><p>把 Logback 相关的 jar 包从 war 包里全部移除。看似粗暴，但这其实是最合理的做法——当你已经确定使用一套日志实现时，就应该清除另一套，避免 SLF4J 的自动绑定逻辑给你”惊喜”。</p><p>后来我们用 Maven 的 <code>dependency:tree</code> 命令找到了引入 Logback 的间接依赖，加上 <code>&lt;exclusion&gt;</code> 一劳永逸。</p><h2 id="日志问题排查通用套路"><a href="#日志问题排查通用套路" class="headerlink" title="日志问题排查通用套路"></a>日志问题排查通用套路</h2><p>下次再遇到类似的日志诡异问题，按这个三步走：</p><p><strong>1. 找到”肇事”日志</strong></p><p>在巨大的日志文件里找几行不符合你配置的日志，用 <code>grep</code> 找到具体是哪行代码打印的：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">grep -n <span class="string">&quot;某个独特关键词&quot;</span> catalina.out</span><br></pre></td></tr></table></figure><p>然后去代码里确认：这个日志是用哪个接口写的？SLF4J 的 <code>Logger</code>？还是 Log4j 的 <code>Logger</code>？</p><p><strong>2. 查 jar 包依赖树</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mvn dependency:tree | grep -E <span class="string">&quot;log4j|logback|slf4j|commons-logging|jul&quot;</span></span><br></pre></td></tr></table></figure><p>重点关注：有没有<strong>多套日志实现</strong>同时出现在 classpath 中？有没有<strong>桥接器方向搞反</strong>？</p><p><strong>3. 清除多余的实现</strong></p><p>只保留一套日志实现。如果需要兼容旧代码，用桥接器将旧接口路由到新实现：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 想用 SLF4J + Logback，把 Log4j 接口也桥接过来 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.slf4j<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>log4j-over-slf4j<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br><span class="line"><span class="comment">&lt;!-- 排除原版 log4j，避免冲突 --&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">exclusion</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>log4j<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>log4j<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">exclusion</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="2026-年的推荐组合"><a href="#2026-年的推荐组合" class="headerlink" title="2026 年的推荐组合"></a>2026 年的推荐组合</h2><p>当前推荐的 Java 日志架构：</p><ul><li><strong>门面</strong>：SLF4J（事实标准）</li><li><strong>实现</strong>：Log4j 2（异步性能优于 Logback，且已修复 Log4j 1.x 的安全漏洞）</li><li><strong>规范</strong>：代码中只使用 <code>org.slf4j.Logger</code>，不要直接依赖任何具体日志实现</li></ul><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">dependency</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">groupId</span>&gt;</span>org.apache.logging.log4j<span class="tag">&lt;/<span class="name">groupId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">artifactId</span>&gt;</span>log4j-slf4j2-impl<span class="tag">&lt;/<span class="name">artifactId</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">version</span>&gt;</span>2.23.1<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependency</span>&gt;</span></span><br></pre></td></tr></table></figure><p>如果你的项目还是旧的 Log4j 1.x，建议趁早升级——不仅是性能和安全问题，Log4j 1.x 已经 EOL 多年了。</p><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-SLF4J-如何选择日志实现？有多个实现共存时会怎样？"><a href="#Q-SLF4J-如何选择日志实现？有多个实现共存时会怎样？" class="headerlink" title="Q: SLF4J 如何选择日志实现？有多个实现共存时会怎样？"></a>Q: SLF4J 如何选择日志实现？有多个实现共存时会怎样？</h3><p>SLF4J 在 classpath 中发现多个实现（如 Logback + Log4j）时，会<strong>不确定地</strong>选择其中一个。如果选到的实现缺少对应的配置文件（比如选了 Logback 但只配了 <code>log4j.xml</code>），就会使用默认行为——所有日志级别全部输出，这正是文中的磁盘被 DEBUG 日志撑爆的根本原因。因此必须保证 classpath 中<strong>有且只有一套</strong>日志实现。</p><h3 id="Q-桥接器方向搞反了会有什么后果？slf4j-log4j12-和-log4j-over-slf4j-能同时用吗？"><a href="#Q-桥接器方向搞反了会有什么后果？slf4j-log4j12-和-log4j-over-slf4j-能同时用吗？" class="headerlink" title="Q: 桥接器方向搞反了会有什么后果？slf4j-log4j12 和 log4j-over-slf4j 能同时用吗？"></a>Q: 桥接器方向搞反了会有什么后果？slf4j-log4j12 和 log4j-over-slf4j 能同时用吗？</h3><p>绝对不能同时用。<code>slf4j-log4j12</code> 是把 SLF4J 日志交给 Log4j 处理，<code>log4j-over-slf4j</code> 是把 Log4j 日志交给 SLF4J（进而交给 Logback 或 Log4j 2）处理。两者同时存在会造成 A → B → A → B 这样的<strong>无限循环</strong>。正确做法是选定一个方向后，用 Maven <code>&lt;exclusion&gt;</code> 彻底排除另一个。</p><h3 id="Q-2026-年推荐的-Java-日志组合是什么？Log4j-2-相比-Logback-有什么优势？"><a href="#Q-2026-年推荐的-Java-日志组合是什么？Log4j-2-相比-Logback-有什么优势？" class="headerlink" title="Q: 2026 年推荐的 Java 日志组合是什么？Log4j 2 相比 Logback 有什么优势？"></a>Q: 2026 年推荐的 Java 日志组合是什么？Log4j 2 相比 Logback 有什么优势？</h3><p>推荐组合：<strong>SLF4J（门面）+ Log4j 2（实现）</strong>。Log4j 2 的优势在于：1) 异步日志性能优于 Logback；2) 已修复 Log4j 1.x 的安全漏洞（Log4Shell 等 CVE）；3) Log4j 1.x 已 EOL 多年，不再有安全补丁。代码中使用 <code>org.slf4j.Logger</code>，实现可随时切换。</p><h3 id="Q-排查日志不按预期输出时，最有效的三步走是什么？"><a href="#Q-排查日志不按预期输出时，最有效的三步走是什么？" class="headerlink" title="Q: 排查日志不按预期输出时，最有效的三步走是什么？"></a>Q: 排查日志不按预期输出时，最有效的三步走是什么？</h3><ol><li><strong>找肇事日志</strong>：从大量日志中用 <code>grep</code> 捞几行不符合你配置的日志，确认代码用的是哪个日志接口；2) <strong>查 jar 包依赖树</strong>：<code>mvn dependency:tree | grep -E &quot;log4j|logback|slf4j&quot;</code>，找多套实现共存或桥接器方向搞反的问题；3) <strong>清除多余实现</strong>：只保留一套，旧接口用桥接器单向路由到新实现。</li></ol><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/4433/</id>
    <link href="https://lichuanyang.top/posts/4433/"/>
    <published>2017-03-31T11:47:00.000Z</published>
    <summary>从 Tomcat debug 日志问题切入，介绍 Java 两大日志体系（log4j 和 slf4j+logback）的架构和互操作。</summary>
    <title>java日志系统简介: 从tomcat大量打印debug日志说起</title>
    <updated>2026-06-27T03:43:25.706Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <category term="序列化， kryo" scheme="https://lichuanyang.top/tags/%E5%BA%8F%E5%88%97%E5%8C%96%EF%BC%8C-kryo/"/>
    <category term="protobuf" scheme="https://lichuanyang.top/tags/protobuf/"/>
    <category term="json" scheme="https://lichuanyang.top/tags/json/"/>
    <content>
      <![CDATA[<div class="markdown_views"><h2 id="为什么需要序列化方案对比"><a href="#为什么需要序列化方案对比" class="headerlink" title="为什么需要序列化方案对比"></a>为什么需要序列化方案对比</h2><p>对于一个java object的序列化，想测一下使用json和使用一般序列化工具，在时间性能、空间性能上的区别。</p><p>json选择用fastjson.</p><p>序列化工具使用了protostuff和kyro. 为什么不用protobuf呢？因为感觉对于一个已有的上百个属性的java class来说，再去新建一个匹配的proto文件有点反人类。protostuff是protobuf的改良版本，可以直接将一个java object进行序列化，使用方法与kyro有点类似，没有protobuf那么多中间过程。其他的，hession, java自带序列化之类的，据说性能比kryo和protobuf差很多，就不测了。</p><h2 id="时间性能对比"><a href="#时间性能对比" class="headerlink" title="时间性能对比"></a>时间性能对比</h2><p>简单测了一下，发现差距还挺明显的，所以感觉也不需要做具体的评测了。把日志截一段发出来，大家感受下。</p><pre><code>fastjson serilise cost &lt;span class=&quot;hljs-number&quot;&gt;555805&lt;/span&gt;  &lt;span class=&quot;hljs-built_in&quot;&gt;length&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;1740&lt;/span&gt;kyro serilise cost &lt;span class=&quot;hljs-number&quot;&gt;227375&lt;/span&gt;   length502protostuff serilise cost &lt;span class=&quot;hljs-number&quot;&gt;78950&lt;/span&gt;   length633fastjson deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;130662&lt;/span&gt;kyro deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;201716&lt;/span&gt;protostuff deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;230533&lt;/span&gt;fastjson serilise cost &lt;span class=&quot;hljs-number&quot;&gt;727915&lt;/span&gt;  &lt;span class=&quot;hljs-built_in&quot;&gt;length&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;1740&lt;/span&gt;kyro serilise cost &lt;span class=&quot;hljs-number&quot;&gt;378958&lt;/span&gt;   length502protostuff serilise cost &lt;span class=&quot;hljs-number&quot;&gt;94739&lt;/span&gt;   length633fastjson deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;154346&lt;/span&gt;kyro deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;373432&lt;/span&gt;protostuff deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;219085&lt;/span&gt;fastjson serilise cost &lt;span class=&quot;hljs-number&quot;&gt;804892&lt;/span&gt;  &lt;span class=&quot;hljs-built_in&quot;&gt;length&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;1740&lt;/span&gt;kyro serilise cost &lt;span class=&quot;hljs-number&quot;&gt;392380&lt;/span&gt;   length502protostuff serilise cost &lt;span class=&quot;hljs-number&quot;&gt;220664&lt;/span&gt;   length633fastjson deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;243560&lt;/span&gt;kyro deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;360010&lt;/span&gt;protostuff deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;132241&lt;/span&gt;fastjson serilise cost &lt;span class=&quot;hljs-number&quot;&gt;601991&lt;/span&gt;  &lt;span class=&quot;hljs-built_in&quot;&gt;length&lt;/span&gt;: &lt;span class=&quot;hljs-number&quot;&gt;1740&lt;/span&gt;kyro serilise cost &lt;span class=&quot;hljs-number&quot;&gt;244349&lt;/span&gt;   length502protostuff serilise cost &lt;span class=&quot;hljs-number&quot;&gt;80924&lt;/span&gt;   length633fastjson deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;241191&lt;/span&gt;kyro deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;230928&lt;/span&gt;protostuff deserilise cost &lt;span class=&quot;hljs-number&quot;&gt;127109&lt;/span&gt;</code></pre><p>cost的时间用的是System.nanoTime(); 三种用的都是不加任何配置的默认配置。 </p><h2 id="空间性能对比"><a href="#空间性能对比" class="headerlink" title="空间性能对比"></a>空间性能对比</h2><p>序列化之后的占用空间，kryo略低于protostuff, 两者都远高于json. 这是很好理解的，毕竟json串是可读的，不要强求太多。 </p><p>而序列化和反序列化的耗时，都是protostuff优于kyro优于fastjson, 而且差别挺明显。</p><h2 id="序列化方案选择建议"><a href="#序列化方案选择建议" class="headerlink" title="序列化方案选择建议"></a>序列化方案选择建议</h2><p>所以结论呢，如果对空间没有极其苛刻的要求，protostuff也许是最佳选择。protostuff相比于kyro还有一个额外的好处，就是如果序列化之后，反序列化之前这段时间内，java class增加了字段（这在实际业务中是无法避免的事情），kyro就废了。但是protostuff只要保证新字段添加在类的最后，而且用的是sun系列的JDK, 是可以正常使用的。因此，如果序列化是用在缓存等场景下，序列化对象需要存储很久，也就只能选择protostuff了。</p><p>当然，如果有可读性之类的需求，就只能用json了。</p></div><p>&nbsp;</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/57802/</id>
    <link href="https://lichuanyang.top/posts/57802/"/>
    <published>2017-03-02T10:10:00.000Z</published>
    <summary>对比 Java 主流序列化方案（kryo、protobuf、protostuff、fastjson）的时间性能和空间性能，通过实测数据帮你选择最合适的序列化工具。</summary>
    <title>Java kryo/protobuf/protostuff序列化 or Json 性能对比</title>
    <updated>2026-06-27T02:15:49.470Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <category term="redis" scheme="https://lichuanyang.top/tags/redis/"/>
    <category term="zookeeper" scheme="https://lichuanyang.top/tags/zookeeper/"/>
    <category term="分布式" scheme="https://lichuanyang.top/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    <content>
      <![CDATA[<p>这篇文章只讲使用，不讲原理，简单粗暴。</p><p>分布式锁，顾名思义，就是分布式的锁，应用于一些分布式系统中。例如，有一个服务部在数太机器上，然后有可能操作数据库中的同一条记录。这时，就需要分布式锁。</p><p>分布式锁实现的方式很多，一般来说需要一个实体来代表一个锁，占用锁时就新建这个实体，锁释放时也对应将相应实体删除。同时，一般还需要一个锁超时过期的策略，避免一些异常情况造成锁无法被释放。</p><p>zookeeper和redis都是常用的实现分布式锁的方式。接下来就简单介绍一下这两种方式的使用。</p><h2 id="基于zookeeper的分布式锁"><a href="#基于zookeeper的分布式锁" class="headerlink" title="基于zookeeper的分布式锁"></a>基于zookeeper的分布式锁</h2><p>使用zookeeper的话，建议直接使用curator客户端.</p><pre><code>        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-title&quot;&gt;dependency&lt;/span&gt;&amp;gt;&lt;/span&gt;            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-title&quot;&gt;groupId&lt;/span&gt;&amp;gt;&lt;/span&gt;org.apache.curator&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-title&quot;&gt;groupId&lt;/span&gt;&amp;gt;&lt;/span&gt;            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-title&quot;&gt;artifactId&lt;/span&gt;&amp;gt;&lt;/span&gt;curator-recipes&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-title&quot;&gt;artifactId&lt;/span&gt;&amp;gt;&lt;/span&gt;            &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;&lt;span class=&quot;hljs-title&quot;&gt;version&lt;/span&gt;&amp;gt;&lt;/span&gt;2.9.1&lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-title&quot;&gt;version&lt;/span&gt;&amp;gt;&lt;/span&gt;        &lt;span class=&quot;hljs-tag&quot;&gt;&amp;lt;/&lt;span class=&quot;hljs-title&quot;&gt;dependency&lt;/span&gt;&amp;gt;&lt;/span&gt;`&lt;/pre&gt;curator中实现了一个InterProcessSemaphoreMutex类，用作分布式锁。实现原理其实也很直白：建立锁的时候约定一个路径新建一个节点，作为锁的实体；锁释放时就将这个节点删除。代码例子片段:&lt;pre class=&quot;prettyprint&quot;&gt;`InterProcessSemaphoreMutex lock;lock.acquire(&amp;hellip;);  &lt;span class=&quot;hljs-comment&quot;&gt;//acquire获取锁&lt;/span&gt;&amp;hellip;.lock.release();释放锁</code></pre><p>详细的使用例子代码可以参考 <a href="https://github.com/lcy362/Scenes/blob/master/src/main/java/com/mallow/concurrent/zklock/InterProcessMutexExample.java">https://github.com/lcy362/Scenes/blob/master/src/main/java/com/mallow/concurrent/zklock/InterProcessMutexExample.java</a></p><h2 id="基于redis的分布式锁"><a href="#基于redis的分布式锁" class="headerlink" title="基于redis的分布式锁"></a>基于redis的分布式锁</h2><p>同样推荐一个第三方的redis客户端redisson, <a href="https://github.com/redisson/redisson">https://github.com/redisson/redisson</a>. redisson的知名度不如curator高，但也是一个非常优秀的开源工具，支持各种集群、数据结构。</p><p>redis锁的原理就是占用锁时新建一个key, 锁释放时key删除。</p><p>代码示例可以参考<a href="https://github.com/lcy362/Scenes/blob/master/src/main/java/com/mallow/concurrent/redislock/ValuelockExample.java">https://github.com/lcy362/Scenes/blob/master/src/main/java/com/mallow/concurrent/redislock/ValuelockExample.java</a></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/48104/</id>
    <link href="https://lichuanyang.top/posts/48104/"/>
    <published>2017-02-20T11:21:00.000Z</published>
    <summary>Java 分布式锁的实战入门，介绍基于 Zookeeper 和 Redis 的分布式锁实现方式，只讲使用不讲原理。</summary>
    <title>java分布式锁入门实战</title>
    <updated>2026-06-27T03:45:23.487Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="算法" scheme="https://lichuanyang.top/categories/%E7%AE%97%E6%B3%95/"/>
    <category term="算法" scheme="https://lichuanyang.top/tags/%E7%AE%97%E6%B3%95/"/>
    <category term="leetcode" scheme="https://lichuanyang.top/tags/leetcode/"/>
    <content>
      <![CDATA[<h2 id="题目"><a href="#题目" class="headerlink" title="题目"></a>题目</h2><p>Given a string, find the length of the longest substring without repeating characters.</p><p>Examples:</p><p>Given &ldquo;abcabcbb&rdquo;, the answer is &ldquo;abc&rdquo;, which the length is 3.</p><p>Given &ldquo;bbbbb&rdquo;, the answer is &ldquo;b&rdquo;, with the length of 1.</p><p>Given &ldquo;pwwkew&rdquo;, the answer is &ldquo;wke&rdquo;, with the length of 3. Note that the answer must be a substring, &ldquo;pwke&rdquo; is a subsequence and not a substring.</p><p>也就是说给定一个字符串，输出不包含重复字母的最长子串长度。</p><h2 id="思路"><a href="#思路" class="headerlink" title="思路"></a>思路</h2><p>遍历一次字符串，O(n)复杂度下可以解决。主要思路就是在遍历的过程中 </p><p>1. 记录每个字母上一次出现的位置 </p><p>2.  维持一个从当前位置往前数不包含重复字母的子串，记录这个字串的起止位置start, end</p><p>遍历的过程中就是根据相应位置字母是否出现过，以及上次出现的位置，不断更新start, end的过程。</p><h2 id="代码"><a href="#代码" class="headerlink" title="代码"></a>代码</h2><p>可以到github上查看: <a href="https://github.com/lcy362/Algorithms/tree/master/src/main/java/com/mallow/algorithm">https://github.com/lcy362/Algorithms/tree/master/src/main/java/com/mallow/algorithm</a></p><pre><code>&lt;span class=&quot;hljs-keyword&quot;&gt;import&lt;/span&gt; java.util.HashMap;&lt;span class=&quot;hljs-javadoc&quot;&gt;/** * leetcode 3 * https://leetcode.com/problems/longest-substring-without-repeating-characters/ * Created by lcy on 2017/2/15. */&lt;/span&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-class&quot;&gt;&lt;span class=&quot;hljs-keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;LongestSubstringNotRepeat&lt;/span&gt; {&lt;/span&gt;    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;lengthOfLongestSubstring&lt;/span&gt;(String s) {        &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (s.length() &amp;lt;= &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;) {            &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; s.length();        }        HashMap&amp;lt;Character, Integer&amp;gt; charPos = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; HashMap&amp;lt;&amp;gt;();        &lt;span class=&quot;hljs-keyword&quot;&gt;char&lt;/span&gt;[] chars = s.toCharArray();        &lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; len = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;        &lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; max = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;        &lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; start = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;        &lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; end = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;;        &lt;span class=&quot;hljs-keyword&quot;&gt;for&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; i = &lt;span class=&quot;hljs-number&quot;&gt;0&lt;/span&gt;; i &amp;lt; chars.length; i++) {            &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (charPos.containsKey(chars[i])) {                &lt;span class=&quot;hljs-keyword&quot;&gt;int&lt;/span&gt; tempstart = charPos.get(chars[i]) + &lt;span class=&quot;hljs-number&quot;&gt;1&lt;/span&gt;;                &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (tempstart &amp;gt; start) {                    start = tempstart;                }                end++;                len = end - start;            } &lt;span class=&quot;hljs-keyword&quot;&gt;else&lt;/span&gt; {                len++;                end++;            }            charPos.put(chars[i], i);            &lt;span class=&quot;hljs-keyword&quot;&gt;if&lt;/span&gt; (len &amp;gt; max) {                max = len;            }        }        &lt;span class=&quot;hljs-keyword&quot;&gt;return&lt;/span&gt; max;    }    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;static&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;main&lt;/span&gt;(String args[]) {        LongestSubstringNotRepeat l = &lt;span class=&quot;hljs-keyword&quot;&gt;new&lt;/span&gt; LongestSubstringNotRepeat();        System.out.println(l.lengthOfLongestSubstring(&lt;span class=&quot;hljs-string&quot;&gt;&quot;abcabcbb&quot;&lt;/span&gt;));        System.out.println(l.lengthOfLongestSubstring(&lt;span class=&quot;hljs-string&quot;&gt;&quot;bbbbb&quot;&lt;/span&gt;));        System.out.println(l.lengthOfLongestSubstring(&lt;span class=&quot;hljs-string&quot;&gt;&quot;pwwkew&quot;&lt;/span&gt;));        System.out.println(l.lengthOfLongestSubstring(&lt;span class=&quot;hljs-string&quot;&gt;&quot;abba&quot;&lt;/span&gt;));    }</code></pre><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/43423/</id>
    <link href="https://lichuanyang.top/posts/43423/"/>
    <published>2017-02-15T12:34:00.000Z</published>
    <summary>LeetCode 第三题详解：滑动窗口法求解不含重复字符的最长子串长度。</summary>
    <title>leetcode第三题: 输出不包含重复字母的最长子串</title>
    <updated>2026-06-27T03:43:25.700Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <category term="多线程" scheme="https://lichuanyang.top/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    <content>
      <![CDATA[<h2 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h2><p>主要还是通过一个例子加深一下对java多线程里wait,notify的理解，因此写了一个例子，三个线程分别输出A,B,C三个字母，控制这三个线程的执行顺序，从而实现ABCABCABC..这样的输出。</p><h2 id="方案一：synchronized-wait-notify"><a href="#方案一：synchronized-wait-notify" class="headerlink" title="方案一：synchronized + wait&#x2F;notify"></a>方案一：synchronized + wait&#x2F;notify</h2><p>这个问题主要还是需要设计一下锁的策略，这里只是提供了一种方式：</p><p>每个线程占用两把锁，分别代表自己(self)和前一个线程（prev）, 三个线程的持有锁情况如下表所示：</p><table><thead><tr><th>线程号</th><th>prev锁</th><th>self锁</th></tr></thead><tbody><tr><td>A</td><td>c</td><td>a</td></tr><tr><td>B</td><td>a</td><td>b</td></tr><tr><td>C</td><td>b</td><td>c</td></tr></tbody></table><p>A 首先启动，持有ac, 运行后先释放a, b可以执行。</p><p>线程run方法代码如下：</p><pre><code>    &lt;span class=&quot;hljs-keyword&quot;&gt;public&lt;/span&gt; &lt;span class=&quot;hljs-keyword&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;hljs-title&quot;&gt;run&lt;/span&gt;() {        &lt;span class=&quot;hljs-keyword&quot;&gt;while&lt;/span&gt; (&lt;span class=&quot;hljs-keyword&quot;&gt;true&lt;/span&gt;) {            &lt;span class=&quot;hljs-keyword&quot;&gt;synchronized&lt;/span&gt; (prev) {                &lt;span class=&quot;hljs-keyword&quot;&gt;synchronized&lt;/span&gt; (self) {                    System.out.print(name);                    self.notify();                }                &lt;span class=&quot;hljs-keyword&quot;&gt;try&lt;/span&gt; {                    &lt;span class=&quot;hljs-comment&quot;&gt;//如果想控制输出速度， 可以将sleep加在此处&lt;/span&gt;                    &lt;span class=&quot;hljs-comment&quot;&gt;//如果加在sout之后，会导致c线程启动并占有b锁之后,a线程才会释放a锁，输出顺序会变成acbacb&lt;/span&gt;                    &lt;span class=&quot;hljs-comment&quot;&gt;//也可以加大三个线程启动的间隔时间解决这一问题&lt;/span&gt;                    &lt;span class=&quot;hljs-keyword&quot;&gt;try&lt;/span&gt; {                        Thread.sleep(&lt;span class=&quot;hljs-number&quot;&gt;1000&lt;/span&gt;);                    } &lt;span class=&quot;hljs-keyword&quot;&gt;catch&lt;/span&gt; (InterruptedException e) {                        e.printStackTrace();                    }                    prev.wait();                } &lt;span class=&quot;hljs-keyword&quot;&gt;catch&lt;/span&gt; (InterruptedException e) {                    e.printStackTrace();                }            }        }</code></pre><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><p>需要注意的是，如果想控制输出速度，需要考虑一下sleep的位置和时间，避免在A线程执行完并释放a锁之前，C线程已经启动并持有了B锁，导致B线程无法正常启动。</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/60353/</id>
    <link href="https://lichuanyang.top/posts/60353/"/>
    <published>2017-01-25T08:59:00.000Z</published>
    <summary>通过多线程顺序输出 ABC 的经典例子，加深对 Java wait/notify 机制的理解，展示锁策略的设计思路。</summary>
    <title>java多线程实现三个字母顺序输出</title>
    <updated>2026-06-27T03:46:06.800Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="消息队列" scheme="https://lichuanyang.top/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    <category term="kafka" scheme="https://lichuanyang.top/tags/kafka/"/>
    <category term="成长" scheme="https://lichuanyang.top/tags/%E6%88%90%E9%95%BF/"/>
    <content>
      <![CDATA[<h2 id="问题过程"><a href="#问题过程" class="headerlink" title="问题过程"></a>问题过程</h2><p>我司需要接收很多外部数据，数据源的形式很多，ibmmq, activemq, redis pubsub, 等等都有。为了将这些数据接到内部amq&#x2F;kafka，之前运行了一大批进程，管理起来十分复杂，因此最近用apache-camel对这些进程作了整合。</p><p>上线几个小时之后，kafka磁盘空间开始报警。初步断定是这次上线导致的。</p><h2 id="排查流程"><a href="#排查流程" class="headerlink" title="排查流程"></a>排查流程</h2><p>主要还是对kafka不熟悉，只是能用而已，因此排查过程走了不少弯路。</p><p>由于camel本身文档很不完善，一开始配置参数时也主要靠看源码+猜，所以首先怀疑配置的压缩参数compressionCodec&#x3D;gzip是否无效。</p><p>因此进行了一次简单的测试（别问我为什么一开始不测）。建了一个单分区的测试topic, 然后分别使用gzip和none模式发送相同的数据，观察kafka的log file文件大小变化趋势。发现压缩确实是有效的。</p><p>因此又跟直接使用kafka api作了对比，发现差别非常大，即使是压缩之后，使用camel-kafka占用的空间也比使用api大数倍。然后去查了两边源码，发现最底层的代码是完全一致的，所以还是怀疑某些参数配的不对。</p><p>之后又注意到一个细节，使用camel时，log的大小跟消息数呈线性关系，比如一条占1字节，10条就占10字节。但使用api的话，1条也占1字节，连续发10条可能才占两字节。</p><p>这时候就怀疑kafka是不是有批量发送之类的机制，然后咨询了负责kafka的同事，果然是这个原因，而造成占用空间差别大的原因就是是否同步发的区别，同步发的话就不存在批量发送了。批量发送的话，这一批消息会被压缩在一起，而单条发时，就是每一条分别压缩。我们知道，在文件非常小的时候，使用gzip压缩的效果是很差的，甚至可能压完比源文件还大。然后又做了些测试，确定了是这个问题。这个参数被同事封装在了kafka的client接口里，因此导致我照着之前代码改参数时漏掉了这一个。</p><h2 id="一些感悟"><a href="#一些感悟" class="headerlink" title="一些感悟"></a>一些感悟</h2><p>其实回想起来，这个问题挺low的，如果对kafka多些了解，是不会有这种问题的。</p><p>首先，对kafka没做过全面的了解，只是学会了怎么用，大概了解了一下它是什么。而很多基本的机制都没有概念。使用一个开源工具时，对它做一次全面的了解还是很重要的，虽然每个工具都深入研究底层代码不现实，但是系统性的了解一遍这些工具有什么机制，确实花不了多少时间。</p><p>再一个，公司内一般会封装一些访问各种组件的工具包，以提升效率，这些工具包最好也了解一下怎么实现的，否则可能不经意间就掉坑里了。</p><p>任重而道远啊..</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/40931/</id>
    <link href="https://lichuanyang.top/posts/40931/"/>
    <published>2016-12-29T09:21:00.000Z</published>
    <summary>一次 Kafka 磁盘空间激增问题的排查过程，深入理解 Kafka 数据压缩机制和批量发送策略。</summary>
    <title>一次kafka空间激增排查：kafka的数据压缩、批量发送等</title>
    <updated>2026-06-08T07:50:49.061Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="Java" scheme="https://lichuanyang.top/categories/Java/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <category term="java integer" scheme="https://lichuanyang.top/tags/java-integer/"/>
    <category term="javaInteger缓存" scheme="https://lichuanyang.top/tags/javaInteger%E7%BC%93%E5%AD%98/"/>
    <content>
      <![CDATA[<p>Java 的 <code>Integer</code> 缓存机制是一个高频面试题，也是日常开发中容易踩坑的地方。理解它，首先从一个看起来违反直觉的实验开始。</p><span id="more"></span><h2 id="诡异的-128"><a href="#诡异的-128" class="headerlink" title="诡异的 128"></a>诡异的 128</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Integer</span> <span class="variable">a</span> <span class="operator">=</span> <span class="number">148</span>;</span><br><span class="line"><span class="type">Integer</span> <span class="variable">b</span> <span class="operator">=</span> <span class="number">148</span>;</span><br><span class="line">System.out.println(a == b);  <span class="comment">// false —— 符合预期，两个不同对象</span></span><br><span class="line"></span><br><span class="line"><span class="type">Integer</span> <span class="variable">c</span> <span class="operator">=</span> <span class="number">48</span>;</span><br><span class="line"><span class="type">Integer</span> <span class="variable">d</span> <span class="operator">=</span> <span class="number">48</span>;</span><br><span class="line">System.out.println(c == d);  <span class="comment">// true —— 等等，为什么？</span></span><br></pre></td></tr></table></figure><p>同一个 <code>==</code> 比较，148 返回 false，48 返回 true。原因就在于 <strong>Integer 缓存</strong>。</p><h2 id="IntegerCache-源码分析"><a href="#IntegerCache-源码分析" class="headerlink" title="IntegerCache 源码分析"></a>IntegerCache 源码分析</h2><p>当 Java 编译 <code>Integer a = 48</code> 时，实际上执行的是：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Integer</span> <span class="variable">a</span> <span class="operator">=</span> Integer.valueOf(<span class="number">48</span>);</span><br></pre></td></tr></table></figure><p>来看 <code>Integer.valueOf()</code> 的源码（JDK 8）：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> Integer <span class="title function_">valueOf</span><span class="params">(<span class="type">int</span> i)</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (i &gt;= IntegerCache.low &amp;&amp; i &lt;= IntegerCache.high)</span><br><span class="line">        <span class="keyword">return</span> IntegerCache.cache[i + (-IntegerCache.low)];</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Integer</span>(i);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>核心逻辑：如果参数值在缓存范围内（默认 -128 ~ 127），直接从缓存数组中返回同一个对象；超出范围才 <code>new Integer(i)</code>。</p><p>这就是为什么 <code>48</code> 在缓存范围内所以 <code>==</code> 为 true，而 <code>148</code> 超出范围每次 <code>new</code>，所以 <code>==</code> 为 false。</p><p><code>IntegerCache</code> 是一个私有的静态内部类：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">IntegerCache</span> &#123;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">low</span> <span class="operator">=</span> -<span class="number">128</span>;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> high;</span><br><span class="line">    <span class="keyword">static</span> <span class="keyword">final</span> Integer cache[];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        <span class="comment">// high 可以通过 JVM 参数配置</span></span><br><span class="line">        <span class="type">int</span> <span class="variable">h</span> <span class="operator">=</span> <span class="number">127</span>;</span><br><span class="line">        <span class="type">String</span> <span class="variable">integerCacheHighPropValue</span> <span class="operator">=</span></span><br><span class="line">            sun.misc.VM.getSavedProperty(<span class="string">&quot;java.lang.Integer.IntegerCache.high&quot;</span>);</span><br><span class="line">        <span class="keyword">if</span> (integerCacheHighPropValue != <span class="literal">null</span>) &#123;</span><br><span class="line">            h = Math.min(Integer.parseInt(integerCacheHighPropValue), Integer.MAX_VALUE - <span class="number">129</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        high = h;</span><br><span class="line"></span><br><span class="line">        cache = <span class="keyword">new</span> <span class="title class_">Integer</span>[(high - low) + <span class="number">1</span>];</span><br><span class="line">        <span class="type">int</span> <span class="variable">j</span> <span class="operator">=</span> low;</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> <span class="variable">k</span> <span class="operator">=</span> <span class="number">0</span>; k &lt; cache.length; k++)</span><br><span class="line">            cache[k] = <span class="keyword">new</span> <span class="title class_">Integer</span>(j++);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><strong>你可以通过 JVM 参数调整缓存上限</strong>：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">java -Djava.lang.Integer.IntegerCache.high=500 MyApp</span><br></pre></td></tr></table></figure><h2 id="其他包装类的缓存"><a href="#其他包装类的缓存" class="headerlink" title="其他包装类的缓存"></a>其他包装类的缓存</h2><table><thead><tr><th>包装类</th><th>缓存范围</th><th>备注</th></tr></thead><tbody><tr><td><code>Boolean</code></td><td><code>TRUE</code> &#x2F; <code>FALSE</code></td><td>只有两个值</td></tr><tr><td><code>Byte</code></td><td>-128 ~ 127</td><td>全部缓存</td></tr><tr><td><code>Short</code></td><td>-128 ~ 127</td><td>同 Integer</td></tr><tr><td><code>Long</code></td><td>-128 ~ 127</td><td>同 Integer</td></tr><tr><td><code>Character</code></td><td>0 ~ 127</td><td>ASCII 范围</td></tr><tr><td><code>Float</code></td><td><strong>无</strong></td><td>浮点数缓存意义不大</td></tr><tr><td><code>Double</code></td><td><strong>无</strong></td><td>同 Float</td></tr></tbody></table><h2 id="三个常见踩坑场景"><a href="#三个常见踩坑场景" class="headerlink" title="三个常见踩坑场景"></a>三个常见踩坑场景</h2><h3 id="1-HashMap-的-Key"><a href="#1-HashMap-的-Key" class="headerlink" title="1. HashMap 的 Key"></a>1. HashMap 的 Key</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Map&lt;Integer, String&gt; map = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line">map.put(<span class="number">128</span>, <span class="string">&quot;hello&quot;</span>);</span><br><span class="line">map.get(<span class="number">128</span>);  <span class="comment">// &quot;hello&quot; —— 即使 == 不等，equals 依然相等，所以 HashMap 正常工作</span></span><br></pre></td></tr></table></figure><p>HashMap 用 <code>equals()</code> 和 <code>hashCode()</code>，不受 <code>==</code> 影响。但如果你用了 <code>IdentityHashMap</code>，缓存边界就成了一个隐形的 bug 来源。</p><h3 id="2-for-循环中的-Integer-比较"><a href="#2-for-循环中的-Integer-比较" class="headerlink" title="2. for 循环中的 Integer 比较"></a>2. for 循环中的 Integer 比较</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ 写法</span></span><br><span class="line"><span class="keyword">for</span> (<span class="type">Integer</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">200</span>; i++) &#123;</span><br><span class="line">    <span class="comment">// i == 某个 Integer 时……可能出问题</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ✅ 写法</span></span><br><span class="line"><span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i &lt; <span class="number">200</span>; i++) &#123;</span><br><span class="line">    <span class="comment">// 用基本类型，干净利落</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-锁对象"><a href="#3-锁对象" class="headerlink" title="3. 锁对象"></a>3. 锁对象</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ 危险！128 以下的值每次返回同一个对象，等于用的是同一个锁</span></span><br><span class="line"><span class="keyword">synchronized</span> (Integer.valueOf(<span class="number">10</span>)) &#123;</span><br><span class="line">    <span class="comment">// 如果你不知道缓存机制，可能以为每次都是新锁</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="为什么缓存范围是-128-127？"><a href="#为什么缓存范围是-128-127？" class="headerlink" title="为什么缓存范围是 -128 ~ 127？"></a>为什么缓存范围是 -128 ~ 127？</h2><p>这是一个字节的有符号表示范围。统计上，这个区间覆盖了日常编程中最常使用的整数值（循环计数器、数组索引、小常量等）。《Java Language Specification》§5.1.7 明确规定了这个行为——这不是实现细节，是有规范保障的。</p><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><ol><li><strong>比较 Integer 值时始终用 <code>equals()</code></strong>，不要用 <code>!=</code></li><li><strong>能用 <code>int</code> 的地方不要用 <code>Integer</code></strong>——基本类型没有缓存陷阱，性能也更好</li><li><strong>永远不要用 Integer 做锁对象</strong></li></ol><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/64970/</id>
    <link href="https://lichuanyang.top/posts/64970/"/>
    <published>2016-12-02T11:45:00.000Z</published>
    <summary>揭秘 Java Integer 类在 128 这个魔力数字背后的缓存机制，理解自动装箱的底层原理。</summary>
    <title>java里128有何魔力？ 聊聊Integer的缓存</title>
    <updated>2026-06-27T02:15:49.472Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="大数据" scheme="https://lichuanyang.top/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <category term="activemq" scheme="https://lichuanyang.top/tags/activemq/"/>
    <category term="jstorm" scheme="https://lichuanyang.top/tags/jstorm/"/>
    <category term="storm" scheme="https://lichuanyang.top/tags/storm/"/>
    <category term="kafka" scheme="https://lichuanyang.top/tags/kafka/"/>
    <content>
      <![CDATA[<p>storm的周边生态非常丰富，与kafka,activemq,hdfs,hbase等的交互都有现成的工具包可以使用。大部分工具，包括今天介绍的这几个，在jstorm中也可以完全正常的使用。</p><h2 id="storm-jms"><a href="#storm-jms" class="headerlink" title="storm-jms"></a>storm-jms</h2><p>实现了与activemq等jms实现的交互。</p><p>这里主要介绍JmsSpout。由于storm中发送队列数据与普通java程序没有任何区别，专门封装一个bolt显得有些多此一举。</p><p><a href="https://github.com/ptgoetz/storm-jms">https://github.com/ptgoetz/storm-jms</a></p><p>包中自带了使用spring方式加载队列配置。</p><h2 id="使用示例"><a href="#使用示例" class="headerlink" title="使用示例"></a>使用示例</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">JmsProvider</span> <span class="variable">jmsQueueProvider</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SpringJmsProvider</span>(</span><br><span class="line">        <span class="string">&quot;jms-activemq.xml&quot;</span>, <span class="string">&quot;jmsConnectionFactory&quot;</span>,</span><br><span class="line">        <span class="string">&quot;TEST_QUEUE&quot;</span>);</span><br><span class="line"><span class="type">JmsTupleProducer</span> <span class="variable">producer</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JsonTupleProducer</span>();</span><br><span class="line"></span><br><span class="line"><span class="comment">// JMS Queue Spout</span></span><br><span class="line"><span class="type">JmsSpout</span> <span class="variable">queueSpout</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">JmsSpout</span>();</span><br><span class="line">queueSpout.setJmsProvider(jmsQueueProvider);</span><br><span class="line">queueSpout.setJmsTupleProducer(producer);</span><br><span class="line">queueSpout.setJmsAcknowledgeMode(Session.CLIENT_ACKNOWLEDGE);</span><br><span class="line">queueSpout.setDistributed(<span class="literal">true</span>);</span><br><span class="line"><span class="type">TopologyBuilder</span> <span class="variable">builder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">TopologyBuilder</span>();</span><br><span class="line"></span><br><span class="line">builder.setSpout(<span class="string">&quot;jms&quot;</span>, queueSpout, <span class="number">30</span>);</span><br></pre></td></tr></table></figure><h2 id="配置文件示例"><a href="#配置文件示例" class="headerlink" title="配置文件示例"></a>配置文件示例</h2><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?xml version=<span class="string">&quot;1.0&quot;</span>?&gt;</span></span><br><span class="line"><span class="comment">&lt;!--</span></span><br><span class="line"><span class="comment"> Licensed to the Apache Software Foundation (ASF) under one or more</span></span><br><span class="line"><span class="comment"> contributor license agreements.  See the NOTICE file distributed with</span></span><br><span class="line"><span class="comment"> this work for additional information regarding copyright ownership.</span></span><br><span class="line"><span class="comment"> The ASF licenses this file to You under the Apache License, Version 2.0</span></span><br><span class="line"><span class="comment"> (the &quot;License&quot;); you may not use this file except in compliance with</span></span><br><span class="line"><span class="comment"> the License.  You may obtain a copy of the License at</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">     http://www.apache.org/licenses/LICENSE-2.0</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment"> Unless required by applicable law or agreed to in writing, software</span></span><br><span class="line"><span class="comment"> distributed under the License is distributed on an &quot;AS IS&quot; BASIS,</span></span><br><span class="line"><span class="comment"> WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.</span></span><br><span class="line"><span class="comment"> See the License for the specific language governing permissions and</span></span><br><span class="line"><span class="comment"> limitations under the License.</span></span><br><span class="line"><span class="comment">--&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="name">beans</span> </span></span><br><span class="line"><span class="tag">  <span class="attr">xmlns</span>=<span class="string">&quot;http://www.springframework.org/schema/beans&quot;</span> </span></span><br><span class="line"><span class="tag">  <span class="attr">xmlns:amq</span>=<span class="string">&quot;http://activemq.apache.org/schema/core&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">xmlns:xsi</span>=<span class="string">&quot;http://www.w3.org/2001/XMLSchema-instance&quot;</span></span></span><br><span class="line"><span class="tag">  <span class="attr">xsi:schemaLocation</span>=<span class="string">&quot;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd</span></span></span><br><span class="line"><span class="string"><span class="tag">  http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd&quot;</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">amq:queue</span> <span class="attr">id</span>=<span class="string">&quot;detrUpdateQueue&quot;</span> <span class="attr">physicalName</span>=<span class="string">&quot;TEST_QUEUE&quot;</span> /&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">amq:connectionFactory</span> <span class="attr">id</span>=<span class="string">&quot;jmsConnectionFactory&quot;</span></span></span><br><span class="line"><span class="tag"><span class="attr">brokerURL</span>=<span class="string">&quot;failover:(tcp://xxx:61616)?jms.prefetchPolicy.queuePrefetch=10&quot;</span> /&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;/<span class="name">beans</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="使用说明"><a href="#使用说明" class="headerlink" title="使用说明"></a>使用说明</h2><p>新建一个xml配置，<a href="amq:queue">amq:queue</a> 节点配置队列名。</p><p><a href="amq:connectionFactory">amq:connectionFactory</a>节点配置activemq的连接url。</p><p>代码中调用</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">JmsProvider</span> <span class="variable">jmsQueueProvider</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SpringJmsProvider</span>(</span><br><span class="line">                <span class="string">&quot;jms-activemq.xml&quot;</span>, <span class="string">&quot;jmsConnectionFactory&quot;</span>,</span><br><span class="line">                <span class="string">&quot;TEST_QUEUE&quot;</span>);</span><br></pre></td></tr></table></figure><p>三个参数依次为配置文件名, connectionFactory和queue节点的名称。</p><h2 id="自定义输出"><a href="#自定义输出" class="headerlink" title="自定义输出"></a>自定义输出</h2><p>通过实现JmsTupleProducer可以实现个性化的输出。</p><p>以JsonTupleProducer 为例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JsonTupleProducer</span> <span class="keyword">implements</span> <span class="title class_">JmsTupleProducer</span> &#123;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Values <span class="title function_">toTuple</span><span class="params">(Message msg)</span> <span class="keyword">throws</span> JMSException &#123;</span><br><span class="line"><span class="keyword">if</span>(msg <span class="keyword">instanceof</span> TextMessage)&#123;</span><br><span class="line"><span class="type">String</span> <span class="variable">json</span> <span class="operator">=</span> ((TextMessage) msg).getText();</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Values</span>(json);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">declareOutputFields</span><span class="params">(OutputFieldsDeclarer declarer)</span> &#123;</span><br><span class="line">declarer.declare(<span class="keyword">new</span> <span class="title class_">Fields</span>(<span class="string">&quot;json&quot;</span>));</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>即将整条消息作为一个字段emit出去。</p><p>declareOutputFields和new Values 即storm中的对应函数。</p><p>通过在toTuple中做相应处理，可以实现定制化的输出。</p><h3 id="关闭ack"><a href="#关闭ack" class="headerlink" title="关闭ack"></a>关闭ack</h3><p>除了将ack数设为0外，conf.setNumAckers(0);</p><p>还需要将jms的确认模式修改为自动ack:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">queueSpout.setJmsAcknowledgeMode(Session.AUTO_ACKNOWLEDGE);</span><br></pre></td></tr></table></figure><h3 id="自定义jms连接器"><a href="#自定义jms连接器" class="headerlink" title="自定义jms连接器"></a>自定义jms连接器</h3><p>JmsProvider类是JmsSpout中用于建立到队列的连接所用的类。</p><p>默认的SpringJmsProvider是通过spring配置来获取jms连接。</p><p>如果有特殊需求，也可以自己实现JmsProvider中的两个接口，用于获取连接ConnectionFactory和队列Destination</p><h2 id="storm-kafka"><a href="#storm-kafka" class="headerlink" title="storm-kafka"></a>storm-kafka</h2><p><a href="https://github.com/apache/storm/tree/master/external/storm-kafka">https://github.com/apache/storm/tree/master/external/storm-kafka</a></p><p>从kafka中获取数据。</p><h2 id="使用示例-1"><a href="#使用示例-1" class="headerlink" title="使用示例"></a>使用示例</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">topic</span> <span class="operator">=</span> <span class="string">&quot;&quot;</span>; <span class="comment">//队列名</span></span><br><span class="line"><span class="type">String</span> <span class="variable">zkRoot</span> <span class="operator">=</span> <span class="string">&quot;/kafkastorm&quot;</span>; <span class="comment">//kafka在zookeeper中记录offset的根目录，无需改变</span></span><br><span class="line"><span class="type">String</span> <span class="variable">id</span> <span class="operator">=</span> <span class="string">&quot;&quot;</span>; <span class="comment">// consumer id</span></span><br><span class="line"><span class="type">String</span> <span class="variable">kafkaZk</span> <span class="operator">=</span> <span class="string">&quot;&quot;</span>; <span class="comment">//kafka的zookeeper地址</span></span><br><span class="line"><span class="type">BrokerHosts</span> <span class="variable">brokerHosts</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ZkHosts</span>(kafkaZk);</span><br><span class="line"><span class="type">SpoutConfig</span> <span class="variable">kafkaConfig</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SpoutConfig</span>(brokerHosts, topic, zkRoot, id);</span><br><span class="line">kafkaConfig.scheme = <span class="keyword">new</span> <span class="title class_">SchemeAsMultiScheme</span>(<span class="keyword">new</span> <span class="title class_">StringScheme</span>());</span><br><span class="line">kafkaConfig.ignoreZkOffsets = <span class="literal">true</span>; <span class="comment">//false时使用kafka中存储的offset信息</span></span><br><span class="line">kafkaConfig.startOffsetTime = kafka.api.OffsetRequest.LatestTime(); <span class="comment">//读取数据开始位置,只有ignoreZkOffsets设为true时startOffsetTime才能生效</span></span><br><span class="line">kafkaConfig.zkServers = <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;String&gt;()&#123; &#123;</span><br><span class="line">    add(<span class="string">&quot;1&quot;</span>); <span class="comment">//存储kafkaoffset的zookeeper节点</span></span><br><span class="line">&#125; &#125;;</span><br><span class="line">kafkaConfig.zkPort = <span class="number">2181</span>;</span><br><span class="line"><span class="type">TopologyBuilder</span> <span class="variable">builder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">TopologyBuilder</span>();</span><br><span class="line">builder.setSpout(topic, <span class="keyword">new</span> <span class="title class_">KafkaSpout</span>(kafkaConfig), <span class="number">10</span>); <span class="comment">//并行度不能高于kafka分区数</span></span><br><span class="line">builder.setBolt....</span><br></pre></td></tr></table></figure><h2 id="storm-hdfs"><a href="#storm-hdfs" class="headerlink" title="storm-hdfs"></a>storm-hdfs</h2><pre><code>HdfsBolt支持向hdfs写入数据</code></pre><h2 id="使用示例-2"><a href="#使用示例-2" class="headerlink" title="使用示例"></a>使用示例</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">SyncPolicy</span> <span class="variable">syncPolicy</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CountSyncPolicy</span>(<span class="number">100</span>); <span class="comment">//每100条hdfs同步一次磁盘</span></span><br><span class="line"><span class="type">DailyRotationPolicy</span> <span class="variable">rotationPolicy</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DailyRotationPolicy</span>(); <span class="comment">//文件每天归档</span></span><br><span class="line"></span><br><span class="line"><span class="type">FileFolderByDateNameFormat</span> <span class="variable">fileNameFormat</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">FileFolderByDateNameFormat</span>()</span><br><span class="line">        .withPath(<span class="string">&quot;/user/lcy/jdata&quot;</span>); <span class="comment">//hdfs路径</span></span><br><span class="line"><span class="type">RecordFormat</span> <span class="variable">format</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DelimitedRecordFormat</span>()</span><br><span class="line">        .withFieldDelimiter(<span class="string">&quot;,&quot;</span>); <span class="comment">//不同field之间的分隔符</span></span><br><span class="line"></span><br><span class="line"><span class="type">UmeHdfsBolt</span> <span class="variable">hdfsbolt</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">UmeHdfsBolt</span>()</span><br><span class="line">        .withFsUrl(<span class="string">&quot;hdfs://:8020&quot;</span>) <span class="comment">//hdfs url</span></span><br><span class="line">        .withFileNameFormat(fileNameFormat)</span><br><span class="line">        .withRecordFormat(format)</span><br><span class="line">        .withRotationPolicy(rotationPolicy)</span><br><span class="line">        .withSyncPolicy(syncPolicy);</span><br></pre></td></tr></table></figure><h2 id="示例说明"><a href="#示例说明" class="headerlink" title="示例说明"></a>示例说明</h2><p>按照示例编码，输出到hdfs形式如下：</p><p>bolt接收到的每条数据为hdfs中的一行；</p><p>输出到hdfs路径为 设定的路径&#x2F;日期&#x2F;默认文件名；</p><p>hdfsbolt的每个线程会输出到一个文件，topology重启会产生新文件；</p><p>文件每天归档</p><h2 id="其他功能"><a href="#其他功能" class="headerlink" title="其他功能"></a>其他功能</h2><p>FileSizeRotationPolicy 支持按固定文件大小归档；TimedRotationPolicy支持按固定时间归档；</p><p>如有其他归档方式需求，可以实现FileRotationPolicy接口，参考DailyRotationPolicy源码。</p><p>通过实现FileNameFormat接口自定义文件路径及文件名，参考FileFolderByDateNameFormat</p><h2 id="FileFolderByDateNameFormat"><a href="#FileFolderByDateNameFormat" class="headerlink" title="FileFolderByDateNameFormat"></a>FileFolderByDateNameFormat</h2><p>FileFolderByDateNameFormat实现目前常用的存储路径。<br>使用示例</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">FileFolderByDateNameFormat</span> <span class="variable">fileNameFormat</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">FileFolderByDateNameFormat</span>()</span><br><span class="line">         .withPath(<span class="string">&quot;/user/test&quot;</span>).withName(<span class="string">&quot;&quot;</span>);</span><br></pre></td></tr></table></figure><p>会自动在路径之后增加日期，文件名中也会增加日期。</p><h2 id="源码参考"><a href="#源码参考" class="headerlink" title="源码参考"></a>源码参考</h2><p>除了storm-hdfs本身源码以外，可以参考<a href="https://github.com/lcy362/StormTrooper">https://github.com/lcy362/StormTrooper</a> 查看FileFolderByDateNameFormat和DailyRotationPolicy的实现。</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/22103/</id>
    <link href="https://lichuanyang.top/posts/22103/"/>
    <published>2016-11-16T12:16:00.000Z</published>
    <summary>Storm/JStorm 生态工具全景，介绍如何连接 ActiveMQ、Kafka、HDFS 等外部系统构建完整数据管道。</summary>
    <title>storm/jstorm生态与周边工具,storm连接activemq,kafka,hdfs等</title>
    <updated>2026-06-27T03:42:25.688Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="大数据" scheme="https://lichuanyang.top/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <category term="jstorm" scheme="https://lichuanyang.top/tags/jstorm/"/>
    <category term="storm" scheme="https://lichuanyang.top/tags/storm/"/>
    <content>
      <![CDATA[<h2 id="UI说明"><a href="#UI说明" class="headerlink" title="UI说明"></a>UI说明</h2><p>jstorm的UI相对于storm提供了更为丰富的监控项。UI本身是在tomcat中运行的一个war包，进行二次开发也相对容易。</p><h2 id="cluster页"><a href="#cluster页" class="headerlink" title="cluster页"></a>cluster页</h2><h3 id="Cluster-Summary-Cluster-Stats-Topology-Summary"><a href="#Cluster-Summary-Cluster-Stats-Topology-Summary" class="headerlink" title="Cluster Summary, Cluster Stats, Topology Summary"></a>Cluster Summary, Cluster Stats, Topology Summary</h3><p>cluster的整体信息, conf中是nimbus节点的配置。</p><h3 id="Topology-Summary"><a href="#Topology-Summary" class="headerlink" title="Topology Summary"></a>Topology Summary</h3><p>当前运行的所有topology列表及概要信息，conf中对应的是topology单独配置的配置项。</p><h3 id="Supervisor-Summary"><a href="#Supervisor-Summary" class="headerlink" title="Supervisor Summary"></a>Supervisor Summary</h3><p>所有supervisor列表, 可以查看supervisor的配置、日志等。</p><h2 id="topology页"><a href="#topology页" class="headerlink" title="topology页"></a>topology页</h2><h3 id="Topology-Summary-1"><a href="#Topology-Summary-1" class="headerlink" title="Topology Summary"></a>Topology Summary</h3><p>主要关注Topology Graph 项，这是Component Metrics 页的概要信息，可以较直观的看出运行状况。</p><p>其中，节点大小和箭头粗细代表数据量，节点颜色用于区分spout和bolt。箭头颜色对应TupleLifeCycle属性，由绿到红越来越慢。箭头变红色时，可以适当增加下游节点的并行度。</p><h3 id="Component-Metrics"><a href="#Component-Metrics" class="headerlink" title="Component Metrics"></a>Component Metrics</h3><p>包含每个spout, bolt的统计信息。</p><p>各参数说明:</p><p>tasks : 并行度。对应代码中为每个spout&#x2F;bolt设置的并行度。</p><p>emitted, acked, failed : 发射&#x2F;收到ack&#x2F;未收到ack的消息数。</p><p>sendTps, recvTps: 发送&#x2F;接收数据tps.</p><p>需要注意，以上数据交互类的指标包含了与storm本身组件，topology_master,acker之间的交互，只能用来查看运行状况，<strong>不适合统计用</strong>。</p><p>process: 对于bolt来说，是excute()函数的执行时间，对于spout来说，是整条消息（包含下游各步）被完整处理的时间，体现系统运行效率的主要参数。</p><p>TupleLifeCycle: 从上游消息被emit,到消息处理完所需时间。和process相比相差太大时，说明在bolt队列中等待了较长时间，可以适当增加并发。</p><p>deser, ser: 序列化&#x2F;反序列化耗时，一般无需关注. storm中每一个emit出来的数据都需要做序列化和反序列化。</p><p>error: 鼠标悬浮在E上，可以查看具体的报错信息。<strong>主要寻找带异常堆栈的数据<strong>， 一些诸如is dead, no response之类的信息无需关注。</strong></strong></p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/31996/</id>
    <link href="https://lichuanyang.top/posts/31996/"/>
    <published>2016-11-16T11:58:00.000Z</published>
    <summary>JStorm UI 功能详解，包括 Cluster Summary、Topology Summary 等监控项的说明和二次开发指南。</summary>
    <title>jstorm UI 介绍</title>
    <updated>2026-06-08T07:50:49.072Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="大数据" scheme="https://lichuanyang.top/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <category term="storm" scheme="https://lichuanyang.top/tags/storm/"/>
    <content>
      <![CDATA[<p>Storm（以及阿里巴巴开源的 JStorm）是一个分布式实时计算框架。它的编程模型跟传统 Java 程序有一些关键区别，如果不了解这些区别，很容易写出在本地跑得好好的、部署到集群就出各种诡异 bug 的代码。</p><p>本文不深入 Storm 内核原理，而是聚焦开发者最需要知道的几个关键区别，帮你快速上手并避开常见坑。</p><span id="more"></span><h2 id="Storm-运行机制速览"><a href="#Storm-运行机制速览" class="headerlink" title="Storm 运行机制速览"></a>Storm 运行机制速览</h2><p>一个 Storm Topology 由若干 Spout（数据源）和 Bolt（处理逻辑）组成，它们分散运行在多个 Worker（进程）中。同一个 Worker 可能同时运行多个 Spout&#x2F;Bolt 的多个线程。</p><p>关键要理解的是：<strong>你的代码会被序列化后分发到各个 Worker 上执行，而不是在提交 Topology 的那台机器上运行。</strong></p><h2 id="与传统-Java-程序的三大关键区别"><a href="#与传统-Java-程序的三大关键区别" class="headerlink" title="与传统 Java 程序的三大关键区别"></a>与传统 Java 程序的三大关键区别</h2><h3 id="1-main-方法只跑在-Nimbus-上"><a href="#1-main-方法只跑在-Nimbus-上" class="headerlink" title="1. main 方法只跑在 Nimbus 上"></a>1. main 方法只跑在 Nimbus 上</h3><p><code>main</code> 方法只在提交 Topology 时运行在 Nimbus（Storm 的 master 节点）上。它的作用仅限于构建 Topology 的结构并提交——<strong>实际的 Spout 和 Bolt 代码不在 main 方法所在进程执行</strong>。</p><p>这意味着：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ❌ 这样不会生效！</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> &#123;</span><br><span class="line">    <span class="comment">// Spring 容器在这里初始化，但 Worker 进程完全看不见</span></span><br><span class="line">    <span class="type">ApplicationContext</span> <span class="variable">ctx</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ClassPathXmlApplicationContext</span>(<span class="string">&quot;applicationContext.xml&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="type">TopologyBuilder</span> <span class="variable">builder</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">TopologyBuilder</span>();</span><br><span class="line">    builder.setSpout(<span class="string">&quot;myspout&quot;</span>, <span class="keyword">new</span> <span class="title class_">MySpout</span>());</span><br><span class="line">    builder.setBolt(<span class="string">&quot;mybolt&quot;</span>, <span class="keyword">new</span> <span class="title class_">MyBolt</span>()).shuffleGrouping(<span class="string">&quot;myspout&quot;</span>);</span><br><span class="line">    </span><br><span class="line">    StormSubmitter.submitTopology(<span class="string">&quot;mytopo&quot;</span>, config, builder.createTopology());</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Spring 配置、数据库连接池、加密密钥等，都不能放在 <code>main</code> 中初始化。<code>main</code> 里只放 Storm 本身的配置项（<code>Config</code> 对象）和 Topology 结构构建。</p><h3 id="2-Spout-Bolt-的生命周期"><a href="#2-Spout-Bolt-的生命周期" class="headerlink" title="2. Spout&#x2F;Bolt 的生命周期"></a>2. Spout&#x2F;Bolt 的生命周期</h3><p>每个 Spout 和 Bolt 都有自己的生命周期方法：</p><table><thead><tr><th>组件</th><th>初始化</th><th>每条消息</th><th>退出</th></tr></thead><tbody><tr><td><strong>Spout</strong></td><td><code>open()</code></td><td><code>nextTuple()</code></td><td><code>close()</code></td></tr><tr><td><strong>Bolt</strong></td><td><code>prepare()</code></td><td><code>execute()</code></td><td><code>cleanup()</code></td></tr></tbody></table><p>关键点：</p><ul><li><strong><code>prepare()</code>&#x2F;<code>open()</code> 在 Worker 进程启动时执行一次</strong>——这里才是做初始化的正确位置。Spring 容器、数据库连接、配置加载等，都放在这里。</li><li><strong>多个 Bolt 需要加载 Spring 时，建议使用相同配置</strong>：多个 Bolt 在同一个 Worker 中可能共享 JVM 进程，重复初始化 Spring 容器可能造成资源浪费甚至冲突。</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyBolt</span> <span class="keyword">extends</span> <span class="title class_">BaseRichBolt</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> SomeService service;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">prepare</span><span class="params">(Map stormConf, TopologyContext context, OutputCollector collector)</span> &#123;</span><br><span class="line">        <span class="comment">// ✅ 初始化放在这里</span></span><br><span class="line">        <span class="type">ApplicationContext</span> <span class="variable">ctx</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ClassPathXmlApplicationContext</span>(<span class="string">&quot;applicationContext.xml&quot;</span>);</span><br><span class="line">        <span class="built_in">this</span>.service = ctx.getBean(SomeService.class);</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">execute</span><span class="params">(Tuple input)</span> &#123;</span><br><span class="line">        <span class="comment">// 每条消息处理</span></span><br><span class="line">        service.process(input);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="3-序列化——最常见的坑"><a href="#3-序列化——最常见的坑" class="headerlink" title="3. 序列化——最常见的坑"></a>3. 序列化——最常见的坑</h3><p>由于 Spout 和 Bolt 可能在不同 Worker（甚至不同机器）上运行，<strong>所有通过 Storm 传输的数据都必须可序列化</strong>：</p><ul><li><code>emit()</code> 发出的 Tuple</li><li>Spout&#x2F;Bolt 的成员变量（如果不是在 <code>prepare()</code> 中初始化而是在构造器或静态块中）</li><li>配置中传递的对象</li></ul><p>Storm 默认使用 <strong>Kryo</strong> 序列化，它比 Java 原生序列化更快，但有一些要求：</p><ul><li><strong>类必须有公开的无参构造函数</strong>。如果类没有无参构造（比如某些第三方库的类），会有 <code>IllegalArgumentException</code></li><li><strong>内部类和匿名类默认不支持</strong>，需要手动注册</li></ul><p>如果不方便添加无参构造函数，可以降级：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">Config</span> <span class="variable">config</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Config</span>();</span><br><span class="line">config.setFallBackOnJavaSerialization(<span class="literal">true</span>);  <span class="comment">// 降级为 Java 序列化</span></span><br></pre></td></tr></table></figure><h2 id="一个完整的-WordCount-示例"><a href="#一个完整的-WordCount-示例" class="headerlink" title="一个完整的 WordCount 示例"></a>一个完整的 WordCount 示例</h2><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Spout：不断发射句子</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SentenceSpout</span> <span class="keyword">extends</span> <span class="title class_">BaseRichSpout</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> SpoutOutputCollector collector;</span><br><span class="line">    <span class="keyword">private</span> String[] sentences = &#123;</span><br><span class="line">        <span class="string">&quot;hello world&quot;</span>, <span class="string">&quot;hello storm&quot;</span>, <span class="string">&quot;goodbye world&quot;</span></span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="keyword">private</span> <span class="type">int</span> <span class="variable">index</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">open</span><span class="params">(Map conf, TopologyContext context, SpoutOutputCollector collector)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.collector = collector;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">nextTuple</span><span class="params">()</span> &#123;</span><br><span class="line">        collector.emit(<span class="keyword">new</span> <span class="title class_">Values</span>(sentences[index]));</span><br><span class="line">        index = (index + <span class="number">1</span>) % sentences.length;</span><br><span class="line">        Utils.sleep(<span class="number">1000</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">declareOutputFields</span><span class="params">(OutputFieldsDeclarer declarer)</span> &#123;</span><br><span class="line">        declarer.declare(<span class="keyword">new</span> <span class="title class_">Fields</span>(<span class="string">&quot;sentence&quot;</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Bolt 1：拆分单词</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SplitBolt</span> <span class="keyword">extends</span> <span class="title class_">BaseRichBolt</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> OutputCollector collector;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">prepare</span><span class="params">(Map conf, TopologyContext context, OutputCollector collector)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.collector = collector;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">execute</span><span class="params">(Tuple tuple)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">sentence</span> <span class="operator">=</span> tuple.getStringByField(<span class="string">&quot;sentence&quot;</span>);</span><br><span class="line">        <span class="keyword">for</span> (String word : sentence.split(<span class="string">&quot; &quot;</span>)) &#123;</span><br><span class="line">            collector.emit(<span class="keyword">new</span> <span class="title class_">Values</span>(word));</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">declareOutputFields</span><span class="params">(OutputFieldsDeclarer declarer)</span> &#123;</span><br><span class="line">        declarer.declare(<span class="keyword">new</span> <span class="title class_">Fields</span>(<span class="string">&quot;word&quot;</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Bolt 2：计数</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CountBolt</span> <span class="keyword">extends</span> <span class="title class_">BaseRichBolt</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> OutputCollector collector;</span><br><span class="line">    <span class="keyword">private</span> Map&lt;String, Integer&gt; counts = <span class="keyword">new</span> <span class="title class_">HashMap</span>&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">prepare</span><span class="params">(Map conf, TopologyContext context, OutputCollector collector)</span> &#123;</span><br><span class="line">        <span class="built_in">this</span>.collector = collector;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">execute</span><span class="params">(Tuple tuple)</span> &#123;</span><br><span class="line">        <span class="type">String</span> <span class="variable">word</span> <span class="operator">=</span> tuple.getStringByField(<span class="string">&quot;word&quot;</span>);</span><br><span class="line">        counts.merge(word, <span class="number">1</span>, Integer::sum);</span><br><span class="line">        System.out.println(word + <span class="string">&quot;: &quot;</span> + counts.get(word));</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">declareOutputFields</span><span class="params">(OutputFieldsDeclarer declarer)</span> &#123;&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="2026-年的视角"><a href="#2026-年的视角" class="headerlink" title="2026 年的视角"></a>2026 年的视角</h2><p>Storm 曾经是实时计算的事实标准，但近年来 <strong>Apache Flink</strong> 已经成为流计算的主流选择。Flink 提供了更好的 exactly-once 语义、更丰富的窗口操作和更成熟的状态管理。如果你是新项目选型，我会建议优先考虑 Flink。</p><p>不过，如果你维护的是已有的 Storm&#x2F;JStorm 遗留系统，本文的内容依然能帮你快速定位问题。理解了 Storm 的 Worker 模型和序列化机制，很多”诡异的线上 bug”其实都有迹可循。</p><hr>]]>
    </content>
    <id>https://lichuanyang.top/posts/27021/</id>
    <link href="https://lichuanyang.top/posts/27021/"/>
    <published>2016-11-16T10:21:00.000Z</published>
    <summary>Storm/JStorm 编程快速入门，重点说明 topology 运行时与传统 Java 程序在初始化上的关键区别。</summary>
    <title>五分钟学会写storm代码: jstorm/storm编码原理与普通java程序的区别</title>
    <updated>2026-06-27T03:42:25.689Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="消息队列" scheme="https://lichuanyang.top/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    <category term="activemq" scheme="https://lichuanyang.top/tags/activemq/"/>
    <category term="监控" scheme="https://lichuanyang.top/tags/%E7%9B%91%E6%8E%A7/"/>
    <content>
      <![CDATA[<h2 id="Web-Console-的安全风险"><a href="#Web-Console-的安全风险" class="headerlink" title="Web Console 的安全风险"></a>Web Console 的安全风险</h2><div>activemq的web console是基于jetty实现，其权限管理也是基于jetty. 根据需求，可以给不同的用户赋予不同的权限。jetty的权限管理还算灵活，虽然配起来比较麻烦，可以分别设定某个角色（role）下的用户是否有对某个页面的访问权限。</div><h2 id="JAAS-认证配置"><a href="#JAAS-认证配置" class="headerlink" title="JAAS 认证配置"></a>JAAS 认证配置</h2><div>下面简要介绍一下配置方法，只需要修改/conf 下的&nbsp;jetty.xml，&nbsp;jetty-realm.properties</div><div>1.&nbsp;jetty-realm.properties</div><div>&nbsp; 这里面配置了所有用户的用户名，密码和所属角色，按照如下格式：</div><div>username: password [,rolename ...] &nbsp;&nbsp;</div><div></div>## 角色与权限定义<div>2\. jetty.xml</div><div>首先对每个角色配置一个Constraint 类，其中roles及对应&nbsp;jetty-realm.properties中的rolename</div><div></div><table><tbody><tr><td style="border-style: solid; border-width: 1px; border-color: #d3d3d3; padding: 10px; margin: 0px; width: 100%;"><div>&lt;bean id="securityConstraint" class="org.eclipse.jetty.util.security.Constraint"&gt;</div><div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;property name="name" value="BASIC" /&gt;<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;property name&#x3D;”roles” value&#x3D;”admin” &#x2F;&gt;</p><p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;property name&#x3D;”authenticate” value&#x3D;”true” &#x2F;&gt;</div></p><div>&nbsp;&nbsp;&nbsp; &lt;/bean&gt;</div></td></tr></tbody></table><div></div><div></div><div>然后配置securityConstraintMapping,&nbsp;</div><table><tbody><tr><td style="border-style: solid; border-width: 1px; border-color: #d3d3d3; padding: 10px; margin: 0px; width: 100%;"><div>&nbsp;&nbsp;&nbsp; &lt;bean id="securityConstraintMapping" class="org.eclipse.jetty.security.ConstraintMapping"&gt;</div><div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;property name="constraint" ref="securityConstraint" /&gt;</div><div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;property name="pathSpec" value="/admin/send.jsp/" /&gt;</div><div>&nbsp;&nbsp;&nbsp; &lt;/bean&gt;</div></td></tr></tbody></table><div>这表示securityConstraint类对应的角色可以访问/admin/send.jsp 页面。</div><div>可以使用/* 代表所有<span style="color: #ff0000;">未单独配置的</span>页面</div><div>假设我们需要新建一个只读用户，就可以配置两个角色admin和readonly，这两个角色都需要增加/*的ConstraintMapping 条目，然后在admin上额外配置所有涉及写操作的页面，包括/admin/deleteDestination.action/*，&nbsp;/admin/purgeDestination.action/* 等。</div><div></div><div>最后，在ConstraintSecurityHandler的constraintMappings属性里，把所有的ConstraintMapping都列出来。</div><div></div><table><tbody><tr><td style="border-style: solid; border-width: 1px; border-color: #d3d3d3; padding: 10px; margin: 0px; width: 100%;"><div>&lt;bean id="securityHandler" class="org.eclipse.jetty.security.ConstraintSecurityHandler"&gt;</div><div>&nbsp; &nbsp;&nbsp;&lt;property name="constraintMappings"&gt;<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;list&gt;</div></p><div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;ref bean="securityConstraintMapping" /&gt;</div><div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;/list&gt;</div><div>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; &lt;/property&gt;</div></td></tr><tr><td style="border-style: solid; border-width: 1px; border-color: #d3d3d3; padding: 10px; margin: 0px; width: 100%;">&nbsp;</td></tr></tbody></table><div>这样，就实现了activemq web console用户的权限配置。</div><h2 id="nbsp"><a href="#nbsp" class="headerlink" title="&nbsp;"></a>&nbsp;</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/32479/</id>
    <link href="https://lichuanyang.top/posts/32479/"/>
    <published>2016-01-05T10:09:00.000Z</published>
    <summary>介绍 ActiveMQ Web Console 基于 Jetty 的权限管理配置方法，实现不同用户角色的访问控制。</summary>
    <title>activemq web console的权限配置</title>
    <updated>2026-06-27T03:43:25.704Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="大数据" scheme="https://lichuanyang.top/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <category term="storm" scheme="https://lichuanyang.top/tags/storm/"/>
    <content>
      <![CDATA[<h2 id="Storm-UI-概览"><a href="#Storm-UI-概览" class="headerlink" title="Storm UI 概览"></a>Storm UI 概览</h2><p><span style="font-family: FangSong_GB2312; font-size: 14px;">Storm UI对于排查storm使用过程中遇到的问题会很有帮助，但是有些属性的含义不是很明确，虽然都是很简单的概念，如果不知道的话也会很难受。</span></p><p><span style="font-family: FangSong_GB2312; font-size: 14px;">先说一点，鼠标只到UI上的标题栏时，是可以看到这一属性的具体属性的，几篇google rank很高的文章，其实就是把这个信息整理了下来。</span></p><p><span style="font-family: FangSong_GB2312; font-size: 14px;">其实大部分属性都是很直白的，看到名字就知道是什么意思，我在这儿之把一些可能造成困扰的属性列一下，方便大家查问题。</span></p><p>&nbsp;</p><h2 id="关键指标详解"><a href="#关键指标详解" class="headerlink" title="关键指标详解"></a>关键指标详解</h2><div style="orphans: 2; word-wrap: break-word;"><span style="font-family: FangSong_GB2312; font-size: 14px;">emitted和transfered: emitted，就是发射出的数据条数，也就是调用OutputCollector的emit方法的次数。transferred则是实际tuple发送到下一个task的数目。乍一看是一样的对吗。其实一般情况下也确实是一样的。但是，比如，一个bolt 发射了数据，但是下游并没有其他bolt取这个数，这个bolt的transfer数就会是0\. 又比如，<span style="orphans: 2;">如果一个bolt A使用all group的方式(每一个bolt都要接收到)向bolt B发射tuple，那么transfered就会是emitted的数倍。</span></span></div><div style="orphans: 2; word-wrap: break-word;"><span style="orphans: 2;"><span style="font-family: FangSong_GB2312; font-size: 14px;"><span style="orphans: 2;">excute latency和process latency : excute latency 很直白，就是代码里excute()这个方法的执行时间, 而process latency则是excute方法执行，直到调用ack方法的时间，可以认为是业务代码执行所需的时间，正常情况下，<span style="orphans: 2;">excute latency是会大于<span style="orphans: 2;">process latency的，但是如果你一直不去ack,process latency会远远大于excute latency。</span></span></span><p></span></span></div></p><div style="orphans: 2; word-wrap: break-word;"><span style="orphans: 2;"><span style="orphans: 2;"><span style="font-family: FangSong_GB2312; font-size: 14px;"><span style="orphans: 2;"><span style="orphans: 2;"><span style="color: #333333; line-height: 26px;">spout的complete latency: 这个可以参考storm的[ack机制](http://xumingming.sinaapp.com/127/twitter-storm%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E6%B6%88%E6%81%AF%E4%B8%8D%E4%B8%A2%E5%A4%B1/)&nbsp;，这个时间就是一个tuple从被发射到这个tuple被ack所需的时间，确切的说，是从spout调用emit方法到调用ack方法的时间差，其实也就是这个由这个tuple生成的tuple树被完全处理所需的时间。</span><p></span></span></span></span></span></div></p><h2 id="nbsp"><a href="#nbsp" class="headerlink" title="&nbsp;"></a>&nbsp;</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/62259/</id>
    <link href="https://lichuanyang.top/posts/62259/"/>
    <published>2015-11-06T13:44:00.000Z</published>
    <summary>Storm UI 中关键监控指标的含义解读，帮助运维人员快速定位 topology 性能瓶颈。</summary>
    <title>storm ui 中一些关键属性的含义</title>
    <updated>2026-06-27T03:46:06.801Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="消息队列" scheme="https://lichuanyang.top/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    <category term="activemq" scheme="https://lichuanyang.top/tags/activemq/"/>
    <content>
      <![CDATA[<h2 id="问题现象"><a href="#问题现象" class="headerlink" title="问题现象"></a>问题现象</h2><p>最近在使用activemq 的连接池时，发现它存在很严重的内存泄露问题。</p><h2 id="排查过程"><a href="#排查过程" class="headerlink" title="排查过程"></a>排查过程</h2><p>通过jmap监控，可以看到java.util.concurrent.locks.ReentrantLock,&nbsp;org.apache.activemq.pool.PooledConnection这两个类占用的空间非常大，而且增长速度也很快。</p><h2 id="根因分析"><a href="#根因分析" class="headerlink" title="根因分析"></a>根因分析</h2><p>网上查了一下，正好找到activemq的<a href="https://issues.apache.org/jira/browse/AMQ-3997">bug 报告</a>.:<a href="https://issues.apache.org/jira/browse/AMQ-3997">https://issues.apache.org/jira/browse/AMQ-3997</a></p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><p>这个bug 在5.7中已经修复，可以通过升级版本解决。</p><p>同时，也有另一种解决方式，就是使用spring带的连接池替换activemq自带的连接池，配置如下：</p><pre name="code" class="html">  &lt;bean id="jmsConnectionFactory"                class="org.apache.activemq.ActiveMQConnectionFactory"&gt;                &lt;property name="brokerURL" value="vm://205-amq-broker2?create=false&amp;waitForStart=10000" /&gt;        &lt;/bean&gt;&lt;!--        &lt;bean id="pooledConnectionFactory"                class="org.apache.activemq.pool.PooledConnectionFactory" init-method="start" destroy-method="stop"&gt;                &lt;property name="maxConnections" value="8" /&gt;                &lt;property name="connectionFactory" ref="jmsConnectionFactory" /&gt;        &lt;/bean&gt;--&gt;      &lt;bean id="cachedConnectionFactory"                class="&lt;span style="color:#ff0000;"&gt;org.springframework.jms.connection.CachingConnectionFactory&lt;/span&gt;"&gt;                        &lt;property name="targetConnectionFactory" ref="jmsConnectionFactory"&gt;&lt;/property&gt;                        &lt;property name="sessionCacheSize" value="10"&gt;&lt;/property&gt;        &lt;/bean&gt;        &lt;bean id="jmsConfig"                class="org.apache.camel.component.jms.JmsConfiguration"&gt;                &lt;property name="connectionFactory" ref="cachedConnectionFactory"/&gt;                &lt;property name="concurrentConsumers" value="10"/&gt;        &lt;/bean&gt;        &lt;bean id="activemq"                class="org.apache.activemq.camel.component.ActiveMQComponent"&gt;                &lt;property name="configuration" ref="jmsConfig"/&gt;</pre><h2 id="nbsp"><a href="#nbsp" class="headerlink" title="&nbsp;"></a>&nbsp;</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/13925/</id>
    <link href="https://lichuanyang.top/posts/13925/"/>
    <published>2015-08-08T13:16:00.000Z</published>
    <summary>记录 ActiveMQ 5.6 连接池存在的严重内存泄露问题，通过 jmap 监控定位 ReentrantLock 和 PooledConnection 的异常增长。</summary>
    <title>activemq 5.6 连接池的内存泄露问题</title>
    <updated>2026-06-27T03:45:23.487Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="消息队列" scheme="https://lichuanyang.top/categories/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/"/>
    <category term="java" scheme="https://lichuanyang.top/tags/java/"/>
    <category term="camel" scheme="https://lichuanyang.top/tags/camel/"/>
    <content>
      <![CDATA[<h2 id="测试场景"><a href="#测试场景" class="headerlink" title="测试场景"></a>测试场景</h2><p><span style="font-family: Arial,Helvetica,sans-serif; background-color: #ffffff;">公司使用activemq和camel做消息的分发，之前数据量不是很大，所以一直没怎么考虑效率问题，对camel的工作原理研究也不深。单是最近随着业务量的增加，camel的效率逐渐成了瓶颈，所以根据日志大概了解了camel的工作原理。虽然camel是被嵌入到activemq中，但在工作过程中，camel和activemq其实还是相对独立的。我们在camel中会配置一个到activemq的连接.</span></p><pre name="code" class="html">http://camel.apache.org/activemq.html</pre><p>关于vm这种传输方式，参考<a href="http://activemq.apache.org/vm-transport-reference.html">http://activemq.apache.org/vm-transport-reference.html</a></p><h2 id="瓶颈分析"><a href="#瓶颈分析" class="headerlink" title="瓶颈分析"></a>瓶颈分析</h2><p>看了下日志，发现这种配置下camel会有一个很严重的问题: camel每次执行转发操作时，都会新建一个到activemq的连接，之后再将其关闭。这严重拖慢了转发效率，因为事实上每次转发都可以使用同一个连接。</p><h2 id="优化建议"><a href="#优化建议" class="headerlink" title="优化建议"></a>优化建议</h2><p>因此查了一下camel文档，找到了 <a href="http://camel.apache.org/activemq.html"> http://camel.apache.org/activemq.html</a> 。 里边有关于线程池的配置：</p><p>&nbsp;</p><pre name="code" class="html">&lt;pre name="code" class="html"&gt;&lt;bean id="jmsConnectionFactory"    class="org.apache.activemq.ActiveMQConnectionFactory"&gt;   &lt;property name="brokerURL" value="tcp://localhost:61616" /&gt;&lt;/bean&gt;&lt;bean id="pooledConnectionFactory"    class="org.apache.activemq.pool.PooledConnectionFactory" init-method="start" destroy-method="stop"&gt;   &lt;property name="maxConnections" value="8" /&gt;   &lt;property name="connectionFactory" ref="jmsConnectionFactory" /&gt;&lt;/bean&gt;&lt;bean id="jmsConfig"    class="org.apache.camel.component.jms.JmsConfiguration"&gt;   &lt;property name="connectionFactory" ref="pooledConnectionFactory"/&gt;   &lt;property name="concurrentConsumers" value="10"/&gt;&lt;/bean&gt;&lt;bean id="activemq"     class="org.apache.activemq.camel.component.ActiveMQComponent"&gt;    &lt;property name="configuration" ref="jmsConfig"/&gt;    &lt;!-- if we are using transacted then enable CACHE_CONSUMER (if not using XA) to run faster         see more details at: http://camel.apache.org/jms    &lt;property name="transacted" value="true"/&gt;    &lt;property name="cacheLevelName" value="CACHE_CONSUMER" /&gt;    --&gt;&lt;/bean&gt;</pre><p>这个正好符合我们的需要。而且顺便把连接换成了多线程，可以进一步提升效率。</p><p>&nbsp;</p><p>需要注意的是，如果使用的是activemq5.6, 这样做会导致内存泄露，我会在下一篇博客中详述。</p><pre></pre><h2 id="nbsp"><a href="#nbsp" class="headerlink" title="&nbsp;"></a>&nbsp;</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/17762/</id>
    <link href="https://lichuanyang.top/posts/17762/"/>
    <published>2015-08-08T12:55:00.000Z</published>
    <summary>分析 Apache Camel 消息转发的工作原理和效率瓶颈，为大数据量场景下的性能优化提供思路。</summary>
    <title>关于apache camel的消息转发效率</title>
    <updated>2026-06-27T03:45:23.486Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="技术杂谈" scheme="https://lichuanyang.top/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/"/>
    <category term="大数据" scheme="https://lichuanyang.top/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <category term="LDA" scheme="https://lichuanyang.top/tags/LDA/"/>
    <content>
      <![CDATA[<p>转自<a href="http://leyew.blog.51cto.com/5043877/860255#559183-tsina-1-46862-ed0973a0c870156ed15f06a6573c8bf0">http://leyew.blog.51cto.com/5043877/860255#559183-tsina-1-46862-ed0973a0c870156ed15f06a6573c8bf0</a></p><p>前几天开始学习lda,走了不少弯路，对lda仍然是一头雾水。看了这篇文档以后总算明白lda是干啥的了</p><p>&nbsp;</p><h3 id="LDA（Latent-Dirichlet-Allocation）学习笔记"><a href="#LDA（Latent-Dirichlet-Allocation）学习笔记" class="headerlink" title="LDA（Latent Dirichlet Allocation）学习笔记"></a>LDA（Latent Dirichlet Allocation）学习笔记</h3><p>最近在看LDA算法，经过了几天挣扎，总算大致了解了这个算法的整体框架和流程。</p><h4 id="示例"><a href="#示例" class="headerlink" title="示例"></a>示例</h4><p>LDA要干的事情简单来说就是为一堆文档进行聚类（所以是非监督学习），一种topic就是一类，要聚成的topic数目是事先指定的。聚类的结果是一个概率，而不是布尔型的100%属于某个类。国外有个博客[1]上有一个清晰的例子，直接引用：</p><blockquote><p>Suppose you have the following set of sentences:</p><ul><li>I like to eat broccoli and bananas.</li><li>I ate a banana and spinach smoothie for breakfast.</li><li>Chinchillas and kittens are cute.</li><li>My sister adopted a kitten yesterday.</li><li>Look at this cute hamster munching on a piece of broccoli.</li></ul><p>What is latent Dirichlet allocation? It&rsquo;s a way of automatically discovering topics that these sentences contain. For example, given these sentences and asked for 2 topics, LDA might produce something like</p><ul><li><strong>Sentences 1 and 2</strong>: 100% Topic A</li><li><strong>Sentences 3 and 4</strong>: 100% Topic B</li><li><strong>Sentence 5</strong>: 60% Topic A, 40% Topic B</li><li><strong>Topic A</strong>: 30% broccoli, 15% bananas, 10% breakfast, 10% munching, &hellip; (at which point, you could interpret topic A to be about food)</li><li><strong>Topic B</strong>: 20% chinchillas, 20% kittens, 20% cute, 15% hamster, &hellip; (at which point, you could interpret topic B to be about cute animals)</li></ul></blockquote><p>上面关于sentence 5的结果，可以看出来是一个明显的概率类型的聚类结果（sentence 1和2正好都是100%的确定性结果）。</p><p>再看例子里的结果，除了为每句话得出了一个概率的聚类结果，而且对每个Topic，都有代表性的词以及一个比例。以Topic A为例，就是说所有对应到Topic A的词里面，有30%的词是broccoli。在LDA算法中，会把每一个文档中的每一个词对应到一个Topic，所以能算出上面这个比例。这些词为描述这个Topic起了一个很好的指导意义，我想这就是LDA区别于传统文本聚类的优势吧。</p><h4 id="LDA整体流程"><a href="#LDA整体流程" class="headerlink" title="LDA整体流程"></a>LDA整体流程</h4><p>先定义一些字母的含义：</p><ul><li>文档集合<strong>D</strong>，topic集合<strong>T</strong></li><li>D中每个文档d看作一个单词序列**&lt; w1,w2,…,wn &gt;**，wi表示第i个单词，设d有n个单词。（LDA里面称之为_word bag_，实际上每个单词的出现位置对LDA算法无影响）</li><li>D中涉及的所有不同单词组成一个大集合<strong>VOCABULARY</strong>（简称<strong>VOC</strong>）</li></ul><p>LDA以文档集合D作为输入（会有切词，去停用词，取词干等常见的预处理，略去不表），希望训练出的两个结果向量（<strong>设聚成k个Topic，VOC中共包含m个词</strong>）：</p><ul><li><strong>对每个D中的文档d，对应到不同topic的概率&theta;<sub>d</sub>&nbsp;&lt; p<sub>t1</sub>,…, p<sub>tk</sub>&nbsp;&gt;</strong>，其中，p<sub>ti</sub>表示d对应T中第i个topic的概率。计算方法是直观的，p<sub>ti</sub>&#x3D;n<sub>ti</sub>&#x2F;n，其中n<sub>ti</sub>表示d中对应第i个topic的词的数目，n是d中所有词的总数。</li><li><strong>对每个T中的topic t，生成不同单词的概率&phi;<sub>t</sub>&nbsp;&lt; p<sub>w1</sub>,…, p<sub>wm</sub>&nbsp;&gt;</strong>，其中，p<sub>wi</sub>表示t生成VOC中第i个单词的概率。计算方法同样很直观，p<sub>wi</sub>&#x3D;N<sub>wi</sub>&#x2F;N，其中N<sub>wi</sub>表示对应到topic t的VOC中第i个单词的数目，N表示所有对应到topic t的单词总数。</li></ul><p>LDA的核心公式如下：</p><p>*<em>p(w|d) &#x3D; p(w|t)<em>p(t|d)</em></em></p><p>直观的看这个公式，就是以Topic作为中间层，可以通过当前的&theta;<sub>d</sub>和&phi;<sub>t</sub>给出了文档d中出现单词w的概率。其中p(t|d)利用&theta;<sub>d</sub>计算得到，p(w|t)利用&phi;<sub>t</sub>计算得到。</p><p>实际上，利用当前的&theta;<sub>d</sub>和&phi;<sub>t</sub>，我们可以为一个文档中的一个单词计算它对应任意一个Topic时的p(w|d)，然后根据这些结果来更新这个词应该对应的topic。然后，如果这个更新改变了这个单词所对应的Topic，就会反过来影响&theta;<sub>d</sub>和&phi;<sub>t</sub>。</p><p>LDA算法开始时，先随机地给&theta;<sub>d</sub>和&phi;<sub>t</sub>赋值（对所有的d和t）。然后上述过程不断重复，最终收敛到的结果就是LDA的输出。</p><p>再详细说一下这个迭代的学习过程：</p><p>针对一个特定的文档d<sub>s</sub>中的第i单词w<sub>i</sub>，如果令该单词对应的topic为t<sub>j</sub>，可以把上述公式改写为：</p><p><em>p<sub>j</sub>(w<sub>i</sub>|d<sub>s</sub>) &#x3D; p(w<sub>i</sub>|t<sub>j</sub>)*p(t<sub>j</sub>|d<sub>s</sub>)</em></p><p>先不管这个值怎么计算（可以先理解成直接从&theta;<sub>ds</sub>和&phi;<sub>tj</sub>中取对应的项。实际没这么简单，但对理解整个LDA流程没什么影响，后文再说。）。现在我们可以枚举T中的topic，得到所有的p<sub>j</sub>(w<sub>i</sub>|d<sub>s</sub>)，其中j取值1~k。然后可以根据这些概率值结果为d<sub>s</sub>中的第i个单词w<sub>i</sub>选择一个topic。最简单的想法是取令p<sub>j</sub>(w<sub>i</sub>|d<sub>s</sub>)最大的t<sub>j</sub>（注意，这个式子里只有j是变量），即</p><p><em>argmax[j]p<sub>j</sub>(w<sub>i</sub>|d<sub>s</sub>)</em></p><p>当然这只是一种方法（好像还不怎么常用），实际上这里怎么选择t在学术界有很多方法，我还没有好好去研究。</p><p>然后，如果d<sub>s</sub>中的第i个单词w<sub>i</sub>在这里选择了一个与原先不同的topic，就会对&theta;<sub>d</sub>和&phi;<sub>t</sub>有影响了（根据前面提到过的这两个向量的计算公式可以很容易知道）。它们的影响又会反过来影响对上面提到的p(w|d)的计算。对D中所有的d中的所有w进行一次p(w|d)的计算并重新选择topic看作一次迭代。这样进行n次循环迭代之后，就会收敛到LDA所需要的结果了。&nbsp;<em>【在这里突然想到了一个问题，就是对&theta;<sub>d</sub>和&phi;<sub>t</sub>这两个向量的更新究竟是在一次迭代对所有的d中的所有w更新之后统一更新（也就是在一次迭代中，&theta;<sub>d</sub>和&phi;<sub>t</sub>不变，统一在迭代结束时更新），还是每对一个d中的一个w更新topic之后，就马上对&theta;<sub>d</sub>和&phi;<sub>t</sub>进行更新呢？这个看来要去看一下那篇LDA最原始的论文了】</em></p><h4 id="怎样计算p-w-t-和p-t-d"><a href="#怎样计算p-w-t-和p-t-d" class="headerlink" title="怎样计算p(w|t)和p(t|d)"></a>怎样计算p(w|t)和p(t|d)</h4><p>待续&hellip;&hellip;</p><p>&nbsp;</p><h4 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h4><h2 id="【1】Introduction-to-Latent-Dirichlet-Allocation：国外博客，很不错的入门文章"><a href="#【1】Introduction-to-Latent-Dirichlet-Allocation：国外博客，很不错的入门文章" class="headerlink" title="【1】Introduction to Latent Dirichlet Allocation：国外博客，很不错的入门文章"></a>【1】<a href="http://blog.echen.me/2011/08/22/introduction-to-latent-dirichlet-allocation/">Introduction to Latent Dirichlet Allocation</a>：国外博客，很不错的入门文章</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/63299/</id>
    <link href="https://lichuanyang.top/posts/63299/"/>
    <published>2012-12-02T07:20:00.000Z</published>
    <summary>LDA（Latent Dirichlet Allocation）主题模型入门学习笔记，帮助理解 LDA 的基本概念和应用场景。</summary>
    <title>一篇不错的lda模型入门文档</title>
    <updated>2026-06-27T03:42:25.690Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="大数据" scheme="https://lichuanyang.top/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <category term="大数据" scheme="https://lichuanyang.top/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <category term="hadoop" scheme="https://lichuanyang.top/tags/hadoop/"/>
    <content>
      <![CDATA[<h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><p>参考<a href="http://www.cnblogs.com/xia520pi/archive/2012/05/16/2504205.html">http://www.cnblogs.com/xia520pi/archive/2012/05/16/2504205.html</a></p><p>&nbsp;</p><p>package org.apache.hadoop.examples;</p><p>import java.io.IOException;</p><p>import java.util.StringTokenizer;</p><p>import org.apache.hadoop.conf.Configuration;</p><p>import org.apache.hadoop.fs.Path;</p><p>import org.apache.hadoop.io.IntWritable;</p><p>import org.apache.hadoop.io.Text;</p><p>import org.apache.hadoop.mapreduce.Job;</p><p>import org.apache.hadoop.mapreduce.Mapper;</p><p>import org.apache.hadoop.mapreduce.Reducer;</p><p>import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;</p><p>import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;</p><p>import org.apache.hadoop.util.GenericOptionsParser;</p><p>public class WordCount {</p><p>　　## Mapper 实现</p><p>public static class&nbsp;<strong>TokenizerMapper</strong></p><p>　　　　　　extends Mapper&lt;Object, Text, Text, IntWritable&gt;{ &nbsp;&#x2F;<span style="color: #3366ff;">&#x2F;继承org.apache.hadoop.mapreduce包中<strong>Mapper</strong>类，并<strong>重写</strong>其map方法</span></p><p>　　　　　　private final static IntWritable one &#x3D; new IntWritable(1); &nbsp; &#x2F;&#x2F;<span style="color: #3366ff;"><strong>Mapper&lt;KEYIN,VALUEIN,KEYOUT,VALUEOUT&gt;</strong></span></p><p>　　　　　　private Text <span style="color: #ff0000;">word</span> &#x3D; new Text();</p><p>&nbsp;</p><p>　　　　　　public void map(Object key, Text value, Context context) &nbsp;&#x2F;&#x2F;<span>Called once for each key&#x2F;value pair in the input split</span></p><p>　　　　　　　　throws IOException, InterruptedException { &nbsp;&#x2F;&#x2F;<span style="color: #3366ff;">value值存储的是文本文件中的一行（以回车符为行结束标记），而key值为该行的首字母相对于文本文件的首地址的偏移量</span></p><p>　　　　　　　　StringTokenizer itr &#x3D; new StringTokenizer(value.toString()); &nbsp; &nbsp;&#x2F;&#x2F;<span style="color: #3366ff;">拆分成单词</span></p><p>　　　　　　　　while (itr.hasMoreTokens()) {</p><p>　　　　　　　　<span style="color: #ff0000;">word</span>.set(itr.nextToken());</p><p>　　　　　　　　context.write(word, one); &nbsp;&#x2F;<span style="color: #3366ff;">&#x2F;输出&lt;word，1&gt;</span></p><p>　　　　　　}</p><p>　　　　}</p><p>　　}</p><p>&#x2F;&#x2F;系统自动对map结果进行排序等处理，reduce输入例 （asd,1-1-1)</p><p>　　## Reducer 实现</p><p>public static class&nbsp;<strong>IntSumReducer</strong></p><p>　　　　　　extends Reducer&lt;Text,IntWritable,Text,IntWritable&gt; { &nbsp;&#x2F;&#x2F;<span style="color: #3366ff;">Reducer&lt;KEYIN,VALUEIN,KEYOUT,VALUEOUT&gt;</span></p><p>　　　　　　private IntWritable result &#x3D; new IntWritable();</p><p>　　　　　　public void reduce(Text key, Iterable&lt;IntWritable&gt; values,Context context)</p><p>　　　　　　　　　　 throws IOException, InterruptedException { &nbsp; &#x2F;<span style="color: #3366ff;">&#x2F;reducer输入为Map过程输出，&lt;key,values&gt;中key为单个单词，而values是对应单词的计数值</span></p><p>　　　　　　　　int sum &#x3D; 0;</p><p>　　　　　　　　for (IntWritable val : values) {</p><p>　　　　　　　　　　　sum +&#x3D; val.get();</p><p>　　　　　　　　}</p><p>　　　　　　result.set(sum);</p><p>　　　　　　context.write(key, result);</p><p>　　　　}</p><p>　　}</p><p>&nbsp;</p><p>　　## Main 方法</p><p>public static void&nbsp;<strong>main</strong>(String[] args) throws Exception {</p><p>　　　　Configuration conf &#x3D; new Configuration();</p><p>　　　　String[] otherArgs &#x3D; new GenericOptionsParser(conf, args).getRemainingArgs();</p><p>　　　　if (otherArgs.length !&#x3D; 2) {</p><p>　　　　　　System.err.println(“Usage: wordcount &lt;in&gt; &lt;out&gt;”);</p><p>　　　　　　System.exit(2);</p><p>　　　　}</p><p>　　　　Job job &#x3D; new Job(conf, “word count”);</p><p>　　　　job.setJarByClass(WordCount.class);</p><p>　　　　job.setMapperClass(TokenizerMapper.class); &#x2F;&#x2F;<span style="color: #3366ff;">setMapperClass：设置Mapper，默认为IdentityMapper</span></p><p>　　　　job.setCombinerClass(IntSumReducer.class);</p><p>　　　　job.setReducerClass(IntSumReducer.class);&#x2F;&#x2F;<span style="color: #3366ff;">setReducerClass：设置Reducer，默认为IdentityReducer</span></p><p>　　　　job.setOutputKeyClass(Text.class);</p><p>　　　　job.setOutputValueClass(IntWritable.class);</p><p>　　　　FileInputFormat.addInputPath(job, new Path(otherArgs[0]));&#x2F;<span style="color: #3366ff;">&#x2F;FileInputFormat.addInputPath：设置输入文件的路径，可以是一个文件，一个路径，一个通配符。可以被调用多次添加多个路径</span></p><p>　　　　FileOutputFormat.setOutputPath(job, new Path(otherArgs[1]));&#x2F;<span style="color: #3366ff;">&#x2F;FileOutputFormat.setOutputPath：设置输出文件的路径，在job运行前此路径不应该存在</span></p><p>　　　　System.exit(job.waitForCompletion(true) ? 0 : 1);</p><p>}</p><p>}</p><h2 id="其他配置说明"><a href="#其他配置说明" class="headerlink" title="其他配置说明"></a>其他配置说明</h2><p>setInputFormat：设置map的输入格式，默认为TextInputFormat，key为LongWritable, value为Text</p><p>setNumMapTasks：设置map任务的个数，此设置通常不起作用，map任务的个数取决于输入的数据所能分成的input split的个数</p><p>&nbsp;</p><p>setMapRunnerClass：设置MapRunner, map task是由MapRunner运行的，默认为MapRunnable，其功能为读取input split的一个个record，依次调用Mapper的map函数</p><p>setMapOutputKeyClass和setMapOutputValueClass：设置Mapper的输出的key-value对的格式</p><p>setOutputKeyClass和setOutputValueClass：设置Reducer的输出的key-value对的格式</p><p>setPartitionerClass和setNumReduceTasks：设置Partitioner，默认为HashPartitioner，其根据key的hash值来决定进入哪个partition，每个partition被一个reduce task处理，所以partition的个数等于reduce task的个数</p><p>&nbsp;</p><p>setOutputFormat：设置任务的输出格式，默认为TextOutputFormat</p><h2 id="nbsp"><a href="#nbsp" class="headerlink" title="&nbsp;"></a>&nbsp;</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/25187/</id>
    <link href="https://lichuanyang.top/posts/25187/"/>
    <published>2012-10-28T10:42:00.000Z</published>
    <summary>Hadoop WordCount 经典示例代码及运行过程说明，大数据入门必读。</summary>
    <title>wordcount代码</title>
    <updated>2026-06-27T03:42:25.691Z</updated>
  </entry>
  <entry>
    <author>
      <name>流沙</name>
    </author>
    <category term="大数据" scheme="https://lichuanyang.top/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <category term="大数据" scheme="https://lichuanyang.top/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <category term="hadoop" scheme="https://lichuanyang.top/tags/hadoop/"/>
    <content>
      <![CDATA[<blockquote><p>⚠️ 这是一篇 <strong>2012 年</strong>的学习笔记。Hadoop 生态在过去十几年发生了巨大变化，下面的链接已非常陈旧。我保留了原始内容作为历史记录，并补充了 2026 年的学习建议。</p></blockquote><span id="more"></span><h2 id="为什么-2026-年还要了解-Hadoop？"><a href="#为什么-2026-年还要了解-Hadoop？" class="headerlink" title="为什么 2026 年还要了解 Hadoop？"></a>为什么 2026 年还要了解 Hadoop？</h2><p>可能你会问：现在都用 Spark、Flink、数据湖了，为什么还要学 Hadoop？</p><p>答案很简单：<strong>HDFS 和 YARN 仍然是大数据基础设施的底座</strong>。Spark 默认跑在 YARN 上，Hive 的表数据存在 HDFS 里，甚至很多云原生数据平台底层还是 HDFS 兼容存储。理解 Hadoop 的核心设计（分布式文件系统、计算资源调度、数据本地性），能帮你更好地理解上层框架的设计决策。</p><h2 id="2026-年推荐的学习路径"><a href="#2026-年推荐的学习路径" class="headerlink" title="2026 年推荐的学习路径"></a>2026 年推荐的学习路径</h2><h3 id="入门：理解核心概念"><a href="#入门：理解核心概念" class="headerlink" title="入门：理解核心概念"></a>入门：理解核心概念</h3><ol><li><strong>HDFS</strong>：分布式文件系统的基本原理——NameNode&#x2F;DataNode 架构、数据块（block）、副本机制、机架感知</li><li><strong>MapReduce</strong>：了解 map-shuffle-reduce 的计算模型，理解为什么它适合批处理但不适合迭代计算</li><li><strong>YARN</strong>：资源管理和调度框架，理解 ApplicationMaster 和 Container 的概念</li></ol><p>推荐阅读：<a href="https://hadoop.apache.org/docs/stable/">Hadoop 官方文档</a>（最新的 3.x 版本）</p><h3 id="进阶：从理论到实践"><a href="#进阶：从理论到实践" class="headerlink" title="进阶：从理论到实践"></a>进阶：从理论到实践</h3><p>与其从零写 MapReduce 程序，我更推荐直接上手 Spark：</p><ul><li><strong>Spark</strong>：比 MapReduce 更快、API 更友好，支持 SQL&#x2F;DataFrame&#x2F;Streaming&#x2F;MLlib 多种范式</li><li><strong>Hive</strong>：SQL on Hadoop，将 SQL 转化为分布式计算任务，适合数据分析场景</li></ul><h3 id="深入：理解架构演进"><a href="#深入：理解架构演进" class="headerlink" title="深入：理解架构演进"></a>深入：理解架构演进</h3><p>Hadoop 1.x → 2.x → 3.x 的几个关键变化值得了解：</p><table><thead><tr><th>版本</th><th>重要变化</th></tr></thead><tbody><tr><td>1.x</td><td>基础 HDFS + MapReduce，单 NameNode（单点故障）</td></tr><tr><td>2.x</td><td>引入 YARN（资源管理分离）、NameNode HA、Federation</td></tr><tr><td>3.x</td><td>Erasure Coding（节省存储）、YARN Timeline Service v2、GPU 调度支持</td></tr></tbody></table><hr><h2 id="📚-原始笔记（2012-年整理）"><a href="#📚-原始笔记（2012-年整理）" class="headerlink" title="📚 原始笔记（2012 年整理）"></a>📚 原始笔记（2012 年整理）</h2><p>以下链接是当年初学 Hadoop 时整理的资料，其中大部分链接可能已失效或内容过时，仅作历史参考：</p><ul><li><a href="http://www.cnblogs.com/xia520pi/archive/2012/05/16/2504205.html">WordCount 运行过程详解</a> — 理解 MapReduce 入门的经典案例</li><li><a href="http://www.cnblogs.com/gpcuster/archive/2010/06/04/1751538.html">HDFS 命令介绍</a> — 基础文件操作命令</li><li><a href="http://hi.baidu.com/gkf8605/item/d6b8af09c3463512eafe38b1">HDFS 命令（另一篇）</a> — 补充命令参考</li><li><a href="http://www.cnblogs.com/forfuture1978/archive/2010/11/14/1877086.html">MapReduce 入门</a> — MapReduce 编程模型入门</li><li><a href="http://hadoop.apache.org/docs/r0.20.2/api/index.html">Hadoop 0.20.2 API</a> — 0.20.2 版本 API 文档</li></ul><blockquote><p>回想 2012 年刚开始接触 Hadoop 的时候，还没有 Spark、没有数据湖、没有 Kubernetes。那时候 MapReduce 就是大数据处理的全部，连 Hive 都还在早期版本。技术的发展速度总是超出预期，但基础原理永远是值得学习的。</p></blockquote><h2 id="常见问题"><a href="#常见问题" class="headerlink" title="常见问题"></a>常见问题</h2><h3 id="Q-2026-年还要学-Hadoop-吗？"><a href="#Q-2026-年还要学-Hadoop-吗？" class="headerlink" title="Q: 2026 年还要学 Hadoop 吗？"></a>Q: 2026 年还要学 Hadoop 吗？</h3><p>要学，但不需要学到能写 MapReduce 的程度。HDFS 和 YARN 仍然是大数据基础设施的底座——Spark 默认跑在 YARN 上，Hive 数据存在 HDFS 里，甚至很多云原生数据平台底层还是 HDFS 兼容存储。理解分布式文件系统、资源调度、数据本地性这些核心概念，能帮你更好地理解上层框架的设计决策。</p><h3 id="Q-学-Hadoop-从哪开始？"><a href="#Q-学-Hadoop-从哪开始？" class="headerlink" title="Q: 学 Hadoop 从哪开始？"></a>Q: 学 Hadoop 从哪开始？</h3><p>建议三步走：<strong>入门阶段</strong>先理解 HDFS（NameNode&#x2F;DataNode 架构、副本机制）和 YARN（资源管理）的核心概念；<strong>实践阶段</strong>直接上手 Spark 做数据处理，而不是死磕 MapReduce 编程；<strong>进阶阶段</strong>了解 Hadoop 1.x → 2.x → 3.x 的架构演进，理解为什么引入了 Erasure Coding、NameNode Federation 等特性。</p><h3 id="Q-学过时技术有价值吗？"><a href="#Q-学过时技术有价值吗？" class="headerlink" title="Q: 学过时技术有价值吗？"></a>Q: 学过时技术有价值吗？</h3><p>有价值，但要区分”学原理”和”学操作”。十年前的技术操作细节确实会过时（比如某个版本的 API 调用方式），但它的设计原理往往历久弥新。MapReduce 的 shuffle 模型今天在 Spark 中依然存在，HDFS 的副本机制和机架感知思路在云存储设计中也能看到影子。关键在于通过过时的实现去理解不变的设计思路。</p><h3 id="Q-有了-Spark-还需要学-MapReduce-吗？"><a href="#Q-有了-Spark-还需要学-MapReduce-吗？" class="headerlink" title="Q: 有了 Spark 还需要学 MapReduce 吗？"></a>Q: 有了 Spark 还需要学 MapReduce 吗？</h3><h2 id="不需要学具体的-MapReduce-编程，但建议理解它的计算模型。MapReduce-的-map-shuffle-reduce-处理流程、数据本地性优化、容错机制，这些思想在-Spark-中都有延续和改进。理解了-MapReduce-的”慢”从何而来，才能真正理解-Spark-的”快”——内存计算、DAG-调度、RDD-血缘关系这些优化，都是对-MapReduce-短板的直接回应。"><a href="#不需要学具体的-MapReduce-编程，但建议理解它的计算模型。MapReduce-的-map-shuffle-reduce-处理流程、数据本地性优化、容错机制，这些思想在-Spark-中都有延续和改进。理解了-MapReduce-的”慢”从何而来，才能真正理解-Spark-的”快”——内存计算、DAG-调度、RDD-血缘关系这些优化，都是对-MapReduce-短板的直接回应。" class="headerlink" title="不需要学具体的 MapReduce 编程，但建议理解它的计算模型。MapReduce 的 map-shuffle-reduce 处理流程、数据本地性优化、容错机制，这些思想在 Spark 中都有延续和改进。理解了 MapReduce 的”慢”从何而来，才能真正理解 Spark 的”快”——内存计算、DAG 调度、RDD 血缘关系这些优化，都是对 MapReduce 短板的直接回应。"></a>不需要学具体的 MapReduce 编程，但建议理解它的计算模型。MapReduce 的 map-shuffle-reduce 处理流程、数据本地性优化、容错机制，这些思想在 Spark 中都有延续和改进。理解了 MapReduce 的”慢”从何而来，才能真正理解 Spark 的”快”——内存计算、DAG 调度、RDD 血缘关系这些优化，都是对 MapReduce 短板的直接回应。</h2>]]>
    </content>
    <id>https://lichuanyang.top/posts/35575/</id>
    <link href="https://lichuanyang.top/posts/35575/"/>
    <published>2012-10-28T10:05:00.000Z</published>
    <summary>Hadoop 学习资料整理，包含 WordCount 运行过程详解、HDFS 命令介绍等入门资源汇总。</summary>
    <title>hadoop基本的学习资料</title>
    <updated>2026-06-27T02:31:54.477Z</updated>
  </entry>
</feed>
