<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>Zhengxin Diao</name>
    <email>diaozxin@163.com</email>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <icon>https://www.gravatar.com/avatar/73da0d5ffd8a1b1edf601acc6ae72d16</icon>
  <id>https://xilidou.com/</id>
  <link href="https://xilidou.com/" rel="alternate"/>
  <link href="https://xilidou.com/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, Zhengxin Diao</rights>
  <subtitle>高质量的技术博客</subtitle>
  <title>犀利豆的博客</title>
  <updated>2026-08-08T02:04:49.444Z</updated>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="AI" scheme="https://xilidou.com/categories/AI/"/>
    <category term="Agent" scheme="https://xilidou.com/tags/Agent/"/>
    <category term="AI" scheme="https://xilidou.com/tags/AI/"/>
    <category term="Claude Code" scheme="https://xilidou.com/tags/Claude-Code/"/>
    <category term="Tools" scheme="https://xilidou.com/tags/Tools/"/>
    <category term="Harness" scheme="https://xilidou.com/tags/Harness/"/>
    <category term="Prompt Engineering" scheme="https://xilidou.com/tags/Prompt-Engineering/"/>
    <content>
      <![CDATA[<p>Claude code tools 研究系列第七篇。前六篇拆完了「交互原语三件套」(Ask &#x2F; EnterPlanMode &#x2F; ExitPlanMode)、搜索双人组 Grep + Glob,以及感知 + 精准执行的搭档 Read &#x2F; Edit。这一篇聊 Edit 的兄弟工具 —— <strong>Write</strong>。</p><p>Grep + Glob + Read + Edit 组合起来,能应对「先定位、后读、再精准修改」的绝大多数场景。但有两类事 Edit 干不了:<strong>新建文件 · 完全重写文件</strong>。这两类事只能靠 Write。</p><p>Write 看似简单(就是「把内容写到文件里」),但它的设计有一个特别的张力 —— **它既是必需的(唯一能创建新文件的工具),又是危险的(能覆盖任何已有文件)**。整套 Write 的 prompt 都在处理这个张力。</p><blockquote><p>本系列先读 前置篇 —— 讲清楚 tool 是什么、Claude 怎么用。本篇按前置篇提出的 4 层骨架展开。</p></blockquote><h2 id="Write"><a href="#Write" class="headerlink" title="Write"></a>Write</h2><h3 id="作用"><a href="#作用" class="headerlink" title="作用"></a>作用</h3><p>Write 是 Claude Code 内置的<strong>文件全量写入工具</strong>。它做的事很直白:给一个绝对路径 + 一段文本内容,把内容写到那个文件里。如果文件已存在,<strong>整个覆盖</strong>;如果不存在,<strong>新建</strong>。</p><p>它解决的核心问题是「AI 如何<strong>安全、显式</strong>地生产新文件 · 或者做完全重写」:</p><ol><li><strong>唯一能创建新文件的执行工具</strong> —— Edit 不能新建,Bash 可以但不受审阅</li><li><strong>完全重写的最经济路径</strong> —— 改动占文件 80% 以上时,Write 比一堆 Edit 高效</li><li><strong>强制基于真实状态覆盖</strong> —— 已存在的文件必须先 Read 过才能 Write,防幻觉覆盖</li><li><strong>可审阅的完整产物</strong> —— tool call 里就是即将写入磁盘的全文,一目了然</li></ol><h3 id="一个具体例子"><a href="#一个具体例子" class="headerlink" title="一个具体例子"></a>一个具体例子</h3><p><strong>场景</strong>:用户说 <strong>「给我加个 <code>UserBadge</code> 组件,展示用户头像 + 名字 + 状态灯,放到 <code>src/components/UserBadge.tsx</code>」</strong>。</p><p>这是一个<strong>从零创建新文件</strong>的典型场景。项目里没有 UserBadge,Claude 探索完项目风格后,准备把新文件写出来。</p><h4 id="Write-是怎么解决的"><a href="#Write-是怎么解决的" class="headerlink" title="Write 是怎么解决的"></a>Write 是怎么解决的</h4><p>Claude 直接调 Write,传两个参数:</p><ul><li><code>file_path</code>: <code>/Users/xxx/project/src/components/UserBadge.tsx</code>(绝对路径)</li><li><code>content</code>: 完整的组件代码(几十行)</li></ul><p><strong>运行时会发生什么</strong>:</p><ul><li>Runtime 检查:目标路径的父目录存在吗?不存在则报错</li><li>Runtime 检查:如果文件已存在,本次会话里 Read 过吗?没有则报错(<strong>跟 Edit 是同一套 harness 追踪机制</strong>)</li><li>Runtime 执行写入:把 <code>content</code> 完整落到磁盘</li><li>如果是新建,顺便创建文件;如果覆盖,替换整个内容</li></ul><p>用户在 tool call log 里看到的是:</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">Write(file_path: src/components/UserBadge.tsx, content: [full 40-line component])</span><br><span class="line">→ File created</span><br></pre></td></tr></table></figure><p><strong>一次到位、无副作用</strong>。</p><h4 id="反例-如果拿-Write-干-Edit-该干的事"><a href="#反例-如果拿-Write-干-Edit-该干的事" class="headerlink" title="反例:如果拿 Write 干 Edit 该干的事"></a>反例:如果拿 Write 干 Edit 该干的事</h4><p>接续上一篇 Edit 的场景 —— 用户说「把 <code>handleClick</code> 重命名成 <code>handleSubmit</code>」。这个文件已经存在,600 行,只需要改 4 处。</p><p><strong>如果 Claude 硬用 Write 而不是 Edit</strong>:</p><ul><li>先 Read 拿到 600 行完整内容</li><li>在脑子里做 4 处替换</li><li>用 Write 把改后的 600 行整体写回</li></ul><p>这时会遇到几个问题:</p><ol><li><strong>Token 浪费严重</strong> —— 600 行内容在 tool call 里被完整传输一次(Write 的 content 参数),而实际改动只有 4 处</li><li><strong>破坏面失控</strong> —— Write 会覆盖整个文件,如果 Claude 在传输过程中丢了个空格 &#x2F; 换错了个引号 &#x2F; 少复制一行,整个文件都被这个 bug 污染</li><li><strong>Diff 难审阅</strong> —— 用户在 tool call log 里看到 600 行 content,得跟旧版本跑一次 diff 才能看清 Claude 到底动了什么</li><li><strong>并发冲突放大</strong> —— 如果用户在另一个编辑器里刚保存了别的改动,Write 会把它整个盖掉,连提示都没有</li><li><strong>误覆盖风险</strong> —— Write 是「整个替换」,没有 Edit 那种「old_string 必须匹配」的安全网,写错内容也不会报错</li></ol><p><strong>核心洞察</strong>:<strong>Write 和 Edit 不是替代关系 · 是分工关系</strong>。Write 干新建 &#x2F; 完全重写,Edit 干增量修改。混着用会失去每个工具的独特安全保障。</p><h4 id="什么时候该-Write-什么时候该-Edit"><a href="#什么时候该-Write-什么时候该-Edit" class="headerlink" title="什么时候该 Write 什么时候该 Edit"></a>什么时候该 Write 什么时候该 Edit</h4><table><thead><tr><th>场景</th><th>选 Write</th><th>选 Edit</th></tr></thead><tbody><tr><td>从零创建新文件</td><td>✅ 唯一选择</td><td>❌ 不能创建</td></tr><tr><td>改动占文件 80%+</td><td>✅ 全量重写更经济</td><td>⚠️ old_string 会很长很脆弱</td></tr><tr><td>改动占文件 20%-</td><td>⚠️ token 浪费 · 风险面大</td><td>✅ 精准替换</td></tr><tr><td>重命名变量 &#x2F; 函数</td><td>❌ 不推荐</td><td>✅ 用 replace_all</td></tr><tr><td>修 typo</td><td>❌ 大炮打蚊子</td><td>✅ 一次替换搞定</td></tr><tr><td>生成配置文件 &#x2F; boilerplate</td><td>✅ 一次写完</td><td>❌ 空文件 Edit 不了</td></tr></tbody></table><p>一个粗略的<strong>思维模型</strong>:如果你的 <code>new_string / new_content</code> 里大部分内容是<strong>从旧文件复制过来的</strong>,那就用 Edit;如果<strong>大部分内容是新写的</strong>,那就用 Write。</p><h3 id="触发条件"><a href="#触发条件" class="headerlink" title="触发条件"></a>触发条件</h3><p>工具官方说明写得非常克制:<strong>「优先编辑已有文件 · 除非明确需要,否则不新建」</strong>。这是 Write 与 Edit 的默认竞争关系的<strong>显式仲裁</strong>。</p><p><strong>该用 Write 的场景</strong>:</p><ul><li><strong>用户明确要求新建文件</strong> —— 「给我加个 xxx 组件」&#x2F;「生成一份 xxx 配置」</li><li><strong>代码需要新模块</strong> —— 拆分现有代码时创建新文件</li><li><strong>完全重写</strong> —— 改动占比 80%+,Edit 的 old_string 会过长,反而增加脆弱性</li><li><strong>生成 boilerplate</strong> —— 脚手架、测试模板、迁移文件</li></ul><p><strong>不该用 Write 的场景</strong>:</p><ul><li><strong>微调现有文件</strong> —— 用 Edit,精准替换是它的强项</li><li><strong>写文档 &#x2F; README 除非用户明确要</strong> —— 官方硬约束,见下方 2 · 工具级描述</li><li><strong>加 emoji 除非用户明确要</strong> —— 官方硬约束,见下方 2 · 工具级描述</li><li><strong>凭幻觉「验证」</strong> —— 跟 Read 那篇提到的反浪费原则一样,Write 完不用回头 Read 验证</li></ul><p>一个有意思的<strong>反自动生产原则</strong>:官方 prompt 里写了 <code>NEVER create documentation files (*.md) or README files unless explicitly requested by the User</code> —— 大写 NEVER。这条约束背后是<strong>血泪教训</strong>:早期 AI 工具经常「贴心地」自动生成一堆 README &#x2F; CHANGELOG &#x2F; API.md,项目主一看满地都是没请示过就冒出来的 markdown,又不好删,只好留着。<strong>Write 的这条约束在把这个反模式钉死</strong>。</p><h3 id="技术实现"><a href="#技术实现" class="headerlink" title="技术实现"></a>技术实现</h3><h4 id="1-·-命名"><a href="#1-·-命名" class="headerlink" title="1 · 命名"></a>1 · 命名</h4><p><code>Write</code></p><p>命名极其直白 —— 一个动词,英语里最基本的「写」。跟 <code>Edit</code>(编辑) 、<code>Read</code>(读)构成同族三兄弟,望文生义:</p><ul><li><strong>Read</strong> —— 读 · 感知外部</li><li><strong>Edit</strong> —— 编辑 · 增量修改</li><li><strong>Write</strong> —— 写 · 全量落盘 &#x2F; 新建</li></ul><p>三个动词都指向「文件」这个操作对象,但语义边界清晰:Read 只输入不输出;Edit 是「已有内容 → 改一部分」;Write 是「有没有都行 → 整个覆盖 &#x2F; 新建」。<strong>动词的粒度直接编码了危险度</strong> —— Write 是三者里最重的动作,名字本身就在提示这一点。</p><p>字段名同样朴素:<code>file_path</code> + <code>content</code>。没有 old_string &#x2F; new_string &#x2F; replace_all 之类的「匹配」概念,因为 Write 根本不做匹配 —— 语义就是「把这段内容盖到磁盘上」。字段少反而是一种坦率:<strong>Write 没有安全网,也不假装有</strong>。</p><h4 id="2-·-工具级描述"><a href="#2-·-工具级描述" class="headerlink" title="2 · 工具级描述"></a>2 · 工具级描述</h4><p>Write 的工具级描述短小精悍,几条约束逐条拆:</p><p><strong>约束 1:覆盖行为的透明化</strong></p><blockquote><p>This tool will overwrite the existing file if there is one at the provided path.</p></blockquote><p>关键词 <strong>will overwrite</strong> —— 没有「小心 &#x2F; 请注意」的软化,直接说清楚。这条描述让 Claude 完全清楚 Write 的破坏性 —— 不会有「我以为它会 merge」的错觉。</p><p><strong>约束 2:Read 先行的强制</strong></p><blockquote><p>If this is an existing file, you MUST use the Read tool first to read the file’s contents. This tool will fail if you did not read the file first.</p></blockquote><p>关键词 <strong>MUST &#x2F; will fail</strong> —— 硬阻断。跟 Edit 是完全一样的约束。这条描述让 Read → Edit &#x2F; Write 的信任链在 Claude 的直觉里建立起来:Runtime 记录本次会话里 Read 过哪些文件,Write 到已存在文件时验证。目的:</p><ul><li><strong>防幻觉覆盖</strong> —— Claude 可能「记得」文件长什么样,但磁盘上的可能已经被改过</li><li><strong>强制感知承诺</strong> —— 「你要覆盖这个文件?先证明你知道现在里面是什么」</li><li><strong>与 Edit 共享信任链</strong> —— Read → Edit 和 Read → Write 走同一套状态机</li></ul><p>新建文件不需要 Read 先行(因为文件还不存在),但一旦文件已存在,就必须走 Read。<strong>这是「Write 的双面性」在 harness 层的体现</strong>。</p><p><strong>约束 3:偏好 Edit 而非 Write</strong></p><blockquote><p>Prefer the Edit tool for modifying existing files — it only sends the diff. Only use this tool to create new files or for complete rewrites.</p></blockquote><p>关键词 <strong>Prefer &#x2F; Only</strong> —— 一个鼓励一个限制,把 Write 的合理适用面<strong>收窄到两种</strong>:</p><ul><li>新建文件</li><li>完全重写</li></ul><p>这条约束是 Write 与 Edit 分工的<strong>权威仲裁</strong>。避免 Claude 因为「Write 语义更简单」就滥用它。</p><p><strong>约束 4:不主动创建文档</strong></p><blockquote><p>NEVER create documentation files (*.md) or README files unless explicitly requested by the User.</p></blockquote><p>关键词 <strong>NEVER &#x2F; unless explicitly requested</strong> —— 大写 + 极端量词。这条特别贴近<strong>用户体验</strong>:防止 Claude 自作聪明生产一堆没人要的 markdown。背后是<strong>血泪教训</strong>:早期 AI 工具经常「贴心地」自动生成一堆 README &#x2F; CHANGELOG &#x2F; API.md,项目主一看满地都是没请示过就冒出来的 markdown,又不好删,只好留着。</p><p>有意思的是这条约束<strong>只针对 Write</strong>(Edit 里也有类似原则,但没这么极端)—— 因为 Write 是「新建文件」的入口,新建 md 文件比编辑现有 md 文件更容易造成噪音污染。</p><p><strong>约束 5:emoji 禁令</strong></p><blockquote><p>Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.</p></blockquote><p>跟 Edit 是同一条,原因也一样:AI 训练模型天然爱在代码 &#x2F; 注释 &#x2F; 提交信息里塞 emoji,大多数专业代码库不欢迎这种风格。</p><p><strong>约束 6:错误恢复路径</strong></p><blockquote><p>This tool will fail if you did not read the file first.</p></blockquote><p>不只是说会失败,隐含了纠正路径:<strong>报错后先 Read,再重试 Write</strong>。这跟 Edit 的「唯一性失败 → 扩上下文 &#x2F; 用 replace_all」是同一种「好 prompt 的标志」 —— 错误路径也要设计。</p><p><strong>「不主动生产」的价值观合成</strong></p><p>约束 3 + 约束 4 + 约束 5 合起来构成 Write 的<strong>「不主动贡献噪音」原则</strong>:除非用户明确要,否则 Claude 不该:</p><ul><li>主动生成 README &#x2F; CHANGELOG &#x2F; docs</li><li>主动创建新文件(能编辑就编辑)</li><li>主动加 emoji</li></ul><p>这三条不是 runtime 硬阻断(Write 参数没有校验 md 后缀 &#x2F; emoji),而是<strong>描述层的行为训练</strong>。把「AI 应该谨慎生产,不应该自动贡献 markdown 和 emoji」这个价值观 hardcode 到 Claude 的默认行为里。</p><h4 id="3-·-字段级描述"><a href="#3-·-字段级描述" class="headerlink" title="3 · 字段级描述"></a>3 · 字段级描述</h4><p>Write 的入参 schema 极其简单,只有两个字段:</p><ul><li><strong>file_path</strong> —— 目标文件的<strong>绝对路径</strong></li><li><strong>content</strong> —— 要写入的完整内容</li></ul><p>看似平平无奇,但每个字段都有讲究:</p><p><strong><code>file_path</code> —— 为什么强制绝对路径</strong></p><p>跟 Read &#x2F; Edit 是同一种设计:消除 CWD 依赖,让每次调用<strong>自解释</strong>。跨会话、跨 subagent、跨 worktree,绝对路径都不会歧义。</p><p><strong><code>content</code> —— 为什么就是「完整内容」</strong></p><p>对比 Edit 的 4 字段(file_path + old_string + new_string + replace_all),Write 只有 2 字段,少了「匹配」和「批量」的概念。原因:</p><ul><li><strong>语义就是「用这段内容覆盖磁盘」</strong> —— 不需要「匹配什么」,因为不是替换</li><li><strong>没有「批量」概念</strong> —— 一次 Write 就是一次完整写入,不存在部分匹配</li><li><strong>失败模式简单</strong> —— 要么写成功,要么写失败(权限 &#x2F; 磁盘 &#x2F; 路径),没有「匹配失败」这种中间态</li></ul><p>Write 的简洁反过来意味着它<strong>没有 Edit 的那些安全网</strong> —— 没有匹配校验、没有唯一性检查、没有 replace_all 分流。<strong>风险面更大,但语义也更清晰</strong>。这是「危险面 + 必要性并存」在字段级的体现:字段少不是能力弱,是<strong>故意不给 Claude 留下「精细调整」的错觉</strong>,逼它意识到「按 Write 就是整个覆盖」。</p><h4 id="4-·-schema-校验规则"><a href="#4-·-schema-校验规则" class="headerlink" title="4 · schema 校验规则"></a>4 · schema 校验规则</h4><p>Write 在 schema 层几乎<strong>没有硬约束</strong> —— 没有长度上限、没有格式校验、没有内容黑名单。就两个字段都是 required,如此而已。</p><p>真正的约束都放在 <strong>runtime</strong> 里,构成一套状态机:</p><table><thead><tr><th>检查</th><th>时机</th><th>失败行为</th><th>意图</th></tr></thead><tbody><tr><td>父目录存在</td><td>写入前</td><td>报错拒写</td><td>防 typo 造成散落目录</td></tr><tr><td>文件已存在 → 本会话 Read 过</td><td>写入前</td><td>报错拒写</td><td>防幻觉覆盖(harness 追踪)</td></tr><tr><td>文件不存在 → 直接允许</td><td>写入前</td><td>直接创建</td><td>新建路径不需要 Read</td></tr><tr><td>权限 &#x2F; 磁盘 &#x2F; 路径合法</td><td>写入时</td><td>报错拒写</td><td>兜底 OS 级失败</td></tr></tbody></table><p><strong>为什么父目录不自动创建</strong>:如果 Write 传的路径是 <code>foo/bar/baz.ts</code> 但 <code>foo/bar/</code> 目录不存在,Write 会直接报错,<strong>不会自动创建目录</strong>。原因:</p><ul><li><strong>防止 typo 造成散落的目录</strong> —— Claude 拼错路径 <code>srcc/component.tsx</code>,如果 Write 自动创建 <code>srcc/</code>,会污染项目结构</li><li><strong>强制 Claude 意识到目录结构</strong> —— 想在新目录写文件?先用 Bash <code>mkdir -p</code> 明确表达意图,不能悄悄拉出一个目录</li><li><strong>失败明确</strong> —— 报错比「悄悄成功」更利于纠错</li></ul><p><strong>Read 先行的 harness 状态共享</strong>:Read 建立「感知承诺」,Edit &#x2F; Write 消费这个承诺:</p><ul><li>Read 的 harness 状态被<strong>两个执行工具共享</strong></li><li>Edit 消费:「我知道 old_string 在文件里的样子」</li><li>Write 消费:「我知道我在覆盖什么」</li></ul><p>这个共享让 Read 的一次调用可以给后续多个 Edit &#x2F; Write 提供感知基础,不用每次都重读。</p><p>schema 层空、runtime 层有状态机,这个分工在告诉我们:<strong>Write 的风险主要不在参数格式,而在时序和感知</strong>。参数格式能不能自动校验?能。但「你有没有先感知文件当前状态」这件事,只能靠 runtime 追踪。schema 就把简单的活留给自己,把难的留给 runtime。</p><hr><h3 id="与邻居工具的分工"><a href="#与邻居工具的分工" class="headerlink" title="与邻居工具的分工"></a>与邻居工具的分工</h3><p>Write 跟前六篇工具形成对照:</p><table><thead><tr><th>维度</th><th>三交互原语</th><th>Grep + Glob</th><th>Read</th><th>Edit</th><th>Write</th></tr></thead><tbody><tr><td>定位</td><td>协作对齐</td><td>定位坐标</td><td>感知外部</td><td>精准执行</td><td><strong>全量执行</strong></td></tr><tr><td>频率</td><td>关键节点</td><td>日常高频</td><td>日常高频</td><td>日常高频</td><td>中频</td></tr><tr><td>参数</td><td>结构化 &#x2F; 空</td><td>pattern</td><td>file_path + 分页</td><td>4 字段(含 old_string)</td><td><strong>2 字段(file_path + content)</strong></td></tr><tr><td>语义</td><td>意图信号</td><td>定位坐标</td><td>感知承诺</td><td>增量替换</td><td><strong>全量覆盖 &#x2F; 新建</strong></td></tr><tr><td>安全网</td><td>用户批准</td><td>head_limit 截断</td><td>分页 &#x2F; PDF 强制页码</td><td>唯一性 &#x2F; Read &#x2F; 匹配失败</td><td><strong>仅 Read + 父目录存在</strong></td></tr><tr><td>保守偏差</td><td>「不确定就规划」</td><td>「先按需搜再全读」</td><td>「不确定就读一读」</td><td>「不确定就 Read」</td><td><strong>「能 Edit 就 Edit · 别新建」</strong></td></tr></tbody></table><p><strong>Write 是 Edit 的兄弟工具,不是替代</strong>。二者分工:</p><ul><li>Edit 干<strong>增量修改</strong> —— old_string &#x2F; new_string &#x2F; replace_all,基于「文件已存在 + 只改一部分」的假设</li><li>Write 干<strong>新建 &#x2F; 完全重写</strong> —— content 一次到位,基于「要么没这文件 &#x2F; 要么整个覆盖」的假设</li></ul><p>混着用会失去每个工具的独特安全保障:拿 Write 干 Edit 的活会浪费 tokens + 破坏面失控 + diff 难审阅;拿 Edit 干 Write 的活根本干不了(Edit 不能创建新文件)。</p><p><strong>Grep+Glob → Read → Edit &#x2F; Write</strong> 四类五个工具共享一套 harness 追踪状态,通过「Read 先行」这条硬约束串联起来。核心哲学:<strong>任何对磁盘的写入,必须建立在对当前磁盘状态的感知之上</strong>。不是靠 AI 自律,而是靠 runtime 强制。</p><hr><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>Write 的精妙之处,不在于它「把内容写到文件」这个功能本身,而在于它的信号分布<strong>极度依赖描述层的价值观 + runtime 状态机</strong>:</p><ul><li><strong>命名</strong> —— 极简,一个动词(Read &#x2F; Edit &#x2F; Write 同族);字段名朴素,没有匹配 &#x2F; 批量概念,直接映射「覆盖」语义</li><li><strong>工具级描述</strong> —— 6 段约束覆盖:覆盖行为透明化、Read 先行硬阻断、显式偏好 Edit、不主动生产文档、emoji 禁令、错误恢复路径;三条软约束合成「不主动贡献噪音」价值观</li><li><strong>字段级描述</strong> —— 只有 2 字段(file_path + content),字段少不是能力弱,是<strong>故意不给 Claude 留下「精细调整」的错觉</strong>,逼它意识到 Write &#x3D; 整个覆盖</li><li><strong>schema 校验</strong> —— schema 层几乎空;真正约束都在 runtime 状态机:父目录必须存在(不自动创建)、已存在文件必须本会话 Read 过、与 Edit 共享同一套 harness 追踪状态</li></ul><p>Write 独特的地方在于它<strong>是唯一能创建新文件 &#x2F; 完全覆盖文件的工具,「必需性」和「危险性」并存</strong>:必需性上,新建和完全重写这两类活只能它干,Edit 顶不上;危险性上,它没有 Edit 的匹配安全网,一次调用就能覆盖 596 行文件的任何位置。这种张力靠三重设计化解 —— <strong>描述层显式偏好 Edit(把 Write 收窄到「新建 &#x2F; 完全重写」)、runtime 状态机强制 Read 先行(消除幻觉覆盖)、三条软约束钉死 AI 反模式(不主动建 docs &#x2F; 不新建 &#x2F; 不加 emoji)<strong>。相当于把「AI 全量写文件」这个天然危险的能力,收敛成一个</strong>用途受限、感知强制、不主动噪音</strong>的执行原语。</p><p>下一篇继续拆 Bash —— 这是整套工具生态里最特殊的一环:<strong>唯一没有边界的兜底工具</strong>。前面 7 个工具都在「让 AI 做有限的事」,而 Bash 让 AI「什么都能做」。看看 Claude Code 团队怎么在「无限能力」和「安全」之间找平衡。</p>]]>
    </content>
    <id>https://xilidou.com/2026/08/08/claude-code-tools-write/</id>
    <link href="https://xilidou.com/2026/08/08/claude-code-tools-write/"/>
    <published>2026-08-08T10:00:00.000Z</published>
    <summary>Write 工具在必要性与覆盖风险之间的设计权衡。</summary>
    <title>Claude Code Tools 研究系列（七）—— Write：创建与全量重写的边界</title>
    <updated>2026-08-08T02:04:49.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="AI" scheme="https://xilidou.com/categories/AI/"/>
    <category term="Agent" scheme="https://xilidou.com/tags/Agent/"/>
    <category term="AI" scheme="https://xilidou.com/tags/AI/"/>
    <category term="Claude Code" scheme="https://xilidou.com/tags/Claude-Code/"/>
    <category term="Tools" scheme="https://xilidou.com/tags/Tools/"/>
    <category term="Harness" scheme="https://xilidou.com/tags/Harness/"/>
    <category term="Prompt Engineering" scheme="https://xilidou.com/tags/Prompt-Engineering/"/>
    <content>
      <![CDATA[<p>Claude code tools 研究系列第六篇。前五篇拆完了「交互原语三件套」(Ask &#x2F; EnterPlanMode &#x2F; ExitPlanMode) 和执行原语链条的前两环 —— 定位工具 Grep + Glob 和感知工具 Read。前者告诉 Claude 「相关文件在哪」,后者告诉 Claude 「文件现在长什么样」。</p><p>这一篇接着 Read 讲它的搭档 —— <strong>Edit</strong>。如果 Grep + Glob 是「找到坐标」、Read 是「知道文件长什么样」,那 Edit 就是「基于这份知道去精准改」。Read 和 Edit 共享同一套 harness 追踪状态,构成「安全改代码」的完整闭环。</p><blockquote><p>本系列先读 前置篇 —— 讲清楚 tool 是什么、Claude 怎么用。本篇按前置篇提出的 4 层骨架展开。</p></blockquote><h2 id="Edit"><a href="#Edit" class="headerlink" title="Edit"></a>Edit</h2><p>如果说 Ask &#x2F; EnterPlanMode &#x2F; ExitPlanMode 是「协作时的礼仪」,那 Edit 就是「劳动时的匠气」 —— 每一次代码改动都要经过它。这个工具<strong>日均调用量远超所有交互工具的总和</strong>,但它的设计比交互工具更「刀刃向内」 —— 一条条约束都在防 AI 犯低级错误。</p><h3 id="作用"><a href="#作用" class="headerlink" title="作用"></a>作用</h3><p>Edit 是 Claude Code 内置的<strong>精准字符串替换工具</strong>。它做的事很简单:在一个已知文件里,把一段确切的文本(<code>old_string</code>)替换成另一段文本(<code>new_string</code>)。</p><p>它解决的核心问题是「AI 如何<strong>安全、精准、可审阅</strong>地改代码」:</p><ol><li><strong>只改需要改的地方</strong> —— 增量替换而不是整文件重写,破坏面最小</li><li><strong>强制基于真实文件</strong> —— 必须先 Read 过才能 Edit,禁止凭幻觉修改</li><li><strong>唯一性保护</strong> —— 目标文本在文件里必须唯一(除非显式声明批量),防止误伤</li><li><strong>可审阅的 diff</strong> —— tool call 里就能看清改了什么,不用整文件比对</li></ol><h3 id="一个具体例子"><a href="#一个具体例子" class="headerlink" title="一个具体例子"></a>一个具体例子</h3><p><strong>场景</strong>:用户说 <strong>「把 <code>handleClick</code> 这个函数名改成 <code>handleSubmit</code>,更符合它的实际语义」</strong>。</p><p>假设有一个 <code>LoginForm.tsx</code> 文件,600 行,<code>handleClick</code> 在里面出现了 4 次:1 次函数定义、2 次 JSX 里的 <code>onClick=&#123;handleClick&#125;</code>、1 次注释里的 “handleClick will…”。</p><h4 id="反例-如果没有-Edit-只有-Write"><a href="#反例-如果没有-Edit-只有-Write" class="headerlink" title="反例:如果没有 Edit(只有 Write)"></a>反例:如果没有 Edit(只有 Write)</h4><p>Claude 只能用 Write 工具<strong>整文件重写</strong>,来完成这次重命名:</p><ul><li>首先 Read 一遍这 600 行,拿到当前内容</li><li>在脑子里做 4 处替换</li><li>用 Write 把改后的 600 行整体写回文件</li></ul><p>用户会遇到几个问题:</p><ol><li><strong>Token 浪费严重</strong> —— 600 行的内容在 tool call 里被完整传两次(Read 输入 + Write 输出),而实际改动只有 4 处</li><li><strong>破坏面失控</strong> —— Write 会覆盖整个文件,如果 Claude 在传输过程中丢了个空格 &#x2F; 换错了个引号 &#x2F; 少复制一行,整个文件都被这个 bug 污染</li><li><strong>Diff 难审阅</strong> —— 用户在 tool call log 里看到的是「整文件 600 行 → 整文件 600 行」,得跑一次 diff 才能看清 Claude 到底动了什么</li><li><strong>幻觉风险</strong> —— 如果 Claude 记忆里的文件跟磁盘上的不一致(比如用户在中间刚编辑过),整文件重写等于<strong>把 Claude 记忆里的版本覆盖到磁盘</strong>,吞掉用户改动</li><li><strong>并发冲突</strong> —— 用户在另一个编辑器里刚保存了一个改动,Claude 的整文件写入会把它盖掉,连提示都没有</li></ol><p><strong>核心痛点</strong>:整文件重写把「改一处」的成本放大到「改全部」,风险面从 4 处扩散到 600 行。</p><h4 id="用-Edit-是怎么解决的"><a href="#用-Edit-是怎么解决的" class="headerlink" title="用 Edit 是怎么解决的"></a>用 Edit 是怎么解决的</h4><p>Claude 会先 Read 拿到文件,然后调 Edit,传三个参数:</p><ul><li><code>file_path</code>: <code>LoginForm.tsx</code> 的绝对路径</li><li><code>old_string</code>: <code>handleClick</code></li><li><code>new_string</code>: <code>handleSubmit</code></li><li><code>replace_all</code>: <code>true</code> (因为文件里出现了 4 次)</li></ul><p><strong>运行时会发生什么</strong>:</p><ul><li>Runtime 检查:这个文件在本次会话里被 Read 过吗?没有则直接报错</li><li>Runtime 检查:如果 <code>replace_all=false</code>,<code>old_string</code> 在文件里出现的次数<strong>必须是 1</strong>;不是 1 就报错</li><li>Runtime 执行替换:找到所有 <code>handleClick</code>,全部换成 <code>handleSubmit</code></li><li>Runtime 只把<strong>diff 部分</strong>写回文件,不动其它 596 行</li></ul><p>用户在 tool call log 里看到的是:</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">Edit(file_path: LoginForm.tsx, old_string: &quot;handleClick&quot;, new_string: &quot;handleSubmit&quot;, replace_all: true)</span><br><span class="line">→ 4 replacements</span><br></pre></td></tr></table></figure><p><strong>一目了然、无副作用、无 token 浪费</strong>。</p><h4 id="对照一下两种形式解决了反例里的哪些痛点"><a href="#对照一下两种形式解决了反例里的哪些痛点" class="headerlink" title="对照一下两种形式解决了反例里的哪些痛点"></a>对照一下两种形式解决了反例里的哪些痛点</h4><table><thead><tr><th>反例痛点</th><th>Edit 的解法</th></tr></thead><tbody><tr><td>Token 浪费严重</td><td>tool call 只传 diff 段,不传全文</td></tr><tr><td>破坏面失控</td><td>只在 <code>old_string</code> 匹配处替换,其它 596 行原封不动</td></tr><tr><td>Diff 难审阅</td><td>tool call 参数本身就是 diff,一眼看清</td></tr><tr><td>幻觉风险</td><td>Read 前置强制:没读过就报错,不允许凭记忆改</td></tr><tr><td>并发冲突</td><td>只改 4 处 · 不覆盖整个文件 · 用户的其它编辑不受影响</td></tr></tbody></table><h3 id="触发条件"><a href="#触发条件" class="headerlink" title="触发条件"></a>触发条件</h3><p>工具官方说明写得很硬:<strong>「永远优先编辑已有文件 · 不要新建文件除非明确要求」</strong>。这条原则背后是一个价值观:<strong>减少不必要的产物 · 尽量在原地修改</strong>。</p><p><strong>该用 Edit 的场景</strong>:</p><ul><li><strong>改一段已知代码</strong> —— 修 bug、重命名、调整逻辑</li><li><strong>微调配置文件</strong> —— 改一个字段值、加一行、删一行</li><li><strong>修改文档</strong> —— 更新 README 的某一段、修 typo</li><li><strong>批量重命名</strong> —— 一个变量在多处出现,用 <code>replace_all</code></li></ul><p><strong>不该用 Edit 的场景</strong>(应该用 Write 或其它工具):</p><ul><li><strong>新建文件</strong> —— Edit 不能创建文件,得用 Write</li><li><strong>完全重写文件</strong> —— 改动占文件 80% 以上,Edit 的 old_string 会很长很脆弱,不如 Write 一次性重写</li><li><strong>需要 fuzzy 匹配</strong> —— Edit 是精确字符串匹配,如果你想「找出所有 <code>console.log(...)</code> 无视括号里内容」,Edit 做不到,得写脚本</li></ul><p>一个<strong>很关键的思维模式</strong>:<strong>Edit 只处理你已经完全知道的字符串</strong>。如果你不确定文件里那段代码长什么样,那你根本不该调 Edit —— 该先 Read 看清楚,或者用 Grep 找上下文。<strong>Edit 不是「探索工具」 · 是「执行工具」</strong>。</p><h3 id="技术实现"><a href="#技术实现" class="headerlink" title="技术实现"></a>技术实现</h3><h4 id="1-·-命名"><a href="#1-·-命名" class="headerlink" title="1 · 命名"></a>1 · 命名</h4><p><code>Edit</code></p><p>一个动词概括所有职责。不叫 <code>Replace</code> &#x2F; <code>Modify</code> &#x2F; <code>Patch</code> —— 「Edit」是编辑器语义,Claude 拿到这个词第一反应就是”改现存文件的一段内容”,不会想成”创建新文件”或”追加内容”。字段名 <code>file_path</code> &#x2F; <code>old_string</code> &#x2F; <code>new_string</code> &#x2F; <code>replace_all</code> 也全是望文生义。</p><h4 id="2-·-工具级描述"><a href="#2-·-工具级描述" class="headerlink" title="2 · 工具级描述"></a>2 · 工具级描述</h4><p>Edit 的描述围绕四件事:<strong>语义定位 &#x2F; Read 先行 &#x2F; 唯一性和补救 &#x2F; 品味约束</strong>。</p><p><strong>开篇一句,奠定基调</strong></p><blockquote><p>Performs exact string replacements in files.</p></blockquote><p>“exact” 一词奠定整个工具的基调 —— 不是模糊,不是相似,不是差不多,是<strong>逐字</strong>替换。这一个词就把 Edit 从「AI 智能改代码」拉回「文本处理器」的定位。</p><p><strong>Read 先行的强制</strong></p><blockquote><p>You must use your <code>Read</code> tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.</p></blockquote><p>关键词 <strong>will error</strong> —— 不是「建议」不是「最好」,是 runtime 层的硬阻断。这条 prompt 训练 Claude 建立一个反射:<strong>想 Edit ? 先 Read。</strong></p><p><strong>行号前缀陷阱</strong></p><blockquote><p>When editing text from Read tool output, ensure you preserve the exact indentation (tabs&#x2F;spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + tab. Everything after that is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.</p></blockquote><p>这一整段专门警告一个具体陷阱。有意思的是官方把「Everything after that is the actual file content」显式说出来,可见团队被这个 bug 咬过很多次。这是<strong>从血泪教训里长出来的 prompt</strong>。</p><p><strong>偏好编辑而非新建</strong></p><blockquote><p>ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.</p></blockquote><p>关键词 <strong>ALWAYS &#x2F; NEVER</strong> —— 大写 + 极端量词。这不只是「建议」,是一种价值观声明:<strong>Claude 应该像一个尊重现有代码结构的工程师,不轻易生产新文件</strong>。</p><p>这也在防一类 AI 反模式:<strong>幻觉性生产</strong> —— AI 觉得「我应该建一个新工具类」而实际上项目里已经有一个够用的,结果堆出一堆散乱的新文件。</p><p><strong>emoji 禁令</strong></p><blockquote><p>Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.</p></blockquote><p>一条乍看奇怪的约束,专门为 Edit 加。为什么?因为 AI(尤其早期训练模型)特别爱在评论 &#x2F; 提交信息 &#x2F; 文档里塞 emoji —— 但<strong>大多数代码库不欢迎这种风格</strong>。这条约束是「代码库品味」的显式表达,让 Claude 的输出更符合专业工程惯例。</p><p><strong>唯一性失败与 replace_all</strong></p><blockquote><p>The edit will FAIL if <code>old_string</code> is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use <code>replace_all</code> to change every instance of <code>old_string</code>.</p></blockquote><p>给出<strong>两种补救路径</strong>:扩上下文 &#x2F; 用 replace_all。这一条特别贴心 —— 不只是说「会失败」,还告诉 Claude 失败后<strong>怎么办</strong>。这是好 prompt 的标志:错误路径也要设计。</p><p><strong>replace_all 的正当用法</strong></p><blockquote><p>Use <code>replace_all</code> for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.</p></blockquote><p>明确 <code>replace_all</code> 是<strong>为「变量重命名」这类场景设计的</strong>。给一个具体使用场景比笼统说「设置为 true 会全部替换」有用得多 —— Claude 读到这条会立刻在脑海里建立映射:「哦,重命名要用这个 flag」。</p><h4 id="3-·-字段级描述"><a href="#3-·-字段级描述" class="headerlink" title="3 · 字段级描述"></a>3 · 字段级描述</h4><p>Edit 有 4 个字段:</p><ul><li><code>file_path</code> —— 目标文件的<strong>绝对路径</strong>(不接受相对路径)</li><li><code>old_string</code> —— 要被替换的确切文本</li><li><code>new_string</code> —— 替换后的文本(必须跟 <code>old_string</code> 不同)</li><li><code>replace_all</code> —— 布尔值,默认 <code>false</code>;设为 <code>true</code> 时替换所有匹配</li></ul><p>字段少,但每个背后都有非平凡的设计:</p><p><strong>精确字符串匹配 · 不是 AST &#x2F; LSP &#x2F; fuzzy diff</strong></p><p>Claude Code 团队选了<strong>最原始也最鲁棒</strong>的方案 —— 纯字符串匹配。原因:</p><ul><li><strong>语言无关</strong> —— 不用为每种语言维护 parser,Python &#x2F; Rust &#x2F; YAML &#x2F; Markdown 通吃</li><li><strong>实现简单</strong> —— 不用引入 tree-sitter &#x2F; LSP 依赖</li><li><strong>失败明确</strong> —— 匹配不上就报错,不会「大概匹配到差不多的地方」</li><li><strong>Claude 可控</strong> —— Claude 输出什么字符串就替换什么,不会被 AST normalizer 悄悄改写</li></ul><p>代价是:Claude 必须<strong>逐字</strong>提供 <code>old_string</code>,包括空格、缩进、换行。这是把「解析文件的复杂度」外包给 Claude 自己 —— 而 Claude 天然擅长处理精确字符串。</p><p><strong>Read 先行的 harness 约束</strong></p><p>如果没在本次会话里 Read 过某个文件,直接 Edit 会报错。为什么?防幻觉。</p><p>Claude 可能「记得」自己上次改过某个文件长什么样,但<strong>上次是上次</strong> —— 磁盘上现在的文件可能已经被用户 &#x2F; 其它 agent &#x2F; 其它工具改过。强制 Read 前置的本质是:<strong>每次 Edit 都基于当前磁盘状态,而不是 Claude 记忆里的版本</strong>。</p><p>这条约束不是靠自律,是靠 runtime 追踪:「这个 file_path 有没有出现在本次会话的 Read tool 调用里?」没有就拒绝。</p><p><strong>唯一性检查的价值</strong></p><p>如果 <code>replace_all=false</code>(默认),Edit 会要求 <code>old_string</code> 在文件里出现<strong>恰好一次</strong>。这个约束防止一类隐蔽 bug:</p><ul><li>Claude 想改函数 A 里的 <code>return null</code>,但文件里另一个函数 B 也有 <code>return null</code></li><li>Edit 找到第一个匹配就替换,可能改错函数</li></ul><p>强制唯一性把这个歧义暴露成<strong>编辑失败</strong>,让 Claude 必须提供<strong>足够多的上下文</strong>来消除歧义 —— 比如 <code>old_string</code> 包含函数签名、周围几行,让它变得独一无二。</p><p><strong>replace_all 是重命名场景的一等公民</strong></p><p>同一个工具里既能改一处也能改全部,靠一个 flag 切换:</p><ul><li>改一个变量名,一次调用就搞定</li><li>不需要循环调用 Edit 一次一次替换</li><li>不需要写正则表达式(容易翻车)</li></ul><p><strong>line number prefix 陷阱</strong></p><p>Read 工具输出内容时会加行号前缀(格式:数字 + tab + 实际内容)。Edit 官方说明专门警告:<strong>old_string 里千万不要包含行号前缀</strong> —— 那是 Read 加上去的展示格式,不是文件真实内容。</p><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">Read 输出: 42  const x = 1;</span><br></pre></td></tr></table></figure><p>Claude 可能想直接把 <code>42  const x = 1;</code> 塞到 old_string 里 —— 错了,磁盘上根本没有 <code>42</code> 这几个字符。正确做法是取 tab 之后的部分:<code>  const x = 1;</code>。</p><p>行号前缀是 Read 的<strong>必要输出</strong>(让 Claude 有坐标系统),同时是 Edit 的<strong>必要过滤</strong>。这个”同一个东西承担两种矛盾角色”的现象,是 Read 和 Edit 深度耦合的根源。</p><h4 id="4-·-schema-校验规则"><a href="#4-·-schema-校验规则" class="headerlink" title="4 · schema 校验规则"></a>4 · schema 校验规则</h4><p>Edit 的 schema 极简:</p><table><thead><tr><th>字段</th><th>类型</th><th>约束</th></tr></thead><tbody><tr><td><code>file_path</code></td><td>string</td><td>必填 · 必须绝对路径</td></tr><tr><td><code>old_string</code></td><td>string</td><td>必填 · 默认唯一性检查</td></tr><tr><td><code>new_string</code></td><td>string</td><td>必填 · 必须 ≠ old_string</td></tr><tr><td><code>replace_all</code></td><td>boolean</td><td>可选 · 默认 false</td></tr></tbody></table><p>关键的<strong>硬拦截不在 schema 里</strong>,而在 harness 层:</p><ol><li><strong>Read 前置</strong> —— 未 Read 直接报错</li><li><strong>唯一性</strong> —— old_string 匹配 &gt; 1 处直接报错(除非 replace_all&#x3D;true)</li><li><strong>匹配失败</strong> —— old_string 找不到直接报错</li><li><strong>无操作检测</strong> —— old_string &#x3D;&#x3D; new_string 直接报错</li></ol><p>这些校验都是<strong>loud fail</strong>:Claude 收到明确的错误消息,能立刻修正;不会静默降级(比如「模糊匹配到差不多的地方」),避免 bug 在下游积累。</p><p>这也解释了为什么 Edit 的 schema 层这么简单 —— <strong>真正的约束都在 runtime 状态机里</strong>,不在参数结构里。</p><hr><h3 id="与邻居工具的分工"><a href="#与邻居工具的分工" class="headerlink" title="与邻居工具的分工"></a>与邻居工具的分工</h3><p>Edit 跟前五篇工具形成对照:</p><table><thead><tr><th>维度</th><th>三交互原语</th><th>Grep + Glob</th><th>Read</th><th>Edit</th></tr></thead><tbody><tr><td>定位</td><td>协作对齐</td><td>定位坐标</td><td>感知外部</td><td>精准执行</td></tr><tr><td>频率</td><td>关键节点</td><td>日常高频</td><td>日常高频</td><td>日常高频</td></tr><tr><td>参数</td><td>结构化(Ask)&#x2F; 空(两个 PlanMode)</td><td>pattern(不需要知路径)</td><td>file_path + 分页</td><td>4 字段(含 old_string)</td></tr><tr><td>语义</td><td>意图信号</td><td>定位坐标</td><td>感知承诺</td><td>数据操作</td></tr><tr><td>失败模式</td><td>用户驳回</td><td>匹配为空 &#x2F; head_limit 截断</td><td>文件不存在 &#x2F; PDF 超页未指定</td><td>匹配失败 &#x2F; 唯一性冲突 &#x2F; 未 Read</td></tr><tr><td>保守偏差</td><td>「不确定就规划」</td><td>「先按需搜再全读」</td><td>「不确定就读一读」</td><td>「不确定就 Read」</td></tr></tbody></table><p><strong>Edit 与前两环的深度耦合</strong>在这张表里最明显 —— Edit 的一半保守偏差(「不确定就 Read」)是<strong>外包给 Read 的</strong>;而 Read 又依赖 Grep+Glob 提供的坐标。三环通过 harness 追踪状态形成信任链:</p><ul><li>Grep &#x2F; Glob 定位:「哪些文件与这个任务相关」</li><li>Read 建立「感知承诺」:「我知道这个文件现在长什么样」</li><li>Edit 消费承诺:基于 Claude 记忆里的准确内容做精准替换</li><li>陷阱共享:行号前缀是 Read 的必要输出,是 Edit 的必要过滤</li><li>状态机协作:harness 追踪 Read 状态 → Edit 时验证 → 缺失就报错</li></ul><hr><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>Edit 的精妙之处,不在于它「让 AI 改代码」这个功能本身,而在于它的信号分布<strong>极度偏向 runtime 状态机</strong>:</p><ul><li><strong>命名</strong> —— 极简,一个动词</li><li><strong>工具级描述</strong> —— 长,7 段约束覆盖语义定位 &#x2F; Read 先行 &#x2F; 唯一性和补救 &#x2F; 品味</li><li><strong>字段级描述</strong> —— 4 字段,每个背后都是非平凡决策(纯字符串 &#x2F; harness Read 状态 &#x2F; 唯一性 &#x2F; replace_all &#x2F; 行号陷阱)</li><li><strong>schema 校验</strong> —— 极简,真正的硬拦截全在 runtime 层(Read 状态 &#x2F; 唯一性 &#x2F; 匹配失败 &#x2F; 空操作)</li></ul><p>Edit 独特的地方在于它<strong>把「安全改代码」的重心从参数校验转移到了状态机</strong>:Edit 本身几乎没有 schema 约束,但通过和 Read 共享 harness 追踪状态,构造了一个”每次编辑都基于当前磁盘真实内容”的强保证。相当于把「AI 精准改代码」这个泛用能力,收敛成一个<strong>语言无关、防幻觉、可审阅、支持批量</strong>的执行原语。</p><p>下一篇继续拆 Write —— Edit 的兄弟工具 · 处理 Edit 干不了的两类事:<strong>新建文件 · 完全重写</strong>。看看 Write 如何在「必需性」和「危险性」之间找平衡。</p>]]>
    </content>
    <id>https://xilidou.com/2026/08/07/claude-code-tools-edit/</id>
    <link href="https://xilidou.com/2026/08/07/claude-code-tools-edit/"/>
    <published>2026-08-07T10:00:00.000Z</published>
    <summary>Edit 工具如何通过唯一性、读取前置与 diff 保证安全修改。</summary>
    <title>Claude Code Tools 研究系列（六）—— Edit：精准字符串替换</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="AI" scheme="https://xilidou.com/categories/AI/"/>
    <category term="Agent" scheme="https://xilidou.com/tags/Agent/"/>
    <category term="AI" scheme="https://xilidou.com/tags/AI/"/>
    <category term="Claude Code" scheme="https://xilidou.com/tags/Claude-Code/"/>
    <category term="Tools" scheme="https://xilidou.com/tags/Tools/"/>
    <category term="Harness" scheme="https://xilidou.com/tags/Harness/"/>
    <category term="Prompt Engineering" scheme="https://xilidou.com/tags/Prompt-Engineering/"/>
    <content>
      <![CDATA[<p>Claude code tools 研究系列第五篇。前四篇拆完了「交互原语三件套」(Ask &#x2F; EnterPlanMode &#x2F; ExitPlanMode) 和搜索双人组 Grep + Glob。前者解决「AI 和用户怎么对齐」,后者解决「Claude 如何在项目里定位相关文件」。</p><p>拿到文件路径之后,Claude 需要<strong>感知这个文件当前长什么样</strong> —— 这就是 <strong>Read</strong>。它是执行原语链条里承上启下的一环:接住 Grep&#x2F;Glob 定位到的坐标 · 为后面的 Edit &#x2F; Write 建立起「感知承诺」的信任链。</p><blockquote><p>本系列先读 前置篇 —— 讲清楚 tool 是什么、Claude 怎么用。本篇按前置篇提出的 4 层骨架展开。</p></blockquote><h2 id="Read"><a href="#Read" class="headerlink" title="Read"></a>Read</h2><p>在所有 tool 里,Read 是<strong>最基础也最容易被低估</strong>的一个。它看起来只是「读一个文件」,但它承担着一个关键角色:<strong>Claude 感知外部世界的唯一合规通道</strong>。</p><p>没有 Read,Claude 只能靠训练时的记忆(过时)+ 用户在聊天里粘贴的片段(局部)+ 幻觉(危险)来构造对项目的理解。有了 Read,Claude 每一次改动才有真实的立足点。</p><h3 id="作用"><a href="#作用" class="headerlink" title="作用"></a>作用</h3><p>Read 是 Claude Code 内置的<strong>文件内容读取工具</strong>。它做的事很直白:给一个绝对路径,返回文件内容 —— 但它承担的职责远不止「读文件」这四个字:</p><ol><li><strong>给 Claude 提供磁盘真实状态</strong> —— 而不是让它靠训练记忆 &#x2F; 用户粘贴 &#x2F; 幻觉猜测</li><li><strong>前置 Edit 的必要条件</strong> —— Read 建立了 harness 层的追踪状态,Edit 才能安全地改</li><li><strong>多模态感知统一入口</strong> —— 文本 &#x2F; 图片 &#x2F; PDF &#x2F; Jupyter notebook 都走同一个工具</li><li><strong>大文件安全读取</strong> —— 分页机制(offset + limit)防止一次性把上下文吃满</li></ol><h3 id="一个具体例子"><a href="#一个具体例子" class="headerlink" title="一个具体例子"></a>一个具体例子</h3><p><strong>场景</strong>:用户说 <strong>「<code>auth/middleware.ts</code> 里 token 校验的 bug 你帮我看看,应该在 verifyToken 那段」</strong>。</p><p>Claude 直接调 Read:</p><ul><li><code>file_path</code>: <code>/Users/xxx/project/src/auth/middleware.ts</code>(<strong>绝对路径</strong>)</li></ul><p><strong>运行时返回</strong>:</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></pre></td><td class="code"><pre><span class="line"> 1import jwt from &#x27;jsonwebtoken&#x27;;</span><br><span class="line"> 2</span><br><span class="line"> 3export async function verifyToken(req, res, next) &#123;</span><br><span class="line"> 4  const token = req.headers.authorization;</span><br><span class="line"> 5  if (!token) return res.status(401).send(&#x27;unauthorized&#x27;);</span><br><span class="line"> 6  </span><br><span class="line"> 7  try &#123;</span><br><span class="line"> 8    const decoded = jwt.verify(token, process.env.JWT_SECRET);</span><br><span class="line"> 9    req.user = decoded;</span><br><span class="line">10    next();</span><br><span class="line">11  &#125; catch (err) &#123;</span><br><span class="line">12    return res.status(401).send(&#x27;invalid token&#x27;);</span><br><span class="line">13  &#125;</span><br><span class="line">14&#125;</span><br></pre></td></tr></table></figure><p><strong>每一行前面有行号 + tab 前缀</strong> —— Claude 可以据此精确定位。看到第 4 行 <code>req.headers.authorization</code> 直接暴露了 bug:没剥离 <code>Bearer </code> 前缀。</p><p>这段输出体现了 Read 的几个关键特性:</p><ul><li><strong>绝对路径 in · 磁盘真实内容 out</strong> —— Claude 拿到的是<strong>当前</strong>磁盘状态,不是训练记忆、不是聊天历史、不是幻觉</li><li><strong>行号前缀建立坐标系</strong> —— 用户说「第 4 行的 bug」Claude 立刻能定位;Claude 说「第 8 行的 jwt.verify」用户也能立刻找到</li><li><strong>默认 2000 行</strong> —— 大文件不会一次性把上下文吃满</li></ul><p><strong>其它形态的输入</strong>:</p><ul><li><strong>大文件</strong>(比如 5000 行):默认只读前 2000 行,可以指定 <code>offset: 2000, limit: 1000</code> 读第 2001~3000 行</li><li><strong>截图 &#x2F; 图片</strong>:runtime 检测扩展名,<strong>直接以视觉方式呈现</strong>给 Claude(而不是返回文本),Claude 可以「看见」报错框、堆栈、字段值</li><li><strong>PDF</strong>:超过 10 页必须指定 <code>pages: &quot;1-5&quot;</code>,每次最多 20 页,防止大文档一次性爆掉上下文</li><li><strong>Jupyter notebook</strong>:返回所有 cell 的代码 + 输出 + Markdown,一起呈现</li></ul><p><strong>核心价值</strong>:Read 是 Claude 感知外部世界的<strong>唯一合规通道</strong> —— 把「猜测 &#x2F; 记忆 &#x2F; 幻觉」替换成「知道」。所有后续的 Edit &#x2F; Write &#x2F; Bash 都建立在这份感知承诺之上。</p><h3 id="触发条件"><a href="#触发条件" class="headerlink" title="触发条件"></a>触发条件</h3><p>工具官方说明写得很清楚:<strong>「假设这个工具能读机器上任何文件」</strong> —— Claude 不该纠结「这个文件我该不该读」,如果需要就直接读。</p><p><strong>该用 Read 的场景</strong>:</p><ul><li><strong>改一个已知文件之前</strong> —— Edit &#x2F; Write 之前的必修课</li><li><strong>理解项目结构</strong> —— 读 <code>package.json</code> &#x2F; <code>tsconfig.json</code> &#x2F; <code>CLAUDE.md</code> 建立项目基线认知</li><li><strong>用户 @filename</strong> —— 用户消息里用 <code>@</code> 引用的文件,Claude 应该主动读</li><li><strong>用户 linked_note</strong> —— 系统上下文里出现的 <code>&lt;linked_note&gt;</code>,直接读</li><li><strong>读图片 &#x2F; PDF &#x2F; notebook</strong> —— 多模态感知的入口</li><li><strong>wikilink 里的嵌入图片</strong> —— 读文档时遇到 <code>image.png</code>,主动 Read 图片建立完整语境</li></ul><p><strong>不该用 Read 的场景</strong>:</p><ul><li><strong>刚刚 Edit 过的文件想「验证一下」</strong> —— harness 追踪了状态,Edit 成功就说明改动生效了,重复 Read 是浪费</li><li><strong>列目录内容</strong> —— 用 Glob 或 bash <code>ls</code>,Read 不能读目录</li><li><strong>搜文件里的关键词</strong> —— 用 Grep,Read 是全读一段,不适合搜索</li><li><strong>凭幻觉「验证」自己上一步的输出</strong> —— 如果 Edit &#x2F; Write 成功,Claude 不用回头怀疑自己</li></ul><p>一个有意思的<strong>反浪费原则</strong>:官方专门写了一条 <code>Do NOT re-read a file you just edited to verify</code> —— 意思是 harness 帮你追踪了文件状态,Claude 不用像人类程序员那样反复确认。</p><h3 id="技术实现"><a href="#技术实现" class="headerlink" title="技术实现"></a>技术实现</h3><h4 id="1-·-命名"><a href="#1-·-命名" class="headerlink" title="1 · 命名"></a>1 · 命名</h4><p><code>Read</code></p><p>命名极简 —— 一个动词概括所有职责。文件、图片、PDF、Jupyter notebook 全部走这一个动词,不叫 <code>ReadFile</code> &#x2F; <code>LoadImage</code> &#x2F; <code>ParsePDF</code>。<strong>统一命名对应统一入口</strong> —— Claude 不需要记多个工具名,选文件读什么由 runtime 根据扩展名分派。</p><p>字段名也是最直觉的一组:<code>file_path</code> &#x2F; <code>offset</code> &#x2F; <code>limit</code> &#x2F; <code>pages</code>,任何写过分页 API 的人一眼看懂。</p><h4 id="2-·-工具级描述"><a href="#2-·-工具级描述" class="headerlink" title="2 · 工具级描述"></a>2 · 工具级描述</h4><p>Read 的描述围绕四件事:<strong>能力宣告 &#x2F; 分页触发 &#x2F; 多模态提示 &#x2F; 反浪费禁令</strong>。</p><p><strong>全权限声明</strong></p><blockquote><p>Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid.</p></blockquote><p>训练 Claude <strong>不要质疑用户给的路径</strong>,不要犹豫「这个文件我能读吗」。信任用户 + 信任工具,直接执行。消除了 Claude 的「过度谨慎」倾向。</p><p><strong>绝对路径硬约束</strong></p><blockquote><p>The file_path parameter must be an absolute path, not a relative path</p></blockquote><p>关键词 <strong>must be</strong> —— 硬性。Claude Code 是一个跨会话、跨 CWD 的 agent,相对路径在不同上下文里会歧义:Claude 以为 CWD 是 <code>~/project</code>,实际是 <code>~/project/src</code>。强制绝对路径把 CWD 依赖去掉,<strong>每次 Read 都是自解释的</strong>。</p><p><strong>分页触发条件</strong></p><blockquote><p>When you already know which part of the file you need, only read that part. This can be important for larger files.</p></blockquote><p>这条不是硬规则,是<strong>优化建议</strong> —— 提醒 Claude「你不需要每次都从头读」。训练 Claude 建立「按需读取」的直觉。</p><p><strong>多模态能力宣告</strong></p><blockquote><p>This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.</p></blockquote><p>关键词 <strong>presented visually</strong> —— 明确告诉 Claude:图片不是被转成文本描述,而是<strong>直接进入你的视觉理解</strong>。这条 prompt 让 Claude 建立「Read 图片 &#x3D; 我能看见」的直觉,而不是「Read 图片 &#x3D; 我读了 alt 描述」。</p><p><strong>PDF 分页强制</strong></p><blockquote><p>For large PDFs (more than 10 pages), you MUST provide the pages parameter to read specific page ranges (e.g., pages: “1-5”). Reading a large PDF without the pages parameter will fail. Maximum 20 pages per request.</p></blockquote><p>关键词 <strong>MUST &#x2F; will fail</strong> —— 硬阻断。跟 Edit 的 Read 先行是同一种设计哲学:<strong>不合规范的调用不允许,而不是允许后返回错误结果</strong>。</p><p><strong>处理截图的社交指令</strong></p><blockquote><p>You will regularly be asked to read screenshots. If the user provides a path to a screenshot, ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths.</p></blockquote><p>这条是<strong>社交行为训练</strong> —— 明确告诉 Claude「用户给你截图路径就直接读」。防止 Claude 出现「用户给我一个路径,我该不该读?」的犹豫。</p><p><strong>空文件的行为约定</strong></p><blockquote><p>If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.</p></blockquote><p>这条 prompt 让 Claude 提前知道<strong>空文件不会返回空字符串</strong>,避免看到 reminder 时误以为「工具出错了」。这是<strong>用体贴的错误消息代替 silent fail</strong>的设计。</p><p><strong>反浪费(不要 verify)</strong></p><blockquote><p>Do NOT re-read a file you just edited to verify — Edit&#x2F;Write would have errored if the change failed, and the harness tracks file state for you.</p></blockquote><p>这条特别有意思 —— 它是在<strong>扭转 Claude 的一个本能倾向</strong>。Claude 训练时可能学到「改完代码要 verify」的编程直觉,但在 Claude Code 里 verify 是浪费,因为 harness 已经追踪了状态。这条 prompt 显式关掉了这个多余行为。</p><h4 id="3-·-字段级描述"><a href="#3-·-字段级描述" class="headerlink" title="3 · 字段级描述"></a>3 · 字段级描述</h4><p>Read 有 4 个字段:</p><ul><li><code>file_path</code> —— 目标文件的<strong>绝对路径</strong>(不接受相对路径)</li><li><code>offset</code> —— 从第几行开始读(可选,默认 0)</li><li><code>limit</code> —— 最多读多少行(可选,默认 2000)</li><li><code>pages</code> —— PDF 的页码范围(如 <code>&quot;1-5&quot;</code>,只对 PDF 生效)</li></ul><p><strong>几个关键设计点</strong>:</p><p><strong>行号 + tab 前缀的双重角色</strong></p><p>Read 返回内容时,每行前面加 <code>行号 + tab + 实际内容</code> 的前缀。这个设计一石二鸟:</p><ul><li><strong>给 Claude 坐标系统</strong> —— Claude 能说「第 42 行的 bug」,用户能定位</li><li><strong>给 Edit 制造陷阱</strong> —— 前缀不是文件真实内容,Edit 时必须剥离(见下一篇 Edit 的详细讨论)</li></ul><p>行号前缀是「感知友好」和「操作陷阱」的<strong>同一个东西</strong>。这也是为什么 Edit 的 prompt 专门用一整段警告这个陷阱 —— <strong>它是 Read 的必要输出,也是 Edit 的必要过滤</strong>。</p><p><strong>分页机制:offset + limit</strong></p><p>为什么默认 2000 行?</p><ul><li>Claude 单次 context 有限,大文件全塞进去会挤爆</li><li>大多数场景下,只需要文件的某一段(比如某个函数)</li><li>强制 Claude 学会「按需读」而不是「全部读」</li></ul><p>分页的存在也隐含了一个哲学:<strong>Claude 不需要看完整个文件才能改一段代码</strong> —— 就像人类程序员打开一个 5000 行文件,也是滚到 verifyToken 函数附近就够了。</p><p><strong>多模态统一入口</strong></p><p>Read 不是「只能读文本」的工具。图片 &#x2F; PDF &#x2F; notebook 都走同一个 tool call:</p><ul><li><strong>图片(PNG&#x2F;JPG&#x2F;GIF&#x2F;WebP)</strong> —— runtime 检测扩展名,把图片以视觉 token 塞给 Claude,而不是文本描述</li><li><strong>PDF</strong> —— runtime 提取文本(超过 10 页强制指定页码防爆),嵌入图像也保留</li><li><strong>Jupyter notebook</strong> —— cell 结构、代码、输出、Markdown 全部返回</li></ul><p><strong>这是「统一感知层」的设计</strong> —— Claude 不用为不同格式学不同工具,一律 Read。runtime 负责把多种格式规范化成 Claude 能吃的输入。</p><p><strong>与 Edit 的 harness 协作</strong></p><p>Read 的一个隐藏职责是<strong>为 Edit 建立追踪状态</strong>。Runtime 会记录:「本次会话里,Claude Read 过哪些文件」。当 Claude 调 Edit 时,runtime 检查这个记录 —— 没读过就报错。</p><p>这个协作让 Read 不只是「读文件」,而是<strong>「感知承诺」</strong> —— Claude 承诺「我知道这个文件当前长什么样」。这个承诺被 Edit 消费,构成整套「基于真实状态改代码」的信任链。</p><h4 id="4-·-schema-校验规则"><a href="#4-·-schema-校验规则" class="headerlink" title="4 · schema 校验规则"></a>4 · schema 校验规则</h4><p>Read 的 schema 层几乎没有硬约束,除了一条:</p><table><thead><tr><th>字段</th><th>类型</th><th>约束</th></tr></thead><tbody><tr><td><code>file_path</code></td><td>string</td><td>必填 · 必须绝对路径</td></tr><tr><td><code>offset</code></td><td>integer</td><td>可选 · 默认 0</td></tr><tr><td><code>limit</code></td><td>integer</td><td>可选 · 默认 2000</td></tr><tr><td><code>pages</code></td><td>string</td><td>可选 · PDF &gt; 10 页时<strong>必填</strong></td></tr></tbody></table><p><strong>默认值是 Read 的核心设计</strong> —— 2000 行默认让 Claude 用默认值就落在”够用又不爆炸”的档位。PDF &gt; 10 页强制 pages 是唯一的硬拦截,防止大文档一次性把 context 吃满。</p><hr><h3 id="与邻居工具的分工"><a href="#与邻居工具的分工" class="headerlink" title="与邻居工具的分工"></a>与邻居工具的分工</h3><p>Read 跟前四篇工具形成对照:</p><table><thead><tr><th>维度</th><th>三交互原语</th><th>Grep + Glob</th><th>Read</th></tr></thead><tbody><tr><td>定位</td><td>协作对齐</td><td>定位坐标</td><td>感知外部</td></tr><tr><td>频率</td><td>关键节点</td><td>日常高频</td><td>日常高频</td></tr><tr><td>输入</td><td>结构化(Ask)&#x2F; 空(两个 PlanMode)</td><td>pattern(不需要知道路径)</td><td>file_path + 分页(需要知道路径)</td></tr><tr><td>输出</td><td>用户决策</td><td>路径列表 &#x2F; 匹配行 &#x2F; 计数</td><td>完整文件内容</td></tr><tr><td>保守偏差</td><td>「不确定就规划」</td><td>「先按需搜再全读」</td><td>「不确定就读一读」</td></tr></tbody></table><p><strong>Grep+Glob → Read 的信任链</strong> —— 是搜索到感知的<strong>顺畅衔接</strong>:</p><ul><li>Grep&#x2F;Glob 输出<strong>坐标</strong>(文件路径 + 可选行号),但只包含匹配行片段</li><li>Read 消费这些坐标 —— 挑出真正需要深入的文件,拉取完整上下文</li><li>Read 建立<strong>感知承诺</strong>,交给下一步的 Edit &#x2F; Write 消费</li></ul><p><strong>Read 与 Edit 的关系</strong> —— 是 Claude Code 里最紧密的一对工具搭档:</p><ul><li><strong>感知承诺</strong>:Read 是「我知道这个文件现在长什么样」的承诺</li><li><strong>操作依据</strong>:Edit 消费这个承诺,基于 Claude 记忆里的准确内容做精准替换</li><li><strong>陷阱共享</strong>:行号前缀是 Read 的必要输出,同时是 Edit 的必要过滤</li><li><strong>状态机协作</strong>:harness 层追踪 Read 状态 → Edit 时验证 → 缺失就报错</li></ul><p>如果说 AskUserQuestion &#x2F; EnterPlanMode &#x2F; ExitPlanMode 是三个原语组成的<strong>协作对齐流水线</strong>,那 Grep+Glob → Read → Edit &#x2F; Write 就是<strong>执行环节的完整流水线</strong> —— 定位、感知、执行,共享一套 harness 追踪状态,组合起来才构成「安全改代码」的完整闭环。</p><hr><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>Read 的精妙之处,不在于它「读文件」这个功能本身,而在于它的信号分布<strong>全压在工具级描述和字段设计上</strong>:</p><ul><li><strong>命名</strong> —— 极简,一个动词覆盖多模态(文本 &#x2F; 图片 &#x2F; PDF &#x2F; notebook)</li><li><strong>工具级描述</strong> —— 长,8 条约束串起「能力宣告 · 分页触发 · 多模态提示 · 反浪费禁令」</li><li><strong>字段级描述</strong> —— 4 字段但每个都有非平凡的设计(绝对路径 &#x2F; 行号双角色 &#x2F; 分页 &#x2F; PDF pages)</li><li><strong>schema 校验</strong> —— 极简,只有”PDF &gt; 10 页必须 pages”这一条硬拦截</li></ul><p>Read 最独特的地方是它是<strong>感知原语</strong> —— 把「猜测 &#x2F; 记忆 &#x2F; 幻觉」替换成「知道」,并把这份「知道」以 harness 追踪状态的形式<strong>承诺</strong>给下游 Edit &#x2F; Write。这份承诺是整个执行原语体系的<strong>信任地基</strong>。</p><p>下一篇继续拆 Edit —— 看看 Read 建立的感知承诺,是怎么被 Edit 消费成一次次精准的字符串替换的。</p>]]>
    </content>
    <id>https://xilidou.com/2026/08/06/claude-code-tools-read/</id>
    <link href="https://xilidou.com/2026/08/06/claude-code-tools-read/"/>
    <published>2026-08-06T10:00:00.000Z</published>
    <summary>Read 工具的职责、分页机制与安全读取设计。</summary>
    <title>Claude Code Tools 研究系列（五）—— Read：感知外部世界的唯一合规通道</title>
    <updated>2026-08-08T02:04:49.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="AI" scheme="https://xilidou.com/categories/AI/"/>
    <category term="Agent" scheme="https://xilidou.com/tags/Agent/"/>
    <category term="AI" scheme="https://xilidou.com/tags/AI/"/>
    <category term="Claude Code" scheme="https://xilidou.com/tags/Claude-Code/"/>
    <category term="Tools" scheme="https://xilidou.com/tags/Tools/"/>
    <category term="Harness" scheme="https://xilidou.com/tags/Harness/"/>
    <category term="Prompt Engineering" scheme="https://xilidou.com/tags/Prompt-Engineering/"/>
    <content>
      <![CDATA[<p>Claude code tools 研究系列第四篇。前三篇拆完了「交互原语三件套」—— AskUserQuestion、EnterPlanMode、ExitPlanMode。这三个工具解决「AI 和用户怎么对齐」。</p><p>从这篇开始,进入<strong>执行原语</strong>的世界 —— Claude 拿到方案后,怎么把代码真的改出来。但在读、改、写之前,先要<strong>知道去哪里读改写</strong>。所以第一个要拆的执行原语是<strong>搜索双人组</strong>:Glob 按路径找 · Grep 按内容找。</p><blockquote><p>本系列先读 前置篇 —— 讲清楚 tool 是什么、Claude 怎么用。本篇按前置篇提出的 4 层骨架展开。</p></blockquote><h2 id="Grep-Glob"><a href="#Grep-Glob" class="headerlink" title="Grep + Glob"></a>Grep + Glob</h2><p>Claude 刚进入一个新项目时,根本不知道文件路径 —— 「auth 相关代码在哪?」「哪些文件用了 useState?」「最近改过的文件是哪些?」这类问题,如果没有搜索工具,Claude 只能靠训练记忆猜(不准)或让用户手动列(累)。</p><p>Claude Code 里搜索是双人组:<strong>Glob 按路径找 · Grep 按内容找</strong>。这一篇把两个工具合并讲,因为它们功能高度耦合、经常组合使用,拆开写会重复很多。</p><p>它们共享一个核心哲学:<strong>按需感知 · 只把 Claude 真正需要看的东西送到 context 里</strong>。这也是接下来要拆的 Read &#x2F; Edit &#x2F; Write 三个「文件操作原语」的前置工具。</p><h3 id="作用"><a href="#作用" class="headerlink" title="作用"></a>作用</h3><p><strong>Glob</strong> 是<strong>按路径 pattern 找文件</strong>的工具 —— 输入一个 shell glob (<code>**/*.ts</code> &#x2F; <code>src/**/api-*.js</code>),返回匹配的文件路径列表,按修改时间倒序排。</p><p><strong>Grep</strong> 是<strong>按内容找文件 &#x2F; 找行</strong>的工具 —— 底层是 ripgrep,输入一个正则表达式,返回匹配的文件路径 &#x2F; 匹配行 &#x2F; 匹配数量(三种输出模式可选)。</p><p>它们共同解决的核心问题是「Claude 如何在一个庞大 codebase 里<strong>定位到需要看的文件</strong>」:</p><ol><li><strong>不用整读整个项目</strong> —— 定位到需要看的文件再 Read,省 context</li><li><strong>不用猜文件在哪</strong> —— 相比训练记忆,搜索直接给磁盘真相</li><li><strong>不用拼 Bash 命令</strong> —— 专用 tool 避开 shell escape &#x2F; 路径依赖 &#x2F; 权限问题</li><li><strong>输出模式可控</strong> —— 尤其 Grep,三档 output_mode 让 Claude 按需拿数据</li></ol><h3 id="一个具体例子"><a href="#一个具体例子" class="headerlink" title="一个具体例子"></a>一个具体例子</h3><p><strong>场景</strong>:用户说 <strong>「你帮我看看 auth 相关的代码是怎么组织的 · 我要 refactor」</strong>。</p><p>Claude 完全不知道 auth 代码在哪:可能在 <code>src/auth/</code>、<code>server/middleware/</code>、<code>lib/security/</code>,也可能散在 <code>pages/api/login.ts</code> 里。</p><h4 id="反例-如果只有-Read"><a href="#反例-如果只有-Read" class="headerlink" title="反例:如果只有 Read"></a>反例:如果只有 Read</h4><p>Claude 没有搜索工具,只能:</p><ul><li><strong>凭训练记忆猜</strong> —— 「Node.js 项目 auth 一般在 <code>src/middleware/auth.js</code>」,Read 过去发现不存在</li><li><strong>让用户列文件</strong> —— 「auth 相关的文件路径能告诉我吗?」用户手动列一堆,累</li><li><strong>整个 src&#x2F; 都 Read 一遍</strong> —— 一个中等项目就 200 个文件,几十万 token,context 直接爆</li></ul><p><strong>痛点</strong>:没有搜索 &#x3D; Claude <strong>看不清 codebase 的形状</strong> · 只能靠间接信息或暴力全读。</p><h4 id="用-Grep-Glob-是怎么解决的"><a href="#用-Grep-Glob-是怎么解决的" class="headerlink" title="用 Grep + Glob 是怎么解决的"></a>用 Grep + Glob 是怎么解决的</h4><p><strong>Step 1 · 用 Glob 先摸文件轮廓</strong></p><p>Claude 调 Glob:</p><ul><li><code>pattern</code>: <code>**/*&#123;auth,login,session,jwt&#125;*</code>(匹配路径 &#x2F; 文件名里带这些关键词的)</li></ul><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">src/auth/middleware.ts    (2h ago)</span><br><span class="line">src/auth/routes.ts        (2h ago)</span><br><span class="line">src/lib/session-store.ts  (3d ago)</span><br><span class="line">src/pages/api/login.ts    (1w ago)</span><br><span class="line">tests/auth.test.ts        (2h ago)</span><br></pre></td></tr></table></figure><p>按修改时间倒序 —— <strong>最近改过的排前面</strong>,通常是主战场。</p><p><strong>Step 2 · 用 Grep 深挖具体调用</strong></p><p>Claude 想知道「哪里在用 <code>jwt.verify</code>」:</p><ul><li><code>pattern</code>: <code>jwt\.verify</code></li><li><code>output_mode</code>: <code>content</code>(返回匹配行 + 文件路径 + 行号)</li><li><code>-C</code>: <code>2</code>(前后各 2 行上下文)</li><li><code>type</code>: <code>ts</code></li></ul><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">src/auth/middleware.ts:8:      const decoded = jwt.verify(token, process.env.JWT_SECRET);</span><br><span class="line">src/auth/middleware.ts:9:      req.user = decoded;</span><br><span class="line">--</span><br><span class="line">src/services/api-client.ts:42:  return jwt.verify(token, PUBLIC_KEY);</span><br><span class="line">--</span><br></pre></td></tr></table></figure><p><strong>每一条都是精确坐标 · 立刻可以 Read 或 Edit</strong>。</p><p><strong>Step 3 · 组合使用</strong></p><p>如果 Claude 只想知道<strong>多少个文件用了 jwt.verify</strong>(而不是具体在哪):</p><ul><li><code>pattern</code>: <code>jwt\.verify</code></li><li><code>output_mode</code>: <code>count</code></li></ul><p>返回 <code>4 files</code>。这次调用只花几十 token,不用把匹配行都拉进 context。</p><p>如果只想知道<strong>哪些文件用到</strong>(路径列表,不要具体行):</p><ul><li><code>pattern</code>: <code>jwt\.verify</code></li><li><code>output_mode</code>: <code>files_with_matches</code></li></ul><p>返回文件路径列表。</p><p><strong>核心洞察</strong>:Grep 的三档 output_mode(<strong>content &#x2F; files_with_matches &#x2F; count</strong>)让 Claude 可以<strong>按需精度地拿数据</strong> —— 想深挖就拿匹配行 · 想缩小范围就拿路径列表 · 想估算规模就拿计数。</p><h3 id="触发条件"><a href="#触发条件" class="headerlink" title="触发条件"></a>触发条件</h3><p><strong>什么时候用 Glob</strong>:</p><ul><li><strong>按文件名 &#x2F; 路径找</strong> —— 「所有 <code>.tsx</code> 文件」&#x2F;「<code>src/api/</code> 下所有文件」&#x2F;「测试文件在哪」</li><li><strong>按修改时间找</strong> —— 「最近改过的文件」(Glob 默认按 mtime 倒序)</li><li><strong>组合 Grep 前先缩范围</strong> —— 先 Glob 缩到相关文件,再 Grep 深挖</li></ul><p><strong>什么时候用 Grep</strong>:</p><ul><li><strong>按内容找文件 &#x2F; 行</strong> —— 「哪里用了 useEffect」&#x2F;「哪里定义了 UserBadge」</li><li><strong>调查 API 使用面</strong> —— 「所有调用 <code>db.query</code> 的地方」</li><li><strong>搜错误信息</strong> —— 用户贴了一段报错,搜 codebase 里哪里可能抛这个</li></ul><p><strong>什么时候两者组合</strong>:</p><ul><li><strong>大 codebase 里定位模块</strong> —— 先 Glob 缩到 <code>**/*auth*</code> 相关文件 · 再 Grep 具体调用</li><li><strong>限定语言 &#x2F; 类型</strong> —— 只在 <code>.ts</code> 文件里搜 —— Grep 的 <code>type: &quot;ts&quot;</code> 直接搞定,不用 Glob 前置</li></ul><p><strong>什么时候不该用</strong>:</p><ul><li><strong>知道确切路径的 Read</strong> —— 直接 Read,不用 Grep&#x2F;Glob 兜圈子</li><li><strong>列目录</strong>(而不是找 pattern) —— 用 Bash <code>ls</code>,Glob 是 pattern 匹配不是目录浏览</li><li><strong>模糊语义搜索</strong>(比如「找所有做认证的代码」) —— Grep 只能字面 &#x2F; 正则匹配,不理解语义,应该用 Agent 派 subagent 去调研</li></ul><h3 id="技术实现"><a href="#技术实现" class="headerlink" title="技术实现"></a>技术实现</h3><p>Grep 和 Glob 是<strong>姊妹工具</strong> —— 分工清晰但共享设计理念。分开拆 4 层,再看一次它们的对偶。</p><hr><h2 id="Glob"><a href="#Glob" class="headerlink" title="Glob"></a>Glob</h2><h4 id="1-·-命名"><a href="#1-·-命名" class="headerlink" title="1 · 命名"></a>1 · 命名</h4><p><code>Glob</code></p><p>命名直接借用 shell &#x2F; Python glob 库的行业约定 —— 「glob」就是”按路径 pattern 找文件”的通用叫法。字段 <code>pattern</code> &#x2F; <code>path</code> 都是任何 shell 用户直觉能懂的名字。</p><p>如果叫 <code>FindByPath</code> 或 <code>SearchFiles</code>,反而弱化了「用 glob 语法而非正则」这个核心承诺。命名本身就在暗示语法。</p><h4 id="2-·-工具级描述"><a href="#2-·-工具级描述" class="headerlink" title="2 · 工具级描述"></a>2 · 工具级描述</h4><p>Glob 的描述极短,只有 5 条 bullet,围绕两件事:<strong>用法约束 · 边界外包</strong>。</p><p><strong>pattern 是 glob 语法,不是正则</strong></p><blockquote><p>Supports glob patterns like “<strong>&#x2F;*.js” or “src&#x2F;</strong>&#x2F;*.ts”</p></blockquote><p>只给两个例子、不给正则例子。<strong>用示范代替禁令</strong> —— 与其写”不要用正则”,不如让 Claude 看到 <code>**/*.js</code> 这种典型 glob 形态。防止 Claude 把 <code>.*\.ts</code> 塞进 pattern 里。</p><p><strong>返回按修改时间倒序</strong></p><blockquote><p>Returns matching file paths sorted by modification time</p></blockquote><p>声明输出的 total order。这条描述让 Claude 建立直觉:<strong>Glob 返回的第一个文件是最近改的</strong>。「找项目主战场」&#x2F;「找刚 refactor 过的模块」这类任务直接吃头几条就够。</p><p><strong>明确用途:按文件名找</strong></p><blockquote><p>Use this tool when you need to find files by name patterns</p></blockquote><p>虽然 Grep 也有 <code>glob:</code> 字段能过滤路径,但那是<strong>过滤</strong>不是<strong>搜索</strong>。Glob 是文件名的一等公民工具。</p><p><strong>开放式搜索转 Agent · 不硬扛</strong></p><blockquote><p>When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead</p></blockquote><p>这条最有意思:<strong>主动承认自己的边界</strong>。如果任务需要多轮 glob + grep 交替(比如「找哪个模块最近改坏了」),描述主动让 Claude 换 Agent tool,别在单次 Glob 里死磕。</p><h4 id="3-·-字段级描述"><a href="#3-·-字段级描述" class="headerlink" title="3 · 字段级描述"></a>3 · 字段级描述</h4><ul><li><code>pattern</code> —— shell glob 表达式(<code>**/*.js</code> &#x2F; <code>src/**/*.&#123;ts,tsx&#125;</code>),不是正则</li><li><code>path</code> —— 可选,限定搜索目录(默认 CWD)</li></ul><p>字段极简。<strong>修改时间排序</strong>是个隐藏的宝贝设计:人类程序员想「最近在改哪块」时,直觉就是看 <code>ls -lt</code>。Glob 默认输出按 mtime 倒序,<strong>让 Claude 一眼看到项目主战场</strong>。冷代码沉在下面,热代码浮在上面。</p><h4 id="4-·-schema-校验规则"><a href="#4-·-schema-校验规则" class="headerlink" title="4 · schema 校验规则"></a>4 · schema 校验规则</h4><p><strong>极简</strong>。只有 <code>pattern</code> 必填、<code>path</code> 可选,没有额外的数值约束。</p><p>Glob 的信号几乎全在<strong>命名 + 工具描述</strong>上。schema 层不加限制,因为 glob 语法本身已经足够收敛。</p><hr><h2 id="Grep"><a href="#Grep" class="headerlink" title="Grep"></a>Grep</h2><h4 id="1-·-命名-1"><a href="#1-·-命名-1" class="headerlink" title="1 · 命名"></a>1 · 命名</h4><p><code>Grep</code></p><p>同样借用行业约定 —— 「grep」是 Unix 世界公认的”按内容匹配”操作。但要注意,tool 底下用的是 <strong>ripgrep</strong>(rg),不是传统 grep。命名保留最熟悉的名字降低认知门槛,内部升级到更快的引擎。</p><h4 id="2-·-工具级描述-1"><a href="#2-·-工具级描述-1" class="headerlink" title="2 · 工具级描述"></a>2 · 工具级描述</h4><p>Grep 的描述比 Glob 详一档,7 条 bullet + 一句宣言,围绕四件事:<strong>双向锁死用法 · 语法说明 · 过滤维度 · 边界外包</strong>。</p><p><strong>ALWAYS · NEVER · 双向锁死</strong></p><blockquote><p>ALWAYS use Grep for search tasks. NEVER invoke <code>grep</code> or <code>rg</code> as a Bash command. The Grep tool has been optimized for correct permissions and access.</p></blockquote><p><strong>整个 Grep 描述里最重的一句</strong>。ALWAYS + NEVER 双向锁死:正面说要用什么、反面禁止哪条捷径、加一句「已优化 permissions 和 access」把「为什么」也答了。防的是 Claude 熟稔 shell 后本能地想走 <code>Bash(&quot;rg foo&quot;)</code> —— bash 里 rg 输出不结构化,也过不了权限层。</p><p><strong>pattern 是 ripgrep 正则</strong></p><blockquote><p>Supports full regex syntax (e.g., “log.*Error”, “function\s+\w+”)</p></blockquote><p>跟 Glob 明确对立 —— Grep 的 pattern 是<strong>正则</strong>。给两个真实感很强的例子:<code>log.*Error</code>(找日志 error)、<code>function\s+\w+</code>(找函数定义),Claude 一看就知道语法风格。</p><p><strong>两条过滤维度 · glob vs type</strong></p><blockquote><p>Filter files with glob parameter (e.g., “<em>.js”, “**&#x2F;</em>.tsx”) or type parameter (e.g., “js”, “py”, “rust”)</p></blockquote><p>给 Claude 两条并列的路径:走 <code>glob:</code>(精确路径 pattern)或走 <code>type:</code>(语言 shortcut,ripgrep 内置表)。type 是 ripgrep 特色 —— 一个 <code>type:rust</code> 顶写 <code>**/*.&#123;rs,toml&#125;</code> 那种。</p><p><strong>output_mode 默认 files_with_matches</strong></p><blockquote><p>Output modes: “content” shows matching lines, “files_with_matches” shows only file paths (default), “count” shows match counts</p></blockquote><p><strong>注意 “(default)” 标在 files_with_matches 上</strong>。为什么不是 <code>content</code>?因为 <strong>content 最耗 context</strong>,把它设成默认容易爆。默认拿路径列表,Claude 再决定要不要深挖。这是<strong>尊重 token 预算的默认值</strong>。</p><p><strong>开放式搜索转 Agent(和 Glob 对称)</strong></p><blockquote><p>Use Agent tool for open-ended searches requiring multiple rounds</p></blockquote><p>跟 Glob 完全对称。两个工具<strong>成对声明自己的边界</strong> —— 遇到多轮迭代场景,换 Agent。</p><p><strong>ripgrep 不是 grep · 字面量要转义</strong></p><blockquote><p>Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use <code>interface\&#123;\&#125;</code> to find <code>interface&#123;&#125;</code> in Go code)</p></blockquote><p>给一个<strong>具体的踩坑例子</strong>:找 Go 代码里的 <code>interface&#123;&#125;</code>,得写成 <code>interface\&#123;\&#125;</code>。为什么专门讲这个?因为 <code>&#123;&#125;</code> 在 ripgrep 里是<strong>量化范围符</strong>(<code>a&#123;2,3&#125;</code> 表示重复 2-3 次),Claude 若按 grep 直觉写 <code>interface&#123;&#125;</code> 会报正则错。<strong>用一个真实例子代替长篇语法讲解</strong>。</p><p><strong>multiline 默认关闭 · 显式开启</strong></p><blockquote><p>Multiline matching: By default patterns match within single lines only. For cross-line patterns like <code>struct \&#123;[\s\S]*?field</code>, use <code>multiline: true</code></p></blockquote><p><strong>默认单行匹配</strong> —— 这条防的是 Claude 写了个跨行正则却拿不到匹配还不知道为啥。给个具体例子:找 Go struct 内的 <code>field</code> 声明,得 <code>multiline: true</code>。<strong>默认关 + 显式开</strong>这个 pattern 用了两次(这条 + Read 的 pages 参数),都是”贵” behavior 走显式开关。</p><h4 id="3-·-字段级描述-1"><a href="#3-·-字段级描述-1" class="headerlink" title="3 · 字段级描述"></a>3 · 字段级描述</h4><p>Grep 的字段远比 Glob 丰富:</p><ul><li><code>pattern</code> —— 正则表达式(ripgrep 语法)</li><li><code>path</code> —— 可选,限定搜索目录</li><li><code>glob</code> —— 可选,只搜匹配 glob 的文件(比如 <code>&quot;*.ts&quot;</code>)</li><li><code>type</code> —— 可选,只搜特定语言(<code>ts</code> &#x2F; <code>py</code> &#x2F; <code>rust</code>)</li><li><code>output_mode</code> —— <code>content</code> &#x2F; <code>files_with_matches</code>(默认) &#x2F; <code>count</code></li><li><code>head_limit</code> —— 限制输出行数</li><li><code>-i</code> —— 大小写不敏感</li><li><code>-n</code> —— 显示行号(content 模式默认加)</li><li><code>-A</code> &#x2F; <code>-B</code> &#x2F; <code>-C</code> —— 后 &#x2F; 前 &#x2F; 前后上下文行数(仅 content 模式)</li><li><code>multiline</code> —— 允许模式跨行匹配</li><li>format flags(<code>-c</code>, <code>-l</code>, <code>-L</code>, <code>-o</code>, <code>-Z</code>) —— 让 grep 走原生,不做包装</li></ul><p><strong>几个关键设计点</strong>:</p><p><strong>output_mode 三档设计</strong> —— 这是 Grep 最精妙的部分。同一个搜索,可以出三种精度:</p><ul><li><code>content</code>(全量匹配行) —— 需要看具体在哪、上下文什么样</li><li><code>files_with_matches</code>(仅文件路径) —— 只想知道涉及哪些文件</li><li><code>count</code>(仅计数) —— 只想知道规模</li></ul><p>对应三种典型意图:「我要 fix」(content)&#x2F;「我要重构」(files_with_matches)&#x2F;「我要评估」(count)。Grep 让 Claude <strong>按意图选精度</strong>,避免每次都拿全量数据浪费 context。</p><p><strong>head_limit 的兜底</strong> —— 一个 <code>console.log</code> 搜索可能返回 1000 行,不做限制会把 context 灌爆。<code>head_limit: 50</code> 让 Grep 只返回前 50 条,<strong>够用又不爆炸</strong>。注意 head_limit 前的排序对 Grep 是<strong>按文件路径字典序</strong>,对 Glob 是<strong>按修改时间倒序</strong>,不是相关性排序、只是截断。</p><p><strong>type vs glob 两种缩范围</strong> —— type 是 ripgrep 基于文件内容&#x2F;扩展名的语言识别,认识 <code>.py</code> <code>.rs</code> <code>.ts</code> 这类;glob 是纯路径匹配,能处理特殊路径(比如 <code>**/legacy/**/*.js</code> 排除某个目录)。type 更简洁,glob 更灵活。</p><p><strong>「format flags 原样透传」的降级通道</strong> —— 当 tool 的规范化输出不够用时,Claude 可以「掉到」原生 ripgrep 的能力。设计者知道自己包装不完美,留了个逃生舱。</p><h4 id="4-·-schema-校验规则-1"><a href="#4-·-schema-校验规则-1" class="headerlink" title="4 · schema 校验规则"></a>4 · schema 校验规则</h4><p>Grep 的 schema 层也<strong>几乎没有硬约束</strong>(数值限制、字符长度),所有约束都是<strong>枚举</strong>:</p><table><thead><tr><th>字段</th><th>类型</th><th>约束</th></tr></thead><tbody><tr><td><code>output_mode</code></td><td>string</td><td>枚举 <code>content</code> &#x2F; <code>files_with_matches</code> &#x2F; <code>count</code>,默认 <code>files_with_matches</code></td></tr><tr><td><code>-i</code> &#x2F; <code>-n</code> &#x2F; <code>multiline</code></td><td>boolean</td><td>默认 false</td></tr><tr><td><code>-A</code> &#x2F; <code>-B</code> &#x2F; <code>-C</code></td><td>integer</td><td>只在 output_mode &#x3D; content 时生效</td></tr><tr><td><code>head_limit</code></td><td>integer</td><td>无默认,不填则不限</td></tr></tbody></table><p><strong>默认值是 Grep 的核心设计</strong> —— output_mode 默认 <code>files_with_matches</code>、multiline 默认关、i&#x2F;n 默认关。<strong>每个默认都朝”少输出 · 简单模式”倾斜</strong>,让 Claude 用默认值就已经在最省 context 的档位。</p><hr><h3 id="为什么专门做-Grep-Glob-而不让-Claude-用-Bash-rg"><a href="#为什么专门做-Grep-Glob-而不让-Claude-用-Bash-rg" class="headerlink" title="为什么专门做 Grep&#x2F;Glob 而不让 Claude 用 Bash + rg?"></a>为什么专门做 Grep&#x2F;Glob 而不让 Claude 用 Bash + rg?</h3><p>Bash 是 catch-all,理论上什么都能干。但直接调 rg 有一堆问题:</p><ul><li><strong>shell escape</strong> —— 正则里的 <code>$</code> <code>!</code> <code>(</code> 都可能被 shell 展开</li><li><strong>路径依赖</strong> —— rg 是不是装了?版本是啥?</li><li><strong>输出解析</strong> —— Bash 返回一大坨文本,Claude 得自己解析</li><li><strong>没有 output_mode 分档</strong> —— rg 的 flag 太多,Claude 得记</li></ul><p>专用 tool 把这些痛点全解决了:参数 typed、输出规范化、无 shell 陷阱、Claude 一次搞定。这也是 Grep 描述里那句 “ALWAYS use Grep… NEVER invoke grep or rg as a Bash command” 的<strong>技术底座</strong>。</p><hr><h3 id="与邻居工具的分工"><a href="#与邻居工具的分工" class="headerlink" title="与邻居工具的分工"></a>与邻居工具的分工</h3><p><strong>Grep + Glob 在 Claude Code 执行原语体系里的位置</strong> —— 提前给出一个「地图」,后续几篇会逐个填充:</p><table><thead><tr><th>维度</th><th>三交互原语(已讲)</th><th>Grep + Glob(本篇)</th><th>Read(下篇)</th><th>Edit(第六篇)</th><th>Write(第七篇)</th></tr></thead><tbody><tr><td>定位</td><td>协作对齐</td><td>定位坐标</td><td>感知内容</td><td>精准执行</td><td>全量执行</td></tr><tr><td>频率</td><td>关键节点</td><td>日常高频</td><td>日常高频</td><td>日常高频</td><td>中频</td></tr><tr><td>输入</td><td>结构化 &#x2F; 空</td><td>pattern(不需要知道路径)</td><td>已知路径</td><td>已知路径 + old_string</td><td>已知路径 + 完整内容</td></tr><tr><td>输出</td><td>用户决策</td><td>路径列表 &#x2F; 匹配行 &#x2F; 计数</td><td>完整文件内容</td><td>修改后的 diff</td><td>新文件 &#x2F; 覆盖</td></tr><tr><td>保守偏差</td><td>「不确定就规划」</td><td>「先按需搜再全读」</td><td>「不确定就读」</td><td>「不确定就 Read」</td><td>「能 Edit 就 Edit」</td></tr></tbody></table><p><strong>完整调查链</strong>(把后续几篇的执行原语组合起来):</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></pre></td><td class="code"><pre><span class="line">用户: 帮我 refactor auth 相关代码</span><br><span class="line">    ↓</span><br><span class="line">Glob (**/*&#123;auth,login,session&#125;*)              ← 本篇</span><br><span class="line">    → 得到相关文件路径列表 (按 mtime 排序)</span><br><span class="line">    ↓</span><br><span class="line">Grep (pattern: &quot;jwt\.verify&quot;, output_mode: files_with_matches)  ← 本篇</span><br><span class="line">    → 得到具体使用了 API 的文件</span><br><span class="line">    ↓</span><br><span class="line">Read (每个相关文件)                              ← 下一篇</span><br><span class="line">    → 拿到完整内容,建立感知承诺</span><br><span class="line">    ↓</span><br><span class="line">Edit / Write                                     ← 后续</span><br><span class="line">    → 基于感知承诺做精准 / 全量修改</span><br></pre></td></tr></table></figure><p><strong>执行原语的信任链</strong>:</p><ul><li><strong>Glob &#x2F; Grep</strong> —— 定位:「哪些文件与这个任务相关」</li><li><strong>Read</strong> —— 感知:「这些文件当前长什么样」</li><li><strong>Edit &#x2F; Write</strong> —— 执行:「基于感知做精准 &#x2F; 全量修改」</li></ul><p>每一步都是 runtime 强制、参数 typed、输出规范化的。<strong>从一个模糊的用户需求,收敛到一次精准的文件改动</strong>,整个过程可预测、可审阅、可组合。</p><hr><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>Grep + Glob 的精妙之处,不在于「让 AI 能搜索」这个功能本身,而在于它们的信号分布<strong>极其对称又各有侧重</strong>:</p><ul><li><strong>Glob</strong> —— 命名承担核心语义(直接用行业约定)、字段极简、schema 无约束。整个工具的复杂度就是”按 glob 语法找路径”这一件事</li><li><strong>Grep</strong> —— 字段最丰富的一环(11 个字段&#x2F;flag),但 schema 层没有数值硬约束,全靠<strong>默认值收敛到最省 context 的档位</strong></li></ul><p>两个工具最精彩的一处对称:<strong>都在描述层主动承认边界</strong> —— 遇到”多轮 glob + grep 交替”这种场景,主动让 Claude 换 Agent。<strong>工具知道自己适合什么、不适合什么</strong> —— 这是 Claude Code 工具生态里非常克制、非常成熟的设计。</p><p>这也是 Claude Code 工具生态的核心哲学 —— <strong>不是给 AI 一个万能的 shell 让它自己想办法,而是把每一步都做成一个「够用 + 安全 + 可组合」的原语</strong>。</p><p>下一篇继续拆 Read —— 拿到坐标后,Claude 如何精准地感知一个文件的当前状态,为 Edit &#x2F; Write 建立起「感知承诺」的信任链。</p>]]>
    </content>
    <id>https://xilidou.com/2026/08/05/claude-code-tools-grep-glob/</id>
    <link href="https://xilidou.com/2026/08/05/claude-code-tools-grep-glob/"/>
    <published>2026-08-05T10:00:00.000Z</published>
    <summary>Grep 与 Glob 如何帮助 Claude 在大型代码库中按需定位信息。</summary>
    <title>Claude Code Tools 研究系列（四）—— Grep + Glob：搜索双人组</title>
    <updated>2026-08-08T02:04:49.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="AI" scheme="https://xilidou.com/categories/AI/"/>
    <category term="Agent" scheme="https://xilidou.com/tags/Agent/"/>
    <category term="AI" scheme="https://xilidou.com/tags/AI/"/>
    <category term="Claude Code" scheme="https://xilidou.com/tags/Claude-Code/"/>
    <category term="Tools" scheme="https://xilidou.com/tags/Tools/"/>
    <category term="Harness" scheme="https://xilidou.com/tags/Harness/"/>
    <category term="Prompt Engineering" scheme="https://xilidou.com/tags/Prompt-Engineering/"/>
    <content>
      <![CDATA[<p>Claude code tools 研究系列第三篇。前两篇拆了 AskUserQuestion 和 EnterPlanMode —— 决策流水线的前两环:澄清 · 展开。这篇聊最后一环 —— ExitPlanMode:<strong>提交方案让用户批准</strong>。</p><blockquote><p>本系列先读 前置篇 —— 讲清楚 tool 是什么、Claude 怎么用。本篇按前置篇提出的 4 层骨架展开。</p></blockquote><h2 id="ExitPlanMode"><a href="#ExitPlanMode" class="headerlink" title="ExitPlanMode"></a>ExitPlanMode</h2><p>从表面看,这可能是 Claude Code 三个交互 tool 里<strong>最不起眼</strong>的一个。它没有 AskUserQuestion 的多选卡片,也没有 EnterPlanMode 的模式切换戏剧性 —— 它只做一件事:<strong>触发一次「批准 &#x2F; 驳回」的确认</strong>。</p><p>但正是这个「什么都不做」的克制,让整套三工具流水线得以闭合。</p><h3 id="作用"><a href="#作用" class="headerlink" title="作用"></a>作用</h3><p>ExitPlanMode 是 Claude Code 内置的<strong>「规划模式退出 · 请求批准」工具</strong>。它的职责就一句话:在 plan mode 里写好完整方案后,调用这个工具,让用户看到 plan 全文并做出决定 —— <strong>批准执行 &#x2F; 让 Claude 修改 &#x2F; 驳回换方向</strong>。</p><p>它解决的核心问题是「AI 从规划切回执行时,如何得到用户显式的批准」:</p><ol><li><strong>让方案可视化</strong> —— plan 文件的完整内容被 UI 展示给用户,不是聊天里飘过的一段话</li><li><strong>强制显式决策</strong> —— 用户必须点批准 &#x2F; 驳回,不能默认继续,防止 Claude 抢跑</li><li><strong>一键切换回执行模式</strong> —— 用户批准后 Claude 自动回到可用 Edit&#x2F;Write 的默认模式</li><li><strong>保留反馈通道</strong> —— 用户可以驳回并要求修改,而不是「要么全按 plan 走 · 要么全废」</li></ol><h3 id="一个具体例子"><a href="#一个具体例子" class="headerlink" title="一个具体例子"></a>一个具体例子</h3><p><strong>场景</strong>:承接上一篇 EnterPlanMode 里那个 auth 重构例子 —— 用户说「把 JWT 换成 session cookies」,Claude 已经进 plan mode 探索完、跟用户 Ask 澄清完(只换 web 端 · session 用 Redis)、写好了 plan 文件,现在准备开始写代码。</p><p>问题来了:<strong>Claude 怎么让用户知道 plan 写完了,可以开始执行了?</strong></p><h4 id="反例-如果没有-ExitPlanMode"><a href="#反例-如果没有-ExitPlanMode" class="headerlink" title="反例:如果没有 ExitPlanMode"></a>反例:如果没有 ExitPlanMode</h4><p>Claude 只能在聊天里说:「我方案写好了,大概是这样… [几百字的方案描述] … 可以开始了吗?」</p><p>用户会遇到几个问题:</p><ol><li><strong>plan 淹没在聊天里</strong> —— 几百字的方案跟前面的探索日志、澄清对话混在一起,难阅读</li><li><strong>没有明确的批准动作</strong> —— 用户回复「OK」&#x2F;「行」&#x2F;「可以」&#x2F;「👍」都能表示同意,但语义不明确</li><li><strong>Claude 需要解析同意语义</strong> —— 拿到「行,不过 sessions 表的字段能不能加个 device_id?」这种半批准半修改的回复,不知道该继续还是回改 plan</li><li><strong>模式切换没有仪式感</strong> —— Claude 从「规划」滑到「执行」是<strong>渐变</strong>的,可能话说到一半就开始写代码,用户措手不及</li><li><strong>驳回成本高</strong> —— 用户如果发现方案有问题,得手动打字说清楚,而不是有一个「驳回并说明理由」的正规通道</li></ol><p><strong>最深层的问题</strong>:如果 Claude 想通过 AskUserQuestion 问「方案 OK 吗?」来解决这个 —— 上一篇提过,<strong>在 ExitPlanMode 触发之前,用户根本看不到 plan 全文</strong>。用 Ask 问「OK 吗?」等于让用户在真空里投票,毫无意义。</p><h4 id="用-ExitPlanMode-是怎么解决的"><a href="#用-ExitPlanMode-是怎么解决的" class="headerlink" title="用 ExitPlanMode 是怎么解决的"></a>用 ExitPlanMode 是怎么解决的</h4><p>Claude 写完 plan 文件后,直接调用 ExitPlanMode(<strong>入参也是空的</strong> —— 见下文技术实现)。UI 层做几件事:</p><p><strong>Step 1 · 展示 plan 全文</strong></p><p>界面会从 plan mode 指定的 plan 文件路径读取内容,渲染成一个<strong>独立、结构化、可滚动</strong>的方案视图。用户看到的不是「聊天里飘过的一段话」,而是一份正式的方案文档:范围 &#x2F; 影响文件 &#x2F; 迁移步骤 &#x2F; 风险 &#x2F; 回滚。</p><p><strong>Step 2 · 提供三种明确的响应通道</strong></p><ul><li>✅ <strong>批准</strong> —— Claude 回到默认模式,按 plan 执行</li><li>✏️ <strong>修改</strong> —— 用户输入反馈,Claude 回 plan mode 继续调整</li><li>❌ <strong>驳回</strong> —— 结束,换方向</li></ul><p><strong>Step 3 · 模式切换是原子的</strong></p><p>用户按下批准的一刻,runtime 做几件事:</p><ul><li>Edit &#x2F; Write &#x2F; NotebookEdit 从禁用变为可用</li><li>CWD 相关缓存刷新</li><li>Claude 拿到「用户已批准」的显式信号,开始执行</li></ul><p><strong>没有语义歧义、没有滑坡、没有 Claude 抢跑</strong>。</p><h4 id="对照一下两种形式解决了反例里的哪些痛点"><a href="#对照一下两种形式解决了反例里的哪些痛点" class="headerlink" title="对照一下两种形式解决了反例里的哪些痛点"></a>对照一下两种形式解决了反例里的哪些痛点</h4><table><thead><tr><th>反例痛点</th><th>ExitPlanMode 的解法</th></tr></thead><tbody><tr><td>plan 淹没在聊天里</td><td>UI 独立渲染 plan 文件全文,不是聊天消息</td></tr><tr><td>没有明确的批准动作</td><td>用户必须点批准 &#x2F; 修改 &#x2F; 驳回,枚举明确</td></tr><tr><td>Claude 需要解析同意语义</td><td>返回值是结构化状态(批准 &#x2F; 未批准),不是自然语言</td></tr><tr><td>模式切换没有仪式感</td><td>批准触发原子性的工具白名单切换</td></tr><tr><td>驳回成本高</td><td>「修改」是一等公民入口,不需要用户手写「你改改」</td></tr></tbody></table><h3 id="触发条件"><a href="#触发条件" class="headerlink" title="触发条件"></a>触发条件</h3><p>工具官方说明写得很直接:<strong>「只在你在 plan mode 里 · 写完 plan 文件 · 准备好接受用户批准的时候用」</strong>。</p><p><strong>该用的场景</strong>:</p><ul><li>在 plan mode 里,plan 文件写完了 —— <strong>唯一合规的调用时机</strong></li></ul><p><strong>不该用的场景</strong>:</p><ul><li><strong>纯研究任务</strong> —— 官方原文举了个反例:「搜索并理解 vim 模式的实现」这种任务,不该用 ExitPlanMode,因为你没在做「实现规划」</li><li><strong>plan 还没定型</strong> —— 半成品方案不该拿出来批准,先补完</li><li><strong>想用它做一般性询问</strong> —— 「我可以继续吗?」这种问题应该用别的通道(如果确实需要问 · 用 AskUserQuestion 澄清具体分叉,而不是问元问题)</li></ul><p>一个有意思的判断线:<strong>能被引用的方案才配触发 ExitPlanMode</strong>。如果你的方案还没到「一份可读、可审阅、可反驳的文档」的程度,那就先继续在 plan mode 里探索,别急着 exit。</p><h3 id="技术实现"><a href="#技术实现" class="headerlink" title="技术实现"></a>技术实现</h3><h4 id="1-·-命名"><a href="#1-·-命名" class="headerlink" title="1 · 命名"></a>1 · 命名</h4><p><code>ExitPlanMode</code></p><p>和 <code>EnterPlanMode</code> 完全对偶 —— <code>Enter/Exit</code> 是标准的进出配对，暗示”有始有终”的状态操作，而不是单向切换。命名直接借用文件描述符 open&#x2F;close、锁 acquire&#x2F;release 这种约定俗成的对偶范式，语义无需解释。</p><p>如果叫 <code>SubmitPlan</code> 或 <code>RequestApproval</code>，语义会滑向”提交某个数据 &#x2F; 请求某个权限”，反而弱化了它作为<strong>模式退出信号</strong>的核心语义。</p><h4 id="2-·-工具级描述"><a href="#2-·-工具级描述" class="headerlink" title="2 · 工具级描述"></a>2 · 工具级描述</h4><p>ExitPlanMode 的描述围绕三件事：<strong>什么时候用 &#x2F; 参数不传 plan 内容 &#x2F; 禁止用 Ask 问元问题</strong>。</p><p><strong>严格的适用边界（开篇）</strong></p><blockquote><p>Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval.</p></blockquote><p>三个条件叠加：<strong>在 plan mode 里 + plan 文件已写完 + 准备接受批准</strong>。任一不满足都不该调。</p><p><strong>参数机制的透明化</strong></p><blockquote><p>This tool does NOT take the plan content as a parameter - it will read the plan from the file you wrote</p></blockquote><p>明确告诉 Claude：<strong>别想着把 plan 内容塞进 tool call 参数</strong>。UI 会自己从 plan 文件读。这是防止 Claude 冗余复制 —— 既省 tokens 也确保「UI 展示的和 plan 文件一致」。</p><p><strong>批准的隐含语义</strong></p><blockquote><p>This tool simply signals that you’re done planning and ready for the user to review and approve</p></blockquote><p>关键词 <strong>signal</strong> —— 这个 tool 不做实际渲染逻辑、不做批准判定，它只发一个信号。渲染、投票、状态切换都由 runtime 处理。<strong>tool call 是最轻量的「信号发射器」</strong> —— 一个非常 Unix 哲学的设计。</p><p><strong>与研究任务的边界</strong></p><blockquote><p>IMPORTANT: Only use this tool when the task requires planning the implementation steps of a task that requires writing code. For research tasks where you’re gathering information, searching files, reading files or in general trying to understand the codebase - do NOT use this tool.</p></blockquote><p>这条呼应 EnterPlanMode 那篇也强调过的：<strong>plan mode 是「实现前的规划」，不是「理解现有代码的调研」</strong>。研究性任务应该用 Agent tool 派 subagent 去调研。</p><p><strong>禁止元问题反模式</strong></p><blockquote><p><strong>Important:</strong> Do NOT use AskUserQuestion to ask “Is this plan okay?” or “Should I proceed?” - that’s exactly what THIS tool does. ExitPlanMode inherently requests user approval of your plan.</p></blockquote><p>这条特别精妙 —— 它不是简单说「用 ExitPlanMode 别用 Ask」，而是从<strong>语义等价性</strong>角度指出：<strong>Ask 问「plan OK 吗」和 ExitPlanMode 是同一个语义，用后者才是正确表达</strong>。前两篇都提过这条反模式的存在，本篇给出了描述层的<strong>根本禁令</strong>。</p><p><strong>澄清 vs 请求批准的顺序</strong></p><p>官方 Examples 第 3 条：</p><blockquote><p>Initial task: “Add a new feature to handle user authentication” - If unsure about auth method (OAuth, JWT, etc.), use AskUserQuestion first, then use exit plan mode tool after clarifying the approach.</p></blockquote><p>明确了 Ask 和 ExitPlanMode 在 plan mode 里的<strong>执行顺序</strong>：先澄清具体分叉，再统一拿方案去批准。<strong>不要边澄清边请求批准</strong>，让流程线性收敛。</p><h4 id="3-·-字段级描述"><a href="#3-·-字段级描述" class="headerlink" title="3 · 字段级描述"></a>3 · 字段级描述</h4><p><strong>空</strong>。</p><p>有一个字段 <code>allowedPrompts</code> 但已被标记 deprecated（”Deprecated: no longer used”），实际不使用。</p><p>这个字段的历史痕迹本身很有意思：从字段名反推，早期版本可能允许 Claude 在请求批准的<strong>同时</strong>声明一批「用户批准后自动放行的操作类型」（比如 <code>run tests</code> &#x2F; <code>install dependencies</code>），让 Claude 一次性拿到复合权限。现在被弃用了，说明 Claude Code 团队后来选择了更保守的路径：<strong>批准就是批准 plan 本身，权限扩展走别的机制</strong>（比如 permissions.yaml）。这是一个<strong>权限设计从「批准即授权」演进到「批准归批准 · 授权归授权」的痕迹</strong>。</p><h4 id="4-·-schema-校验规则"><a href="#4-·-schema-校验规则" class="headerlink" title="4 · schema 校验规则"></a>4 · schema 校验规则</h4><p><strong>空</strong>。</p><p>和 EnterPlanMode 一样 —— input_schema 只有一个 deprecated 字段，无实际约束。调用行为本身 &#x3D; 提交意图，不需要传任何数据。</p><p><strong>空 schema 的运行时职责</strong>：</p><ol><li>只在 plan mode 里可用 —— 默认模式下调不动</li><li>触发 UI 展示 plan —— UI 从 plan mode 状态里知道 plan 文件的路径，读取渲染</li><li>等待用户显式响应 —— 同步阻塞，没有默认继续</li><li>批准 → 原子性模式切换 —— 工具白名单恢复、缓存刷新、Claude 拿到批准信号</li></ol><p>这几件事都是 runtime 干的，不需要 Claude 传参 —— 又一次呼应 EnterPlanMode 的空 schema 设计：<strong>权限和状态收敛在 runtime，Claude 只发信号</strong>。</p><hr><h3 id="与邻居工具的分工"><a href="#与邻居工具的分工" class="headerlink" title="与邻居工具的分工"></a>与邻居工具的分工</h3><p><strong>决策流水线的最后一环</strong> —— 三个工具的完整闭环：</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></pre></td><td class="code"><pre><span class="line">用户: 「帮我重构 auth · JWT 换 session」</span><br><span class="line">    ↓</span><br><span class="line">Claude: 有几个分叉需要确认</span><br><span class="line">    ↓</span><br><span class="line">AskUserQuestion (澄清: 只换 web 端 · Redis session store)</span><br><span class="line">    ↓</span><br><span class="line">Claude: 好 · 让我先做个规划</span><br><span class="line">    ↓</span><br><span class="line">EnterPlanMode (用户批准进入)</span><br><span class="line">    ├─ Grep / Read / Glob 探索</span><br><span class="line">    ├─ Ask 澄清子问题 (中间可能再问几次)</span><br><span class="line">    └─ 写 plan 文件</span><br><span class="line">    ↓</span><br><span class="line">ExitPlanMode (用户看到完整 plan)</span><br><span class="line">    ├─ ✅ 批准 → 默认模式 · 按 plan 执行</span><br><span class="line">    ├─ ✏️ 修改 → 回 plan mode 调整 · 完成后再 Exit</span><br><span class="line">    └─ ❌ 驳回 → 结束</span><br></pre></td></tr></table></figure><p><strong>三个工具各司其职，组合起来才构成一次完整的「协作对齐」</strong>：</p><ul><li><strong>AskUserQuestion</strong> —— 澄清:「A 还是 B?」（单点决策）</li><li><strong>EnterPlanMode</strong> —— 展开：进入只读探索，把方案落成文档</li><li><strong>ExitPlanMode</strong> —— 拍板：让用户对整个方案做批准 &#x2F; 修改 &#x2F; 驳回</li></ul><hr><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>ExitPlanMode 的精妙之处，不在于它「让用户批准方案」这个功能本身，而在于它的信号分布<strong>跟 EnterPlanMode 高度镜像</strong> —— 命名对偶（Enter&#x2F;Exit）、工具描述堆行为约束、字段和 schema 都是空的。</p><p>如果说 AskUserQuestion 是「让用户点选项」、EnterPlanMode 是「进入规划模式」，那 ExitPlanMode 就是这套系统里<strong>最谦逊的一环</strong>：它什么都不做，只发一个信号，却让整个流程有了终点、让整套协作有了「拍板」的仪式感。</p><p><strong>三工具流水线到此闭合</strong>：</p><blockquote><p>Claude Code 把「AI 与人协作」这件事，拆成了三个可组合、可编排、可预测的交互原语：澄清 · 展开 · 拍板。每一个原语都是<strong>克制</strong>的 —— 只做一件小事 —— 但组合起来足够表达任何协作场景。</p></blockquote><p>下一篇继续拆 Grep + Glob —— 从”协作对齐”三工具切换到”代码探索”两工具，看看信息搜索类 tool 是怎么编码”搜什么 &#x2F; 怎么搜 &#x2F; 返回多少”的。</p>]]>
    </content>
    <id>https://xilidou.com/2026/08/04/claude-code-tools-exit-plan-mode/</id>
    <link href="https://xilidou.com/2026/08/04/claude-code-tools-exit-plan-mode/"/>
    <published>2026-08-04T10:00:00.000Z</published>
    <summary>ExitPlanMode 的设计拆解：规划模式如何安全地回到执行模式。</summary>
    <title>Claude Code Tools 研究系列（三）—— ExitPlanMode：把方案提交给用户批准</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="AI" scheme="https://xilidou.com/categories/AI/"/>
    <category term="claude" scheme="https://xilidou.com/tags/claude/"/>
    <category term="agent" scheme="https://xilidou.com/tags/agent/"/>
    <content>
      <![CDATA[<p>Claude code tools 研究系列第二篇。上一篇拆了 AskUserQuestion —— 一个「让用户点选项」的结构化提问工具。这篇聊它的<strong>兄弟工具</strong> —— EnterPlanMode。</p><blockquote><p>本系列先读 前置篇 —— 讲清楚 tool 是什么、Claude 怎么用。本篇按前置篇提出的 4 层骨架展开。</p></blockquote><h2 id="EnterPlanMode"><a href="#EnterPlanMode" class="headerlink" title="EnterPlanMode"></a>EnterPlanMode</h2><p>跟 AskUserQuestion 一样,是每天都能见到的高频工具。但它的设计比 Ask 更「重」 —— 它不是问一个问题,而是<strong>把 Claude 切换到一种全新的工作模式</strong>。</p><h3 id="作用"><a href="#作用" class="headerlink" title="作用"></a>作用</h3><p>EnterPlanMode 是 Claude Code 内置的<strong>「规划模式入口」工具</strong>。它做的事很简单也很暴力:把 Claude 从「边想边写」的默认模式,切换到一个<strong>只读探索 + 方案设计</strong>的规划模式,拿到用户对方案的显式批准之后,再回到写代码模式。</p><p>它解决的核心问题是「AI 与用户之间的方案对齐」:</p><ol><li><strong>防止半途改错方向</strong> —— 让 Claude 在动手改任何一个文件之前,先跟用户对齐方案</li><li><strong>强制只读探索</strong> —— 进入规划模式后 Edit &#x2F; Write &#x2F; NotebookEdit 被禁用,物理上无法「边探索边偷偷改」</li><li><strong>明确决策边界</strong> —— 用户看到完整方案后批准 &#x2F; 驳回 &#x2F; 让 Claude 修改,不是看到 PR 才发现方向错了</li><li><strong>可追溯的规划产物</strong> —— Plan mode 产出的是一份写下来的 plan 文件,不是聊天里飘过的一段话,可以引用、可以修订</li></ol><h3 id="一个具体例子"><a href="#一个具体例子" class="headerlink" title="一个具体例子"></a>一个具体例子</h3><p><strong>场景</strong>:用户对 Claude 说 <strong>「帮我重构这个身份验证模块,把 JWT 换成 session cookies」</strong>。</p><p>这个需求听起来清晰,但实际横跨:登录路由 &#x2F; token 生成中间件 &#x2F; 前端存储层 &#x2F; 会话过期策略 &#x2F; 数据库 schema (要不要建 sessions 表?) &#x2F; 已有 API 调用者的向后兼容处理。<strong>多文件、多决策、多依赖</strong>。</p><h4 id="反例-如果没有-EnterPlanMode"><a href="#反例-如果没有-EnterPlanMode" class="headerlink" title="反例:如果没有 EnterPlanMode"></a>反例:如果没有 EnterPlanMode</h4><p>Claude 只能凭上下文猜一个方案,直接动手:</p><ul><li>打开 <code>auth/middleware.ts</code> —— 改成读 session cookie</li><li>打开 <code>auth/routes.ts</code> —— 删掉 JWT 签发,改成 <code>req.session</code></li><li>打开 <code>frontend/api.ts</code> —— 删掉 <code>Authorization</code> header 逻辑</li><li>打开 <code>models/user.ts</code> —— 加个 <code>sessionId</code> 字段</li><li>改到一半发现:原来项目里有 3 个别的服务通过 JWT 校验访问这套 API…</li></ul><p>用户看到 diff 一脸懵:「我要的是 web 端 session,后台服务的 JWT 请保留啊。你把整套 API 都换了怎么办?」</p><p>这一轮出现的问题:</p><ol><li><strong>方向性错误提前 5 步才发现</strong> —— 已经改了 4 个文件,回滚很痛</li><li><strong>决策边界模糊</strong> —— 「是替换所有 auth 还是只替换 web 端」这种关键分叉,Claude 猜错了没人拦</li><li><strong>用户看不到全景</strong> —— 只看到一堆 diff,反推方案很累</li><li><strong>重要副作用未预警</strong> —— 数据库要不要建 sessions 表?session 过期用 memory 还是 Redis?这些都在 Claude 脑子里飘过但没落地成文</li><li><strong>回滚成本高</strong> —— 每一步改动都花了 tokens 和心智,推翻等于全废</li></ol><p><strong>核心痛点</strong>:「边想边写」让 Claude 在一个<strong>方案还没定型的状态下</strong>开始产出 diff · 用户不到最后一刻看不到全局。</p><h4 id="用-EnterPlanMode-是怎么解决的"><a href="#用-EnterPlanMode-是怎么解决的" class="headerlink" title="用 EnterPlanMode 是怎么解决的"></a>用 EnterPlanMode 是怎么解决的</h4><p>Claude 会先声明「我要进 plan mode」,请求用户批准 —— <strong>注意这一步本身就是一个交互确认</strong>,拒绝了就退回默认模式。用户批准后:</p><p><strong>Step 1 · 进入只读探索</strong></p><p>Claude 的工具箱被收窄:</p><ul><li>✅ 可用:Read &#x2F; Glob &#x2F; Grep &#x2F; Agent &#x2F; AskUserQuestion &#x2F; ExitPlanMode</li><li>❌ 禁用:Edit &#x2F; Write &#x2F; NotebookEdit</li></ul><p>物理上无法改任何一个文件。所有探索行为都是读性质的。</p><p><strong>Step 2 · 摸清项目现状</strong></p><ul><li>用 Grep 找出所有使用 JWT 的地方 —— 发现除了 web 端,还有 3 个内部服务</li><li>用 Read 看 <code>auth/middleware.ts</code> 的现有校验逻辑</li><li>用 Glob 定位所有 auth 相关的测试文件</li><li>用 Agent 派一个 general-purpose subagent 去调研「项目里有没有既有的 session store 约定」</li></ul><p><strong>Step 3 · 遇到关键分叉,用 AskUserQuestion 澄清</strong></p><p>比如问用户:</p><ul><li>是「只换 web 端」还是「全部换」?</li><li>session store 用 memory &#x2F; Redis &#x2F; DB?</li></ul><p>—— 这就回到了上一篇讲的 Ask 澄清模式,<strong>Ask 和 EnterPlanMode 天然配对</strong>。</p><p><strong>Step 4 · 写下方案</strong></p><p>Claude 把整套方案写到一个 plan 文件里:范围、影响文件、迁移步骤、风险、回滚策略。<strong>这是一份可以被引用、被修订的产物,不是聊天记录</strong>。</p><p><strong>Step 5 · ExitPlanMode 请求批准</strong></p><p>用户看到完整的方案,做出决定:</p><ul><li>✅ 批准 → Claude 回到写代码模式,按 plan 执行</li><li>✏️ 修改 → 反馈意见,Claude 修 plan</li><li>❌ 驳回 → 换方向</li></ul><p><strong>没有一个文件在被批准之前被改过</strong>。用户的 tokens、时间、心智不会浪费在错方向上。</p><h4 id="对照一下两种形式解决了反例里的哪些痛点"><a href="#对照一下两种形式解决了反例里的哪些痛点" class="headerlink" title="对照一下两种形式解决了反例里的哪些痛点"></a>对照一下两种形式解决了反例里的哪些痛点</h4><table><thead><tr><th>反例痛点</th><th>EnterPlanMode 的解法</th></tr></thead><tbody><tr><td>方向性错误提前 5 步才发现</td><td>ExitPlanMode 批准之前不能改任何文件</td></tr><tr><td>决策边界模糊</td><td>plan mode 里可用 AskUserQuestion 澄清关键分叉</td></tr><tr><td>用户看不到全景</td><td>plan 文件是完整方案,而不是一堆 diff</td></tr><tr><td>重要副作用未预警</td><td>强制探索 → 设计 → 呈现,给 Claude 时间考虑周全</td></tr><tr><td>回滚成本高</td><td>探索是只读的,plan 被驳回不用回滚代码</td></tr></tbody></table><h3 id="触发条件"><a href="#触发条件" class="headerlink" title="触发条件"></a>触发条件</h3><p>工具官方说明写了一条很有意思的原则:<strong>「非平凡的实现任务默认走 plan」</strong> —— 这是一个<strong>偏保守的默认</strong>。</p><p><strong>7 类</strong>「该用 plan」的场景:</p><ul><li><strong>新功能实现</strong> —— 不管多小,只要是从 0 加一块功能都有隐含决策(放哪、按钮点了做啥、报错怎么处理…)</li><li><strong>多种合理方案</strong> —— 「加缓存」可能是 Redis &#x2F; 内存 &#x2F; 文件,「实时更新」可能是 WebSocket &#x2F; SSE &#x2F; 轮询,选择本身就是设计</li><li><strong>修改现有行为</strong> —— 「更新登录流程」到底改什么?动前先说清楚</li><li><strong>架构决策</strong> —— 选模式、选依赖、选数据流方向,都是要用户拍板的</li><li><strong>跨 3 个以上文件的改动</strong> —— 影响面大到 diff 看不出全貌</li><li><strong>需求本身不清晰</strong> —— 「让 app 变快」 —— 得先 profile,先讨论优化方向</li><li><strong>用户偏好会影响实现</strong> —— 如果你想用 AskUserQuestion 澄清,那更应该用 EnterPlanMode 展开</li></ul><p><strong>4 类</strong>「不该用 plan」的场景:</p><ul><li><strong>单行修复</strong> —— 修 typo、明显的 off-by-one</li><li><strong>只添加一个函数,需求非常清晰</strong> —— 直接写,不用铺垫</li><li><strong>用户已经给出非常具体、详细的指令</strong> —— 用户已经在做规划的事,Claude 再规划一次是重复</li><li><strong>纯研究 &#x2F; 探索任务</strong> —— 这种任务应该用 Agent tool,不涉及后续实现</li></ul><p>一个<strong>很有意思的偏差</strong>:官方原文写 “err on the side of planning” —— <strong>如果不确定,倾向于规划</strong>。这个默认设置本身就说明设计者的态度:<strong>bias toward alignment over speed</strong>。</p><h3 id="技术实现"><a href="#技术实现" class="headerlink" title="技术实现"></a>技术实现</h3><h4 id="1-·-命名"><a href="#1-·-命名" class="headerlink" title="1 · 命名"></a>1 · 命名</h4><p><code>EnterPlanMode</code></p><p>对比 AskUserQuestion 4 层都有信号，EnterPlanMode 的信号分布很不一样 —— <strong>命名承担了本该由 schema 承担的角色</strong>：</p><ul><li><code>Enter</code> —— 动词，暗示”进入一种状态”（不是获取数据、不是执行动作）</li><li><code>PlanMode</code> —— 状态名，配对 <code>ExitPlanMode</code> 形成对偶</li></ul><p>一个反事实设计：如果叫 <code>SetMode(mode: &quot;plan&quot;)</code>，模型会把它当成”设置一个属性”，随手切换、随手切换回。当前命名把它编码成一次<strong>有仪式感的状态跳转</strong> —— 需要显式 Enter，需要显式 Exit，语义比参数化的 SetMode 强得多。</p><p>这也是为什么后面 schema 层可以是空的 —— 命名已经把语义顶死了，schema 不需要再兜底。</p><h4 id="2-·-工具级描述"><a href="#2-·-工具级描述" class="headerlink" title="2 · 工具级描述"></a>2 · 工具级描述</h4><p>EnterPlanMode 的描述围绕四件事：<strong>什么时候用 &#x2F; 什么时候不用 &#x2F; 与邻居的分工 &#x2F; 运行时会发生什么</strong>。</p><p><strong>开篇的保守偏差</strong></p><blockquote><p>Prefer using EnterPlanMode for implementation tasks unless they’re simple.</p></blockquote><p>一句话就重塑了 Claude 的行为倾向 —— 「不确定的时候先规划」，而不是「不确定的时候直接干」。这是把<strong>默认档位调保守</strong>写进了 tool 顶部。</p><p><strong>7 类 use case 的量化门槛</strong></p><p>原文 “When to Use This Tool” 段落列了 7 个编号 heading，每条都带具体判断线索。最典型的一条：</p><blockquote><p>Multi-File Changes: The task will likely touch more than 2-3 files</p></blockquote><p>给出<strong>量化门槛</strong>（2-3 文件）而不是主观感觉。这减少了 Claude 在”要不要用 plan mode”这件事上的分歧 —— 主观直觉被编译成客观规则。</p><p><strong>与 AskUserQuestion 的边界</strong></p><blockquote><p>If you would use AskUserQuestion to clarify the approach, use EnterPlanMode instead</p></blockquote><p>这条把一个模糊边界（什么时候用 Ask 什么时候用 plan）转化成明确规则：<strong>Ask 只解决单点澄清，涉及方案层面的分叉直接开 plan</strong>。避免”用 Ask 问一堆问题拼凑出一个方案”这种反模式 —— 那种 Ask 循环体验很差。</p><p><strong>与 Agent 的边界</strong></p><blockquote><p>Pure research&#x2F;exploration tasks (use the Agent tool with explore agent instead)</p></blockquote><p>明确了另一条边界：<strong>纯研究不做实现的，别用 EnterPlanMode</strong>。为什么？因为 EnterPlanMode 是「实现前的规划」，如果不打算实现，进 plan mode 是空转 —— 直接用 Agent 派 subagent 调研更合适。</p><p><strong>用户批准是硬要求</strong></p><blockquote><p>This tool REQUIRES user approval - they must consent to entering plan mode</p></blockquote><p>这不是「AI 单方面切换状态」 —— 用户是流程的守门员。这也解释了为什么这是个空参数的 tool call：调用本身就是一次「请示」，不是执行。</p><p><strong>不确定时的默认</strong></p><blockquote><p>If unsure whether to use it, err on the side of planning - it’s better to get alignment upfront than to redo work</p></blockquote><p>这是整段 prompt 的<strong>价值观声明</strong> —— 与其做错回滚，不如多花一轮对齐。这个价值观在 AskUserQuestion 那篇也见过 —— <strong>Claude Code 的整个工具生态都 bias toward alignment</strong>。</p><p><strong>社交礼仪 framing</strong></p><blockquote><p>Users appreciate being consulted before significant changes are made to their codebase</p></blockquote><p>这一句在训练 Claude 的<strong>社交直觉</strong> —— 不只是效率考虑，规划本身是一种「尊重用户对自己 codebase 的所有权」的姿态。这个 framing 让 Claude 不把「先规划」看成打扰，而看成协作礼仪。</p><h4 id="3-·-字段级描述"><a href="#3-·-字段级描述" class="headerlink" title="3 · 字段级描述"></a>3 · 字段级描述</h4><p><strong>空</strong>。</p><p>EnterPlanMode 没有任何入参字段 —— schema 是空对象 <code>&#123;&#125;</code>。所以字段级描述这一层不存在。所有意图都上移到工具级描述里。</p><h4 id="4-·-schema-校验规则"><a href="#4-·-schema-校验规则" class="headerlink" title="4 · schema 校验规则"></a>4 · schema 校验规则</h4><p><strong>空</strong>。</p><p><code>input_schema</code> 是空对象 —— 无字段、无类型、无约束。调用行为本身 &#x3D; 状态切换意图，不需要传任何数据。</p><p>这一层的”空”本身就是设计信号：<strong>权限收敛在工具层实现，不在参数层</strong>。Claude 不需要”申请”某些权限或”声明”进入哪种模式，官方 runtime 在 Claude 调用 EnterPlanMode 后自动执行以下动作：</p><ol><li>需要用户批准 —— 就像 Ask 一样，进入 plan mode 需要用户点「同意进入 plan」</li><li>工具白名单被收窄 —— 进入后 Edit &#x2F; Write &#x2F; NotebookEdit 被禁用</li><li>CWD 相关的缓存被重写 —— system prompt sections &#x2F; memory files &#x2F; plans directory 都会重刷，确保 plan mode 上下文干净</li><li>只能通过 ExitPlanMode 退出 —— 不像 AskUserQuestion 那样问完就结束，plan mode 是一个<strong>持续的状态</strong></li></ol><hr><h3 id="与邻居工具的分工"><a href="#与邻居工具的分工" class="headerlink" title="与邻居工具的分工"></a>与邻居工具的分工</h3><ul><li><strong>AskUserQuestion</strong> —— 单点澄清:「A 还是 B？」</li><li><strong>EnterPlanMode</strong> —— 展开完整方案（在 plan 期间 Ask 可以继续用）</li><li><strong>ExitPlanMode</strong> —— 提交方案让用户批准</li></ul><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></pre></td><td class="code"><pre><span class="line">遇到不清楚的分叉</span><br><span class="line">    ↓</span><br><span class="line">Ask 澄清 (选 A / 选 B)</span><br><span class="line">    ↓</span><br><span class="line">EnterPlanMode (进入规划模式)</span><br><span class="line">    ├─ Grep / Read / Glob / Agent 探索</span><br><span class="line">    ├─ Ask 澄清子问题 (可以多次)</span><br><span class="line">    └─ 写 plan 文件</span><br><span class="line">    ↓</span><br><span class="line">ExitPlanMode (提交方案)</span><br><span class="line">    ├─ 用户批准 → 回默认模式 · 按 plan 写代码</span><br><span class="line">    ├─ 用户修改 → 回 plan mode 改</span><br><span class="line">    └─ 用户驳回 → 结束</span><br></pre></td></tr></table></figure><p>上一篇讲过 AskUserQuestion <strong>不应该</strong>在 plan mode 里被用作「方案 OK 吗」的元问题 —— 原因再复述：<strong>因为用户在 ExitPlanMode 触发之前根本看不到 plan · 用户无东西可批</strong> · 「OK 吗」这个问题在这个时序里没有语义。</p><hr><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>EnterPlanMode 的精妙之处，不在于它「让 AI 先想再做」这个功能本身，而在于它的<strong>信号分布极端偏斜</strong>：命名承担核心语义（Enter + PlanMode 的对偶）、工具级描述堆满行为约束（7 类 use case + 保守偏差 + 社交礼仪）、字段级描述和 schema 都是空的。</p><p>这告诉我们一个更本质的事：<strong>空 schema 本身就是一种设计</strong>。当一个 tool 的语义就是”状态切换”时，参数化会破坏这个语义 —— 参数化的 SetMode 邀请随手切换，而无参的 EnterPlanMode 是一次仪式化的请示。</p><p>下一篇继续拆 ExitPlanMode —— 三工具决策流水线的最后一环 · 看看「提交方案批准」这个动作是怎么设计的。</p>]]>
    </content>
    <id>https://xilidou.com/2026/08/03/claude-code-tools-enter-plan-mode/</id>
    <link href="https://xilidou.com/2026/08/03/claude-code-tools-enter-plan-mode/"/>
    <published>2026-08-03T10:00:00.000Z</published>
    <summary>EnterPlanMode 的设计拆解：只读探索、方案对齐与显式批准。</summary>
    <title>Claude Code Tools 研究系列（二）—— EnterPlanMode：为什么空 schema 也是一种设计</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="AI" scheme="https://xilidou.com/categories/AI/"/>
    <category term="Agent" scheme="https://xilidou.com/tags/Agent/"/>
    <category term="AI" scheme="https://xilidou.com/tags/AI/"/>
    <category term="Claude Code" scheme="https://xilidou.com/tags/Claude-Code/"/>
    <category term="Tools" scheme="https://xilidou.com/tags/Tools/"/>
    <category term="AskUserQuestion" scheme="https://xilidou.com/tags/AskUserQuestion/"/>
    <category term="Harness" scheme="https://xilidou.com/tags/Harness/"/>
    <category term="Prompt Engineering" scheme="https://xilidou.com/tags/Prompt-Engineering/"/>
    <category term="交互设计" scheme="https://xilidou.com/tags/%E4%BA%A4%E4%BA%92%E8%AE%BE%E8%AE%A1/"/>
    <content>
      <![CDATA[<p>之前研究过 Claude Code 的设计，用 Java 写了一个乞丐版的 claude code，开源地址 <a href="https://github.com/diaozxin007/jooj">jooj</a>。Claude Code 的 tools 都设计得非常精巧，所以想逐个研究一下。大家共同学习。</p><p>本系列开篇先聊 <strong>AskUserQuestion</strong> —— 一个最常见、但设计上最容易被低估的 tool。</p><h2 id="AskUserQuestion"><a href="#AskUserQuestion" class="headerlink" title="AskUserQuestion"></a>AskUserQuestion</h2><p>是最常见到的 tools 之一。</p><h3 id="作用"><a href="#作用" class="headerlink" title="作用"></a>作用</h3><p>AskUserQuestion 是 Claude Code 内置的<strong>结构化提问工具</strong>。它不是让 Claude 输出一段问题字符串等用户回复，而是把问题渲染成一个交互式选择面板 —— 用户看到的是一组预设选项（卡片形式），而不是一段纯文字提问。</p><p>它解决的核心问题是「AI 与用户之间的高效对齐」：</p><ol><li><strong>降低用户负担</strong> —— 从「打字回答」变成「点选项」，响应时间大幅缩短</li><li><strong>结构化输入</strong> —— Claude 拿到的是明确的枚举值，不用再解析自然语言</li><li><strong>收敛歧义</strong> —— 通过预设选项引导用户在明确的方案之间选择，避免「随便你决定」式的模糊回答</li><li><strong>保底逃生舱</strong> —— 系统始终自动附加「其它」选项，允许用户输入自定义文本，避免「选项不合口味只能退出」</li></ol><h3 id="一个具体例子"><a href="#一个具体例子" class="headerlink" title="一个具体例子"></a>一个具体例子</h3><p>在展开触发条件、技术实现、prompt 细节之前，先看一个具体场景，感受一下「不用 AskUserQuestion 会怎样 vs 用了会怎样」。</p><p><strong>场景</strong>：用户对 Claude 说 <strong>「帮我给这个应用加个用户登录」</strong>。</p><p>这个需求描述得很不完整 —— 用哪种认证方式没定、登录凭证存哪里没定。Claude 既不能瞎猜（用户可能有团队规范），也没法直接从代码里读出来（新功能没先例）。</p><h4 id="反例：如果没有-AskUserQuestion"><a href="#反例：如果没有-AskUserQuestion" class="headerlink" title="反例：如果没有 AskUserQuestion"></a>反例：如果没有 AskUserQuestion</h4><p>Claude 只能用一段自由文本把问题甩回去，大概长这样：</p><blockquote><p>「你想用什么认证方式？我建议 JWT，但也可以用会话 cookie 或 OAuth。另外登录凭证存哪里，httpOnly cookie 还是 localStorage？」</p></blockquote><p>用户会遇到几个问题：</p><ol><li><strong>认知负担高</strong> —— 一段长文字里塞了 2 个决策 + 5 个选项，需要用户先解析题目再回答</li><li><strong>回答成本高</strong> —— 要么打一段字回复（「JWT + httpOnly」），要么去网上搜「JWT vs 会话 cookie」看两小时再回来</li><li><strong>Claude 解析成本高</strong> —— 拿到「就 JWT 吧，cookie 那个」这种回复，还得反推用户到底选了哪个，可能理解错</li><li><strong>推荐值淹没在文字里</strong> —— Claude 说「建议 JWT」，但和其它选项混在一起，用户容易忽略</li><li><strong>没有兜底</strong> —— 如果用户想用一个 Claude 没提到的方案（比如免密邮件链接），要么另起一段解释，要么被 Claude 的三选一绑架</li></ol><p><strong>核心痛点</strong>：这种纯文本形式，让「协作对齐」变成了一次昂贵的自然语言往返。</p><h4 id="用-AskUserQuestion-是怎么解决的"><a href="#用-AskUserQuestion-是怎么解决的" class="headerlink" title="用 AskUserQuestion 是怎么解决的"></a>用 AskUserQuestion 是怎么解决的</h4><p>Claude 会构造一个包含 <strong>两个问题</strong> 的调用。用户在界面上看到的两张卡片长这样：</p><p><strong>第一个问题</strong> —— 认证方式</p><p><img data-src="/images/ask-user-question-auth.jpg" alt="AskUserQuestion 认证方式选择"></p><p><strong>第二个问题</strong> —— 凭证存储</p><p><img data-src="/images/ask-user-question-storage.jpg" alt="AskUserQuestion 凭证存储选择"></p><p>每张卡片顶部是那个短标签（「认证方式」&#x2F;「凭证存储」），下面是 3 个 &#x2F; 2 个选项 + 一个自动追加的「其它」。用户点两下选完，Claude 拿到的返回值大致是：</p><ul><li>第一个问题 → 用户选了 <strong>JWT（推荐）</strong></li><li>第二个问题 → 用户选了 <strong>httpOnly cookie（推荐）</strong></li></ul><p><strong>决策时间从几分钟压到几秒</strong>。这就是 AskUserQuestion 存在的意义 —— 不是「让 AI 问问题」，而是「让协作的每一次澄清都变得低成本」。</p><h4 id="对照一下两种形式解决了反例里的哪些痛点"><a href="#对照一下两种形式解决了反例里的哪些痛点" class="headerlink" title="对照一下两种形式解决了反例里的哪些痛点"></a>对照一下两种形式解决了反例里的哪些痛点</h4><table><thead><tr><th>反例痛点</th><th>AskUserQuestion 的解法</th></tr></thead><tbody><tr><td>认知负担高</td><td>拆成 2 张独立卡片，一次聚焦一个决策</td></tr><tr><td>回答成本高</td><td>点选项而不是打字，权衡说明直接标在选项下</td></tr><tr><td>Claude 解析成本高</td><td>返回值是明确的选项文本，不用做自然语言解析</td></tr><tr><td>推荐值淹没在文字里</td><td>「（推荐）」后缀 + 前置位置，第一眼看到</td></tr><tr><td>没有兜底</td><td>「其它」自动追加，用户想输入自定义方案永远有出口</td></tr></tbody></table><p>这个对照本质上就是 AskUserQuestion 每个设计点的存在理由 —— 每一条都对应一个自由文本对话解决不了的痛点。带着这个直觉，再往下看触发条件、技术实现和 prompt 细节，会发现每一条约束都对应到这里的某个具体痛点。</p><h3 id="触发条件"><a href="#触发条件" class="headerlink" title="触发条件"></a>触发条件</h3><p>工具的官方说明里明确写了触发边界：<strong>只有在你被卡住，而这个决策又真正属于用户时才使用</strong>。</p><p>三类<strong>该问</strong>的场景：</p><ul><li><strong>无法从请求推断</strong> —— 需求本身模糊（比如「帮我加个登录」，没说 OAuth 还是 JWT）</li><li><strong>无法从代码推断</strong> —— 现有代码里没有先例可以模仿</li><li><strong>没有合理默认值</strong> —— 涉及品味 &#x2F; 业务规则 &#x2F; 架构分叉，不该由 AI 拍板</li></ul><p>三类<strong>不该问</strong>的场景：</p><ul><li><strong>答案能从代码里读出来</strong> —— 该花时间读代码，而不是打断用户</li><li><strong>只有一种明显合理的做法</strong> —— 直接做，提交信息里说明理由即可</li><li><strong>在计划模式里问「方案 OK 吗」</strong> —— 这是 ExitPlanMode 的职责，用 Ask 是重复</li></ul><p>一个典型反模式：<strong>避免「这个方案 OK 吗 &#x2F; 我可以继续吗」这类元问题</strong>。ExitPlanMode 本身就是「请求批准」，Ask 用来做这个纯属重复。</p><h3 id="技术实现"><a href="#技术实现" class="headerlink" title="技术实现"></a>技术实现</h3><p>从工具的入参定义反推，它的核心结构可以用文字描述如下：</p><p>Claude 调用这个工具时，传入一个 <strong>问题列表</strong>（1 到 4 个问题）。列表里每一项是一个 <strong>问题对象</strong>，包含四个部分：</p><ul><li><strong>问题文本</strong> —— 完整的问题文本，以问号结尾</li><li><strong>卡片短标签</strong> —— 显示在卡片顶部的短标签，最多 12 个字符</li><li><strong>是否多选</strong> —— 布尔值，控制是否允许多选（默认单选）</li><li><strong>选项列表</strong> —— 2 到 4 个选项</li></ul><p>每个选项本身又包含三个字段：</p><ul><li><strong>选项文本</strong> —— 用户看到的选项显示文本（1 到 5 个字）</li><li><strong>选项说明</strong> —— 这个选项含义 &#x2F; 权衡的说明</li><li><strong>视觉预览</strong> —— 可选：当选项差异需要「可视化对比」时（比如两个示意图、两段代码），聚焦这个选项时界面会渲染这段内容</li></ul><p>几个关键设计点：</p><ol><li><strong>一次可以问 1-4 个问题</strong> —— 支持批量决策（比如「选认证方式 + 选凭证存储」一次问完），但不允许无脑打包 10 个问题轰炸用户</li><li><strong>每个问题 2-4 个选项</strong> —— 强制 Claude 做初步归类，把 N 种可能收敛到少数几个可点选项，而不是甩一张长清单给用户</li><li><strong>「其它」是隐式选项</strong> —— 用户端自动追加，Claude 不用手动列。这保证了「Claude 想到的选项 ≠ 全部」时用户不会被卡死</li><li><strong>推荐值机制</strong> —— 如果 Claude 有倾向，把它放第一个选项 + 文本后追加「（推荐）」，用户可以一眼看到并快速采纳</li><li><strong>返回值结构</strong> —— 用问题文本作为 key，映射到用户选择的选项文本；另有一个字段承载用户在视觉预览场景下额外写的注释</li></ol><p><strong>视觉预览字段</strong> 是一个有意思的进阶点 —— 当选项之间的差异需要「可视化对比」（比如两个界面示意图、两种代码风格），把内容塞在这个字段里，界面会在聚焦某个选项时渲染出来。这对「选哪种 API 设计 &#x2F; 选哪种排版」这种问题特别有用。</p><p><strong>与 EnterPlanMode &#x2F; ExitPlanMode 的分工</strong>：</p><ul><li>计划模式里，用 AskUserQuestion 澄清「选哪种方案」（在方案定稿之前）</li><li>计划模式里，不要用 AskUserQuestion 问「我的方案 OK 吗」（用 ExitPlanMode）</li><li>非计划模式里，用 AskUserQuestion 处理任何需要用户拍板的技术分叉</li></ul><p>三个工具串起来是一条完整的决策流水线：<strong>Ask 澄清 → EnterPlanMode 展开 → ExitPlanMode 拍板</strong>。</p><h3 id="prompt-详解"><a href="#prompt-详解" class="headerlink" title="prompt 详解"></a>prompt 详解</h3><p>工具官方说明里每一句都在给 Claude 塞一条行为约束，逐条拆一下：</p><p><strong>约束 1：严格的适用边界（开篇第一句）</strong></p><blockquote><p>Use this tool only when you are blocked on a decision that is genuinely the user’s to make: one you cannot resolve from the request, the code, or sensible defaults.</p></blockquote><p>这句话在训练 Claude「不要主动打扰」—— 遇到不确定，第一反应应该是<strong>先查代码、先用合理默认值</strong>，而不是甩问题给用户。</p><p><strong>约束 2：「其它」逃生舱的透明化</strong></p><blockquote><p>Users will always be able to select “Other” to provide custom text input</p></blockquote><p>系统不是把这个选项藏起来让 Claude 假装不知道 —— 而是明确告诉 Claude「其它会自动加，你不用列」。这样 Claude 不会浪费一个选项去手写「自定义」。</p><p><strong>约束 3：多选参数的语义</strong></p><blockquote><p>Use multiSelect: true to allow multiple answers to be selected for a question</p></blockquote><p>对应场景：选多个功能开关 &#x2F; 多个环境 &#x2F; 多个要修的文件。默认单选保护用户不被过多选择卡住。</p><p><strong>约束 4：推荐值的表达形式</strong></p><blockquote><p>If you recommend a specific option, make that the first option in the list and add “(Recommended)” at the end of the label</p></blockquote><p>有意思的点：<strong>推荐值不是单独字段，而是通过「约定俗成的位置 + 后缀」实现的</strong>。好处：</p><ul><li>保持入参定义简单，不引入一个「是否推荐」的布尔字段</li><li>界面侧只用渲染选项文本，不用做特殊处理</li><li>Claude 要表态必须写进选项文本，无法藏在元数据里 —— 用户一眼能看见</li></ul><p><strong>约束 5：与计划模式的时序关系</strong></p><blockquote><p>Plan mode note: To switch into plan mode, use EnterPlanMode (not this tool). Once in plan mode, use this tool to clarify requirements or choose between approaches BEFORE finalizing your plan. Do NOT use this tool to ask “Is my plan ready?”, “Should I proceed?”, or otherwise reference “the plan” in questions — the user cannot see the plan until you call ExitPlanMode for approval.</p></blockquote><p>这段是最有教学价值的 —— 明确了整套流程的<strong>时序</strong>：</p><ol><li>计划模式里，先用 Ask 澄清方案分叉（如「选 A 还是 B」）</li><li>澄清完后，用 EnterPlanMode 落一份完整方案</li><li><strong>最后一步</strong>用 ExitPlanMode 请求批准 —— <strong>不要</strong>再用 Ask 问「OK 吗」</li></ol><p>尤其注意原文最后半句 —— 「用户在你调用 ExitPlanMode 之前根本看不到方案」—— 这才是「不要在计划模式里问『方案 OK 吗』」的<strong>真正原因</strong>：不是重复，而是<strong>用户根本没东西可批</strong>。</p><p>三个工具各司其职：Ask 澄清 &#x2F; EnterPlanMode 展开 &#x2F; ExitPlanMode 拍板。这套约束本质上是在阻止 Claude 在计划模式里绕回来用 Ask 做「批准」这件事。</p><p><strong>约束 6：卡片短标签是必填字段（结构层强制）</strong></p><blockquote><p>Very short label displayed as a chip&#x2F;tag (max 12 chars). Examples: “Auth method”, “Library”, “Approach”.</p></blockquote><p>这是一个交互约束 —— 界面里每个问题渲染成一张卡片，卡片顶端的标签用这个短字符串，而不是完整的问题文本。这就要求 Claude 把长问题浓缩成一个短标签（比如「登录流程应该用哪种认证方式？」的短标签就是「认证方式」）。</p><p><strong>约束 7：问题必须以问号结尾</strong></p><blockquote><p>Should be clear, specific, and end with a question mark.</p></blockquote><p>看似很小的一条，但决定了界面的自然度 —— 问句语气 vs 陈述语气对用户的心理暗示完全不同。这也间接强制 Claude 把内容组织成「真正的疑问」而不是「疑似指令」。</p><hr><p><strong>小结</strong>：AskUserQuestion 的精妙之处，不在于它「让 AI 问用户问题」这个功能本身，而在于它通过入参结构约束 + prompt 约束，把<strong>「什么时候问 &#x2F; 怎么问 &#x2F; 用什么形式呈现 &#x2F; 和谁配合」</strong> 全都规范住了。相当于把「AI 提问」这个泛用能力，收敛成一个可预测、可组合、可维护的交互原语。</p><p>下一篇继续拆下一个 tool，欢迎关注这个系列。</p>]]>
    </content>
    <id>https://xilidou.com/2026/07/28/claude-code-tools-ask-user-question/</id>
    <link href="https://xilidou.com/2026/07/28/claude-code-tools-ask-user-question/"/>
    <published>2026-07-28T22:22:00.000Z</published>
    <summary>Claude Code Tools 系列开篇：拆解 AskUserQuestion 这个「结构化提问工具」的设计。用「登录方案选型」这个具体场景对比自由文本提问 vs 结构化选项，从作用、触发条件、技术实现到 prompt 详解逐条剖析——它不是「让 AI 问问题」的功能，而是把「AI 提问」这个泛用能力收敛成可预测、可组合、可维护的交互原语。</summary>
    <title>Claude Code Tools 研究系列（一）—— AskUserQuestion：把「AI 提问」变成结构化交互原语</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Agent" scheme="https://xilidou.com/categories/Agent/"/>
    <category term="Agent" scheme="https://xilidou.com/tags/Agent/"/>
    <category term="Loop" scheme="https://xilidou.com/tags/Loop/"/>
    <category term="chat" scheme="https://xilidou.com/tags/chat/"/>
    <content>
      <![CDATA[<h2 id="一、起因-LLM-一次调用不够"><a href="#一、起因-LLM-一次调用不够" class="headerlink" title="一、起因:LLM 一次调用不够"></a>一、起因:LLM 一次调用不够</h2><p>我做了个工具—— <a href="https://text2everything.vip/">text2everything.vip</a>  自然语言描述流程,LLM 出 Mermaid 代码,右边实时渲染。</p><p>第一版是<strong>单次调用</strong>:用户输入 → LLM → 出图。跑了几个月发现两类问题:</p><ul><li><strong>语法错了没救</strong> —— LLM 生成的代码看着对,Mermaid 渲染直接 <code>Parse error</code>,用户看到红色堆栈就走了。</li><li><strong>语义偏了不自知</strong> —— 用户说”画个登录流程”,LLM 惯性给你加个”发送验证码”,既不问,也不告诉你它加了。</li></ul><p>这两个问题都不是”再调 prompt”能治的:第一次失败得<strong>有第二次机会</strong>;补常识没错,但<strong>必须留痕</strong>才能审计。</p><p>所以第二版做成一个 <strong>agent</strong>:能多轮对话,能自己纠错,能记住用户说过什么。这篇讲一下这个 agent 长什么样。</p><span id="more"></span><h2 id="二、核心洞察-这不是一个循环-是两个"><a href="#二、核心洞察-这不是一个循环-是两个" class="headerlink" title="二、核心洞察:这不是一个循环,是两个"></a>二、核心洞察:这不是一个循环,是两个</h2><p>看似”多轮生成 UML”是一个循环,拆开看是两个,目标完全不同:</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">外层 · 问清楚要画啥</span><br><span class="line">用户说话 → 提取关键信息 → 发现哪里没说清 → 追问 → 补齐</span><br><span class="line"></span><br><span class="line">内层 · 画出来 + 修</span><br><span class="line">结构化需求 → 生成代码 → 校验 → 有错就修 → 输出</span><br></pre></td></tr></table></figure><p>外层管 <strong>“信息够不够”</strong> ,内层管 <strong>“代码对不对”</strong> 。混在一起做的话,你会发现”这个歧义要问用户吗”永远没答案 —— 因为外层的歧义(用户没说)和内层的歧义(生成错了)是两回事。</p><h2 id="三、Agent-是个团队-分工要明确"><a href="#三、Agent-是个团队-分工要明确" class="headerlink" title="三、Agent 是个团队,分工要明确"></a>三、Agent 是个团队,分工要明确</h2><p>我把 agent 想象成一家律师事务所处理案子。<strong>每个角色只做一件事</strong>:</p><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>审查员</strong></td><td>看档案哪一栏是空的</td><td>不决定问不问</td></tr><tr><td><strong>决策官</strong></td><td>决定问不问用户、问哪个</td><td>不自己去问</td></tr><tr><td><strong>律师</strong></td><td>拿档案起草文件(生成 Mermaid)</td><td>但可以补合理假设</td></tr><tr><td><strong>校对员</strong></td><td>检查文件语法 + 有没有夹带私货</td><td>有错就退回让律师改</td></tr><tr><td><strong>前台</strong></td><td>按顺序叫各角色出场,维护整个档案</td><td>不做任何判断</td></tr></tbody></table><p>一开始我把这些揉在一个 prompt 里,发现 debug 极其痛苦 —— 因为<strong>说不清是”记录员搞错了”还是”律师瞎猜了”</strong> 。拆开之后每个角色都是纯函数,给一个输入永远给一个输出,单独测试。</p><h3 id="调用关系-前台按顺序叫各角色出场"><a href="#调用关系-前台按顺序叫各角色出场" class="headerlink" title="调用关系:前台按顺序叫各角色出场"></a>调用关系:前台按顺序叫各角色出场</h3><p>6 个角色之间<strong>不是各自平级各干各的</strong>,而是一条流水线,前台是唯一的调度者:</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></pre></td><td class="code"><pre><span class="line">用户消息</span><br><span class="line">    ↓</span><br><span class="line">  前台 (append 到对话历史)</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">    ├─ 决定&quot;问一个&quot; ──→ 措辞员 ──→ 用户</span><br><span class="line">    │                              ↑ (下一轮回到最上面)</span><br><span class="line">    │</span><br><span class="line">    └─ 决定&quot;信息够了&quot; ──→ 律师 ──→ 生成 Mermaid 代码</span><br><span class="line">                                    ↓</span><br><span class="line">                              校对员 · 语法检查</span><br><span class="line">                                    │</span><br><span class="line">                                    ├─ FAIL ──→ 内层修复循环(退回律师)</span><br><span class="line">                                    │</span><br><span class="line">                                    └─ OK ──→ 校对员 · 语义检查</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></pre></td></tr></table></figure><p><strong>几个值得注意的点</strong>:</p><ul><li><strong>前台是唯一有状态的</strong>,别的角色都是”输入 → 输出”的纯函数,不记得上一次。多轮对话的记忆全靠前台维护档案。</li><li><strong>决策官是唯一分叉点</strong>,整个 agent “问 or 画”就它一个人决定。</li><li><strong>内层修复循环发生在律师和校对员之间</strong>,不惊动用户 —— Mermaid 语法错的自愈就靠这一段。</li><li><strong>语义检查发现问题不阻塞出图</strong>,而是<strong>跟着图一起呈现给用户</strong>(“图给你了,顺便问一下我加的这个要不要?”),这样用户看着图判断更容易。</li></ul><h3 id="关键规矩-记录员保守-vs-律师可以猜"><a href="#关键规矩-记录员保守-vs-律师可以猜" class="headerlink" title="关键规矩:记录员保守 vs 律师可以猜"></a>关键规矩:记录员保守 vs 律师可以猜</h3><p>这是整个架构的<strong>地基</strong>,展开讲一下。</p><p>用户说”画个登录流程,用户输密码,系统验证后返回结果”。<strong>记录员</strong>输出:</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">&#123;</span><br><span class="line">  actors: [&quot;用户&quot;, &quot;系统&quot;],</span><br><span class="line">  events: [</span><br><span class="line">    &#123; from: &quot;用户&quot;, to: &quot;系统&quot;, action: &quot;输入密码&quot; &#125;,</span><br><span class="line">    &#123; from: &quot;系统&quot;, to: &quot;用户&quot;, action: &quot;返回结果&quot; &#125;,</span><br><span class="line">  ],</span><br><span class="line">  sync: &quot;unknown&quot;    // ← 用户没说同步还是异步,绝对不猜</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>sync: &quot;unknown&quot;</code> 这个字段就是<strong>追问的信号</strong> —— 审查员看到 unknown,决策官就决定要不要问。</p><p><strong>如果记录员帮用户补了</strong>(比如”登录一般同步,填 sync”),这个信号就没了 —— agent 分不清”用户真说过 sync”和”我瞎猜的 sync”,追问逻辑就崩了。</p><p><strong>律师就没这个约束了</strong>。生成 Mermaid 时,律师可以补”验证码服务”这种合理假设,但<strong>必须写进档案的”假设”栏</strong>。后面校对员会拿档案 + 假设对账 —— 图里出现的每个元素都必须能追溯到”用户说的”或”律师的假设”,找不到就叫”夹带私货”,单独拉出来问用户”我加了这个,要保留吗?”</p><p><strong>这一步一确定,后面很多问题自然就解了</strong> —— 尤其是”用户说过’不要 X’,下一轮 LLM 又画上了 X”这种坑。因为夹带私货能被自动检测出来,agent 会主动把它捞出来问用户,而不是甩给用户自己发现。</p><h2 id="四、”要不要追问用户”这件事-只让一个人管"><a href="#四、”要不要追问用户”这件事-只让一个人管" class="headerlink" title="四、”要不要追问用户”这件事,只让一个人管"></a>四、”要不要追问用户”这件事,只让一个人管</h2><p>这是我最喜欢的一条决策:<strong>所有”问不问用户”的判断,集中在一个纯函数里</strong>,叫 Planner(决策官)。</p><ul><li>审查员 &#x2F; 校对员<strong>只识别问题</strong> —— 缺什么、错什么。</li><li>决策官<strong>拿三样东西做决定</strong>:候选问题池 + 剩余预算 + 之前问过哪些。</li><li>输出三选一:<strong>问一个</strong> &#x2F; <strong>信息够了开始画</strong> &#x2F; <strong>给用户看摘要让他确认</strong>。</li></ul><p>一个 session 有个<strong>全局预算</strong>(比如 5 轮 agent 主动提问)。<strong>用户主动改需求 &#x2F; 修图 &#x2F; 要求重画都不算消耗预算</strong> —— 只有 agent 主动打扰用户才算。这一条防止用户”越用越怕说话”。</p><p>这个设计的实际收益:<strong>想改追问节奏、调预算策略,只改这一个函数</strong>。别的组件不用动。</p><h2 id="五、走一个例子-·-4-轮对话"><a href="#五、走一个例子-·-4-轮对话" class="headerlink" title="五、走一个例子 · 4 轮对话"></a>五、走一个例子 · 4 轮对话</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><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">Turn 1  用户:&quot;画个用户注册流程时序图&quot;</span><br><span class="line">        记录员 → &#123; actors: [&quot;用户&quot;], events: [], sync: unknown &#125;</span><br><span class="line">        审查员 → 缺 actors / events / sync</span><br><span class="line">        决策官(预算 5)→ 问 actors</span><br><span class="line">        Agent: &quot;除了用户,还有哪些角色?&quot;</span><br><span class="line">        [预算: 5 → 4]</span><br><span class="line"></span><br><span class="line">Turn 2-3  用户答:前端 / 后端 / 数据库,以及各步骤</span><br><span class="line">          决策官逐轮问,预算 4 → 3 → 2</span><br><span class="line"></span><br><span class="line">Turn 4  用户:&quot;先画看看&quot;  ← 跳过澄清,直接生成,不消耗预算</span><br><span class="line">        律师第一次生成 → 惯性加了个&quot;发邮件通知&quot;(没人提过邮件)</span><br><span class="line">        校对员语法检查 → FAIL(未声明的邮件系统)</span><br><span class="line">        触发内层修复循环 → 律师二次生成,去掉邮件 → 语法 OK</span><br><span class="line">        校对员语义检查 → 发现 3 个未声明的响应箭头 → 私货警告</span><br><span class="line">        输出:图 + 一个提示&quot;我加了完整响应流程,想保留吗?&quot;</span><br><span class="line"></span><br><span class="line">Turn 5  用户:&quot;响应不要,只画到存数据库就行&quot;</span><br><span class="line">        Agent 把&quot;响应流程&quot;记入&quot;负事实&quot;(用户明说不要的)</span><br><span class="line">        重新生成 → 没响应箭头了</span><br><span class="line">        [预算不消耗,这属于用户看图后核对,不算 agent 追问]</span><br></pre></td></tr></table></figure><p><strong>5 轮预算,agent 实际主动追问 3 次,剩余 2 轮备用。</strong> 这就是全局预算的弹性。</p><h2 id="六、加新图类型的成本"><a href="#六、加新图类型的成本" class="headerlink" title="六、加新图类型的成本"></a>六、加新图类型的成本</h2><p>系统支持 Sequence · Flowchart · ERD · State · Class · Mindmap · Gantt · Arch 八种。</p><p><strong>加一种新图</strong> &#x3D; 新写 4 个角色的实现:</p><ul><li><strong>记录员</strong> —— 怎么从自然语言提取事实</li><li><strong>审查员</strong> —— 哪些字段是必填,哪些是可选</li><li><strong>校对员</strong> —— 什么算夹带私货</li><li><strong>措辞员</strong> —— 怎么问用户(时序图问”参与者”,类图问”实体”,措辞不一样)</li></ul><p><strong>决策官 &#x2F; 前台 &#x2F; 状态存储都不用动</strong>,因为它们不关心画的是哪种图。</p><h2 id="七、3-条我觉得值得抄的经验"><a href="#七、3-条我觉得值得抄的经验" class="headerlink" title="七、3 条我觉得值得抄的经验"></a>七、3 条我觉得值得抄的经验</h2><ol><li><strong>两层循环拆开做</strong> —— “问清楚”和”画出来”是两件事,共享数据但目标不同。混在一起会陷入”这个歧义要问吗”的死循环。</li><li><strong>记录员保守 + 律师可以猜,分工要严格</strong> —— 记录员帮用户补常识就等于把 agent 的判断力交给了 LLM 的直觉。律师的每个假设必须留档,校对员才能审。</li><li><strong>所有 policy 集中在一个决策官纯函数里</strong> —— 想调追问节奏、改预算策略,只动一处代码。别的角色都是”识别问题”,只有它做”决定要不要问”。</li></ol><h2 id="八、后续"><a href="#八、后续" class="headerlink" title="八、后续"></a>八、后续</h2><p>近期在做的:</p><ul><li>自动分类置信度校准(用户不说图类型时,LLM 猜错的概率)</li><li>每种图的”夹带私货”规则集扩充</li><li>一键分享:把 prompt + spec + 生成图打包成链接,收到 issue 时能一键重放整个对话</li></ul><p>如果你也在做类似的 LLM 多轮 agent(不管是画图 &#x2F; 写 SQL &#x2F; 生成 API mock),这三条我觉得最能救命。<strong>Agent 不是”用 LLM 多调几次”这么简单,是要设计一个能自己纠错、能记住负面反馈、能审计每个决策的团队。</strong></p><p>欢迎大家试用 <a href="https://text2everything.vip/">https://text2everything.vip/</a> 给我提 bug。</p>]]>
    </content>
    <id>https://xilidou.com/2026/07/24/chat-agent/</id>
    <link href="https://xilidou.com/2026/07/24/chat-agent/"/>
    <published>2026-07-24T15:11:02.000Z</published>
    <summary>
      <![CDATA[<h2 id="一、起因-LLM-一次调用不够"><a href="#一、起因-LLM-一次调用不够" class="headerlink" title="一、起因:LLM 一次调用不够"></a>一、起因:LLM 一次调用不够</h2><p>我做了个工具—— <a href="https://text2everything.vip/">text2everything.vip</a>  自然语言描述流程,LLM 出 Mermaid 代码,右边实时渲染。</p>
<p>第一版是<strong>单次调用</strong>:用户输入 → LLM → 出图。跑了几个月发现两类问题:</p>
<ul>
<li><strong>语法错了没救</strong> —— LLM 生成的代码看着对,Mermaid 渲染直接 <code>Parse error</code>,用户看到红色堆栈就走了。</li>
<li><strong>语义偏了不自知</strong> —— 用户说”画个登录流程”,LLM 惯性给你加个”发送验证码”,既不问,也不告诉你它加了。</li>
</ul>
<p>这两个问题都不是”再调 prompt”能治的:第一次失败得<strong>有第二次机会</strong>;补常识没错,但<strong>必须留痕</strong>才能审计。</p>
<p>所以第二版做成一个 <strong>agent</strong>:能多轮对话,能自己纠错,能记住用户说过什么。这篇讲一下这个 agent 长什么样。</p>]]>
    </summary>
    <title>Agent 架构设计</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Tools" scheme="https://xilidou.com/categories/Tools/"/>
    <category term="AI" scheme="https://xilidou.com/tags/AI/"/>
    <category term="LLM" scheme="https://xilidou.com/tags/LLM/"/>
    <category term="Mermaid" scheme="https://xilidou.com/tags/Mermaid/"/>
    <category term="text2mermaid" scheme="https://xilidou.com/tags/text2mermaid/"/>
    <category term="flowchart" scheme="https://xilidou.com/tags/flowchart/"/>
    <category term="sequence diagram" scheme="https://xilidou.com/tags/sequence-diagram/"/>
    <category term="ERD" scheme="https://xilidou.com/tags/ERD/"/>
    <category term="mindmap" scheme="https://xilidou.com/tags/mindmap/"/>
    <category term="gantt" scheme="https://xilidou.com/tags/gantt/"/>
    <category term="状态图" scheme="https://xilidou.com/tags/%E7%8A%B6%E6%80%81%E5%9B%BE/"/>
    <category term="类图" scheme="https://xilidou.com/tags/%E7%B1%BB%E5%9B%BE/"/>
    <category term="画图工具" scheme="https://xilidou.com/tags/%E7%94%BB%E5%9B%BE%E5%B7%A5%E5%85%B7/"/>
    <category term="技术文档" scheme="https://xilidou.com/tags/%E6%8A%80%E6%9C%AF%E6%96%87%E6%A1%A3/"/>
    <content>
      <![CDATA[<h2 id="引子"><a href="#引子" class="headerlink" title="引子"></a>引子</h2><p>我做了个小工具，<a href="https://text2everything.vip/">text2mermaid</a>（域名 <code>text2everything.vip</code>）——你用一句自然语言描述一个流程、一段接口时序、一组表关系，它调用大模型给你生成对应的 Mermaid 代码，实时预览，一键导出 SVG &#x2F; PNG。免注册，免费用。</p><p>这篇文章讲清楚三件事：为什么做、怎么用、什么场景下最省事。</p><p><img data-src="/images/text2mermaid-hero.png" alt="text2mermaid 首页：左侧文本框输入自然语言描述，右侧实时预览生成的 Mermaid 图，顶部可切换 flowchart / sequence / ERD 等 7 种图类型"></p><span id="more"></span><h2 id="为什么做"><a href="#为什么做" class="headerlink" title="为什么做"></a>为什么做</h2><p>Mermaid 是我写技术文档时的默认画图方案。它有几个好处很难被替代：</p><ul><li><strong>代码即图</strong>——存在 markdown 里、进 git、可 diff、跟着 PR 走版本</li><li><strong>在文档里原地渲染</strong>——GitHub &#x2F; GitLab &#x2F; Notion &#x2F; Obsidian &#x2F; Hexo 主题 都原生支持</li><li><strong>改起来快</strong>——想加一个节点直接敲一行，不用去挪线</li></ul><p>但它有一个隐形门槛：<strong>语法要背</strong>。flowchart 的 <code>A --&gt;|Yes| B</code>、sequence 的 <code>Alice -&gt;&gt; Bob:</code>、ERD 的 <code>USER ||--o&#123; POST : writes</code>、状态图的 <code>[*] --&gt; Idle</code>——七种图七套语法，写一次得查一次文档。写代码写到一半、脑子里都是业务逻辑，还得停下来去 Google “mermaid ERD cardinality symbols”，节奏就断了。</p><p>这两年做 <a href="/2026/07/07/harness1/">Loop Engineering 相关的工作</a>、写各种 AI 相关的博客，画图的频率上来以后，这个中断成本被放大了。之前的方法有几种，各有毛病：</p><ul><li><strong>draw.io &#x2F; excalidraw</strong>：拖拽画，好看，但出图不可 diff，改一个节点要重新导出图片贴回文档，跟不上文档节奏</li><li><strong>手写 Mermaid</strong>：跟文档节奏对齐，但语法上下文切换烦人</li><li><strong>让 ChatGPT &#x2F; Claude 帮忙</strong>：能生成，但要开一个对话、粘贴描述、复制代码、贴到文档、发现坏了再回去修——步骤太多，而且大模型经常给你生成一份<strong>语法看着对但渲染不出来</strong>的代码</li></ul><p>我想要的是<strong>一个专门做这一件事的入口</strong>：打开就用，光标已经在输入框里，回车就出预览，坏了它自己知道要修，好了直接复制或导出。text2mermaid 是这个想法的产物。</p><h2 id="怎么用"><a href="#怎么用" class="headerlink" title="怎么用"></a>怎么用</h2><p>打开 <a href="https://text2everything.vip/">text2everything.vip</a>，你会看到三栏布局：</p><ul><li><strong>顶部输入区</strong>：用中文或英文描述你要的图。可以直接说”用户登录流程，带 2FA”，也可以详细到”一个博客系统的 ER 图，users 和 posts 一对多，posts 和 comments 一对多，加上 tags 的多对多”</li><li><strong>左边代码 &#x2F; 右边预览</strong>：<code>⌘/Ctrl + Enter</code> 触发生成，代码和图同步刷新；也可以切成纯 Text &#x2F; 纯 Preview &#x2F; Split 三种视图</li><li><strong>图类型菜单</strong>：顶部 “Diagram types” 下拉里能切换 flowchart &#x2F; sequence &#x2F; ERD &#x2F; state &#x2F; class &#x2F; mindmap &#x2F; gantt，切换后 AI 会按目标图类型生成</li><li><strong>导出</strong>：右上角三个按钮——Copy code（贴回你的 markdown）、Export SVG（放文档里矢量清晰）、Export PNG（贴 Slack &#x2F; 微信）</li></ul><p>四个示例入口（Login flow with 2FA &#x2F; OAuth sequence &#x2F; Blog schema ERD &#x2F; CI&#x2F;CD pipeline）如果你懒得想 prompt，点一下就有输出，可以从改示例开始。</p><h2 id="支持哪些图"><a href="#支持哪些图" class="headerlink" title="支持哪些图"></a>支持哪些图</h2><p>Mermaid 官方支持十几种图，我先做了使用频率最高的七种，每种都有独立的 landing 页：</p><table><thead><tr><th>类型</th><th>用途</th><th>页面</th></tr></thead><tbody><tr><td>flowchart</td><td>业务流程、分支决策</td><td><a href="https://text2everything.vip/flowchart-from-text">&#x2F;flowchart-from-text</a></td></tr><tr><td>sequence diagram</td><td>接口时序、多服务交互</td><td><a href="https://text2everything.vip/sequence-diagram-from-text">&#x2F;sequence-diagram-from-text</a></td></tr><tr><td>erd</td><td>数据库表关系、领域模型</td><td><a href="https://text2everything.vip/erd-from-text">&#x2F;erd-from-text</a></td></tr><tr><td>state diagram</td><td>状态机、生命周期</td><td><a href="https://text2everything.vip/state-diagram-from-text">&#x2F;state-diagram-from-text</a></td></tr><tr><td>class diagram</td><td>UML 类图、继承</td><td><a href="https://text2everything.vip/class-diagram-from-text">&#x2F;class-diagram-from-text</a></td></tr><tr><td>mindmap</td><td>主题脑图、大纲</td><td><a href="https://text2everything.vip/mindmap-from-text">&#x2F;mindmap-from-text</a></td></tr><tr><td>gantt</td><td>项目排期、里程碑</td><td><a href="https://text2everything.vip/gantt-from-text">&#x2F;gantt-from-text</a></td></tr></tbody></table><h2 id="三个真实用得上的场景"><a href="#三个真实用得上的场景" class="headerlink" title="三个真实用得上的场景"></a>三个真实用得上的场景</h2><p><strong>写系统设计文档时画时序图</strong>。以前我要打开 mermaid 文档确认 <code>participant</code> 和 <code>-&gt;&gt;</code> 的顺序、<code>activate</code> 怎么写；现在直接一句”用户下单接口时序：Client 请求 API Gateway，Gateway 校验鉴权后调 Order Service，Order Service 扣库存并写 DB，异步发 Kafka 通知履约”，就是一张能用的图。改起来也简单——直接编辑生成出来的代码。</p><p><strong>评审前快速画 ER 图</strong>。给别人讲一个新表结构最快的方式是画一张 ERD。之前如果表有五张、关系复杂点，我会偷懒用文字描述；现在把描述扔进去就出图，还能 Export SVG 贴到评审文档。</p><p><strong>画 CI&#x2F;CD 的 flowchart</strong>。常见的坑是分支太多、图很快变复杂。用自然语言写描述，AI 会自动整理节点位置，比自己维护 <code>subgraph</code> 缩进舒服。</p><h2 id="关于开放性和成本"><a href="#关于开放性和成本" class="headerlink" title="关于开放性和成本"></a>关于开放性和成本</h2><ul><li><strong>免注册免费用</strong>。目前主要是自己在用，也希望更多人能试试。</li><li><strong>代码在你手上</strong>。生成后 Copy code &#x2F; Export SVG 都是纯文本 &#x2F; 纯 SVG，可以直接进你的 git 仓库，不锁定在这个网站。</li><li><strong>对国内友好</strong>。域名可直连，不需要梯子。</li></ul><p>如果你也是那种画图 &#x3D; 写 Mermaid 的人，把这个网站收藏起来，下次要画图时可以先来这里输一句话看看效果——顺手就顺手了，不合适再回去手写也不亏。</p><p>网站地址： <strong><a href="https://text2everything.vip/">https://text2everything.vip/</a></strong></p><p>有想加的图类型、遇到生成不对的 case、或者觉得哪里体验不好，欢迎在博客邮箱（<a href="mailto:&#100;&#x69;&#x61;&#111;&#122;&#x78;&#x69;&#110;&#64;&#49;&#54;&#51;&#46;&#x63;&#x6f;&#109;">&#100;&#x69;&#x61;&#111;&#122;&#x78;&#x69;&#110;&#64;&#49;&#54;&#51;&#46;&#x63;&#x6f;&#109;</a>）告诉我。</p>]]>
    </content>
    <id>https://xilidou.com/2026/07/20/text2mermaid/</id>
    <link href="https://xilidou.com/2026/07/20/text2mermaid/"/>
    <published>2026-07-20T15:30:00.000Z</published>
    <summary>介绍我最近做的一个小工具 text2mermaid（text2everything.vip）——用自然语言描述流程、时序、表关系、状态机等，AI 直接生成 Mermaid 代码并实时预览，支持 flowchart / sequence / ERD / state / class / mindmap / gantt 七种图，可导出 SVG / PNG，免注册免费用。文章讲清楚了做这个工具的动机、和手写 Mermaid / 传统画图工具的对比、以及三个真实使用场景。</summary>
    <title>text2mermaid — 我做了一个用自然语言生成 Mermaid 图的网站：为什么做、怎么用、支持哪些图</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="AI" scheme="https://xilidou.com/categories/AI/"/>
    <category term="AI" scheme="https://xilidou.com/tags/AI/"/>
    <category term="LLM" scheme="https://xilidou.com/tags/LLM/"/>
    <category term="Obsidian" scheme="https://xilidou.com/tags/Obsidian/"/>
    <category term="Claudian" scheme="https://xilidou.com/tags/Claudian/"/>
    <category term="Karpathy" scheme="https://xilidou.com/tags/Karpathy/"/>
    <category term="CLAUDE.md" scheme="https://xilidou.com/tags/CLAUDE-md/"/>
    <category term="知识管理" scheme="https://xilidou.com/tags/%E7%9F%A5%E8%AF%86%E7%AE%A1%E7%90%86/"/>
    <category term="Wiki" scheme="https://xilidou.com/tags/Wiki/"/>
    <category term="IDE" scheme="https://xilidou.com/tags/IDE/"/>
    <category term="Markdown" scheme="https://xilidou.com/tags/Markdown/"/>
    <content>
      <![CDATA[<h2 id="引子"><a href="#引子" class="headerlink" title="引子"></a>引子</h2><p>AI 时代，代码编辑器（idea）没变，变的是「围绕代码的所有上下文」需要一个新的承载层。本文把 Andrej Karpathy 提出的「Obsidian is the IDE, LLM is the programmer, wiki is the codebase」思路落地：用 Obsidian + Claudian 插件 + vault 根 <code>CLAUDE.md</code> schema，把设计决策、AI 对话历史、DB schema、跨服务上下文沉淀成可 Lint 的知识网络，让 LLM 从「一次性问答机」升级为纪律性的 wiki 维护者。文末给出一次两周跨度、15 个决策、13 个 commit 的重构实战，以及两条可以直接扔给 Claudian 的 Vault Lint prompt。</p><p><img data-src="/images/obsidian-as-ide-arch.png" alt="Obsidian vault 三层架构示意图：Raw sources（不可变原始资料）/ The wiki（LLM 维护的 Markdown 页）/ The schema（vault 根 CLAUDE.md），LLM 通过 Claudian 承担 Ingest、Query、Lint 三个操作，把知识库当作代码库来维护"></p><span id="more"></span><h2 id="一、AI-时代-idea-已经不够用了：从-IDE-到「认知承载层」"><a href="#一、AI-时代-idea-已经不够用了：从-IDE-到「认知承载层」" class="headerlink" title="一、AI 时代 idea 已经不够用了：从 IDE 到「认知承载层」"></a>一、AI 时代 idea 已经不够用了：从 IDE 到「认知承载层」</h2><p>使用 idea + AI 插件遇到几个问题:</p><ul><li>用问答的形式,问一句,写一段代码片段,人工 review 结果</li><li>和 AI 沟通过的记录,不好搜索</li><li>多次沟通以后上下文累加,AI 注意力不集中;重开 session 以后记忆清零,从头教他写代码</li><li>一个需求涉及多个微服务,代码需求不止在一个 codebase 里完成</li><li>在 idea 的视角里只有代码,没有数据库结构、中间件相关信息</li></ul><p>idea 已经不能满足 AI 时代的代码迭代了。</p><h2 id="二、Obsidian-是什么：本地优先的-Markdown-知识管理工具"><a href="#二、Obsidian-是什么：本地优先的-Markdown-知识管理工具" class="headerlink" title="二、Obsidian 是什么：本地优先的 Markdown 知识管理工具"></a>二、Obsidian 是什么：本地优先的 Markdown 知识管理工具</h2><p>Obsidian 官网：<a href="https://obsidian.md/">obsidian.md</a></p><p>Obsidian 是一个<strong>本地优先(local-first)、基于纯 Markdown 文件的知识管理工具</strong>。核心特点:</p><ul><li><strong>数据在本地</strong>:所有笔记就是磁盘上的 <code>.md</code> 文件,不锁定在某家云服务,离线可用,自己完全掌控</li><li><strong>双向链接(Wiki-links)</strong>:<code>[[note-name]]</code> 把碎片连成网,任何一篇笔记都能被反向查询「谁引用了我」</li><li><strong>图谱视图</strong>:笔记之间的引用关系可视化,一眼看出知识网络的形态</li><li><strong>插件生态</strong>:开放的插件市场,社区已经把它扩展成了任务管理、日历、Dataview 查询、AI 助手等各种形态</li><li><strong>纯文本 &#x3D; AI 友好</strong>:文件都是 UTF-8 Markdown,对 AI 来说是可读、可写、可 diff 的一等公民,不用像 idea 那种 IDE 需要专门的插件桥接</li></ul><p>回到上文列的 idea + AI 痛点,Obsidian 恰好每一条都对症:</p><table><thead><tr><th>idea + AI 的问题</th><th>Obsidian 的解</th></tr></thead><tbody><tr><td>问答式碎片、结果难 review</td><td>笔记是持久文档,AI 在这里改的每一笔都留痕、可回溯</td></tr><tr><td>沟通记录不好搜索</td><td>全文搜索 + 双向链接 + 标签,历史对话是可检索的知识资产</td></tr><tr><td>上下文累加,重开 session 记忆归零</td><td>用笔记显式落项目上下文,新 session 让 AI 先读笔记恢复记忆</td></tr><tr><td>一个需求涉及多个微服务</td><td>一个 vault 可以同时装 A、B、C 服务的笔记,链接跨服务打通</td></tr><tr><td>idea 只有代码,没有数据库&#x2F;中间件视角</td><td>vault 里可以并列放代码分析、DB schema、Kafka topic、部署拓扑</td></tr></tbody></table><p>换句话说,Obsidian 不是取代 idea 的<strong>编辑器</strong>,而是取代它的<strong>认知承载层</strong>——代码继续在 idea 里写,但<strong>围绕代码的所有上下文</strong>(设计决策、AI 对话历史、数据库结构、跨服务调用图)搬到 Obsidian 里长期沉淀。</p><hr><h2 id="三、环境准备：Obsidian-Claudian-插件"><a href="#三、环境准备：Obsidian-Claudian-插件" class="headerlink" title="三、环境准备：Obsidian + Claudian 插件"></a>三、环境准备：Obsidian + Claudian 插件</h2><ul><li>下载 Obsidian 并开启插件</li><li>安装 <a href="https://github.com/YishenTu/claudian">Claudian 插件</a>（GitHub），让 Claude Code 直接读写 vault</li></ul><hr><h2 id="四、理论基础：Karpathy-的「Wiki-as-Codebase」三层架构"><a href="#四、理论基础：Karpathy-的「Wiki-as-Codebase」三层架构" class="headerlink" title="四、理论基础：Karpathy 的「Wiki as Codebase」三层架构"></a>四、理论基础：Karpathy 的「Wiki as Codebase」三层架构</h2><p>理论出处：<a href="https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f">Andrej Karpathy 的这篇 gist</a> 一句话破题(下方为原文直译):</p><blockquote><p>“Obsidian is the IDE; the LLM is the programmer; the wiki is the codebase.”</p><p>—— Obsidian 是 IDE,LLM 是程序员,wiki 是 codebase。</p></blockquote><p>也就是说,你写的每份笔记都不是”文档”,而是 LLM 正在维护的一份<strong>代码库</strong>——只不过存的是关于世界的结构化知识,不是可执行指令。</p><h3 id="4-1-三层架构"><a href="#4-1-三层架构" class="headerlink" title="4.1 三层架构"></a>4.1 三层架构</h3><p>Karpathy 把整个系统分成三层,恰好对应 vault 里三类文件:</p><table><thead><tr><th>层</th><th>定义</th><th>对应</th></tr></thead><tbody><tr><td><strong>Raw sources</strong></td><td>不可变原始资料,LLM 只读不改</td><td>粘贴进来的会议纪要、PDF、代码 diff、剪藏文章</td></tr><tr><td><strong>The wiki</strong></td><td>LLM 生成&#x2F;维护的 markdown 页</td><td>你的分析笔记、决策记录、概念页</td></tr><tr><td><strong>The schema</strong></td><td>让 LLM 变成”有纪律的维护者”的配置</td><td>vault 根 <code>CLAUDE.md</code></td></tr></tbody></table><p><strong>关键点</strong>:<code>CLAUDE.md</code> 不只是”避免重复告诉 AI 项目背景”的便利工具,它是 schema——<strong>它决定了 LLM 是随手回答的聊天机器人,还是纪律性的 wiki 维护者</strong>。</p><h3 id="4-2-三个操作"><a href="#4-2-三个操作" class="headerlink" title="4.2 三个操作"></a>4.2 三个操作</h3><p>LLM 在这份 wiki 上做三件事:</p><ol><li><strong>Ingest</strong> —— 摄入新原始资料,更新多份 wiki 页,维护交叉引用</li><li><strong>Query</strong> —— 搜 wiki 综合答案,好答案回写成新页面</li><li><strong>Lint</strong> —— 检查矛盾、过时声明、孤儿页面、缺失链接</li></ol><p>前两个大多数人已经在做,Lint 是新能力——<strong>给知识库跑健康检查</strong>,和给代码跑 lint 一个道理。</p><h3 id="4-3-为什么现在才可行"><a href="#4-3-为什么现在才可行" class="headerlink" title="4.3 为什么现在才可行"></a>4.3 为什么现在才可行</h3><p>人类曾经普遍放弃维护内部 wiki,因为<strong>维护成本超过收益</strong>——没人有精力持续更新交叉引用、消除矛盾、补链接。</p><p>LLM 恰好补上这个瓶颈:<strong>“人筛选原始资料、指定分析方向、问好问题”,LLM 承担所有的记账工作</strong>——一次 ingest 触碰几十份文件,成本几乎为零。</p><p>不是 Obsidian 变强了,是<strong>wiki 的维护成本从人身上转移到了 LLM 身上</strong>。</p><hr><h2 id="五、怎么用：一个-vault-CLAUDE-md-四个日常动作"><a href="#五、怎么用：一个-vault-CLAUDE-md-四个日常动作" class="headerlink" title="五、怎么用：一个 vault + CLAUDE.md + 四个日常动作"></a>五、怎么用：一个 vault + CLAUDE.md + 四个日常动作</h2><h3 id="5-1-一个项目一个-vault-CLAUDE-md"><a href="#5-1-一个项目一个-vault-CLAUDE-md" class="headerlink" title="5.1 一个项目一个 vault + CLAUDE.md"></a>5.1 一个项目一个 vault + <code>CLAUDE.md</code></h3><p>不用一个 vault 装所有东西。每个项目建一个子文件夹,vault 根放 <code>CLAUDE.md</code>——技术栈、目录结构、commit 规范、常见 gotcha、每次开新对话都要重复告诉 AI 的东西,一次写清楚。Claudian 每次启动自动读它,session 重开不再从零。</p><h3 id="5-2-需求进来的典型流程"><a href="#5-2-需求进来的典型流程" class="headerlink" title="5.2 需求进来的典型流程"></a>5.2 需求进来的典型流程</h3><p>对比过去(打开 AI 侧边栏 → 问一句 → 复制代码 → 关闭 → 记忆归零),现在的路径:</p><ol><li><strong>建需求笔记</strong> —— 背景、目标、约束</li><li><strong>让 AI 读上下文</strong> —— 用 <code>[[]]</code> 引架构、DB schema、历史需求笔记,Claudian 自动读</li><li><strong>对话式分析</strong> —— 出方案,追问,拍板,<strong>关键决策让 AI 写回笔记</strong></li><li><strong>回 idea 写代码</strong> —— 笔记里的方案作为提示</li><li><strong>回写决策</strong> —— 代码写完把”为什么这么改 &#x2F; 踩了什么坑 &#x2F; commit hash”记回笔记</li><li><strong>下一个需求</strong> —— 老笔记 &#x3D; 新对话的上下文</li></ol><p><strong>心态转变</strong>:AI 的输出不是「用完即弃的问答」,而是<strong>沉淀成文档</strong>。</p><h3 id="5-3-日常高频动作"><a href="#5-3-日常高频动作" class="headerlink" title="5.3 日常高频动作"></a>5.3 日常高频动作</h3><ul><li><strong><code>@</code> 引用文件</strong> —— 直接把某笔记塞进对话上下文,跨服务对齐特别好用</li><li><strong>粘代码进笔记让 AI 改</strong> —— 结果留在笔记里,可回看</li><li><strong>决策记录(ADR)</strong> —— 每个架构决策一个笔记,<code>[[]]</code> 串起来,半年后不用猜”当时为什么这么设计”</li><li><strong>daily note 记排查过程</strong> —— 实时思路直接进当天日记,AI 基于流水推进</li><li><strong>图谱视图</strong> —— 一眼看哪些模块笔记密集(核心区),哪些孤立(遗漏区)</li></ul><h3 id="5-4-和-idea-的分工"><a href="#5-4-和-idea-的分工" class="headerlink" title="5.4 和 idea 的分工"></a>5.4 和 idea 的分工</h3><table><thead><tr><th>场景</th><th>idea</th><th>Obsidian + Claudian</th></tr></thead><tbody><tr><td>写代码、debug、跑测试</td><td>✅</td><td></td></tr><tr><td>断点调试、profiler</td><td>✅</td><td></td></tr><tr><td>需求分析、方案设计</td><td></td><td>✅</td></tr><tr><td>架构文档、DB schema</td><td></td><td>✅</td></tr><tr><td>和 AI 长对话、决策落地</td><td></td><td>✅</td></tr><tr><td>跨服务上下文串联</td><td></td><td>✅</td></tr><tr><td>团队共享知识、复盘</td><td></td><td>✅</td></tr></tbody></table><p>一句话:<strong>idea 负责「码」,Obsidian 负责「脑」</strong>。</p><hr><h2 id="六、实战案例：一次两周、15-个决策、13-个-commit-的重构复盘"><a href="#六、实战案例：一次两周、15-个决策、13-个-commit-的重构复盘" class="headerlink" title="六、实战案例：一次两周、15 个决策、13 个 commit 的重构复盘"></a>六、实战案例：一次两周、15 个决策、13 个 commit 的重构复盘</h2><p>在本地做了一个项目 P1~P6 六个阶段主体 + 4 轮架构 refactor,涉及 15 个技术决策(D1-D15),持续两周。</p><p>如果只在 idea 里做,几乎不可能完成——每次开新 session 记忆归零,反复解释项目背景就要废掉一半时间。Obsidian 在这里做了 5 件事:</p><ol><li><strong>一份主计划笔记贯穿始终</strong> —— 所有对话从它出发,笔记本身也被 AI 补写、修订</li><li><strong>15 个决策全部落笔</strong> —— 每个都记「背景 &#x2F; 备选 &#x2F; 选择 &#x2F; 理由」,不留在对话里</li><li><strong>跨 session 无缝续接</strong> —— 第七天开新 session,一句”读一下这份笔记继续”就够了</li><li><strong>commit hash 回写笔记</strong> —— 13 个 commit 全部登记,笔记直接变 changelog</li><li><strong>最终沉淀到 CLAUDE.md</strong> —— 下一轮改造进来,新 session 直接知道现状</li></ol><p>反过来,如果只在 idea 里做?15 个决策散落在几十次对话里搜不到、复现不了;每次新 session 浪费 20 分钟重建上下文;commit 之间的”设计意图”3 个月后自己都看不懂。</p><p><strong>结论</strong>:改造规模越大、跨度越长,Obsidian + Claudian 的 leverage 越明显。这不是可有可无的辅助,是<strong>让”AI 参与长周期复杂改造”变得可行的基础设施</strong>。</p><hr><h2 id="七、实操：给-Obsidian-vault-跑一次-Lint（死链-孤儿扫描）"><a href="#七、实操：给-Obsidian-vault-跑一次-Lint（死链-孤儿扫描）" class="headerlink" title="七、实操：给 Obsidian vault 跑一次 Lint（死链 + 孤儿扫描）"></a>七、实操：给 Obsidian vault 跑一次 Lint（死链 + 孤儿扫描）</h2><p>理论讲完,回到最实用的一个动作:<strong>定期让 AI 给你自己的 vault 跑健康检查</strong>。这是把 wiki 当代码库来 code review。</p><p>推荐两类基础 Lint,一句 prompt 就能跑通。你在自己 vault 上跑一遍才有意义,这里给的是<strong>模板 + 结论解读</strong>。</p><h3 id="7-1-死链扫描：找出「你以为写过但没写」的核心笔记"><a href="#7-1-死链扫描：找出「你以为写过但没写」的核心笔记" class="headerlink" title="7.1 死链扫描：找出「你以为写过但没写」的核心笔记"></a>7.1 死链扫描：找出「你以为写过但没写」的核心笔记</h3><p><strong>Prompt(直接扔给 Claudian)</strong>:</p><blockquote><p>扫全 vault,找所有形如 <code>[[xxx]]</code> 但目标文件不存在的 wikilink,按引用次数排序,输出 top 30。忽略图片 embed、代码块内、Templater 变量。</p></blockquote><p>跑完的报告,基本可分成 6 类:</p><table><thead><tr><th>类别</th><th>典型</th><th>处理</th></tr></thead><tbody><tr><td>A. Daily 前后日跳转</td><td><code>[[2026-05-14_周四]]</code></td><td>模板机制常态,可忽略;或让 Templater 只在文件存在时渲染</td></tr><tr><td>B. 模板占位符误识别</td><td><code>[[&lt;% after_date %&gt;]]</code></td><td>假死链,忽略</td></tr><tr><td>C. 剪藏工具事故</td><td>网页里带 <code>[[]]</code> 的评论者昵称、URL 被误转成 wikilink</td><td>批量搜索替换清理</td></tr><tr><td><strong>D. 想链但没写</strong></td><td>某个概念被反复引用却始终没建笔记</td><td><strong>明确该行动</strong>——被引用次数越多,越该优先补写</td></tr><tr><td>E. 拼写 &#x2F; 格式错误</td><td>结尾多反斜杠、大小写不匹配</td><td>修 bug 一样直接改</td></tr><tr><td>F. MOC 目录缺失</td><td>目录名被链但没同名索引页</td><td>建索引页</td></tr></tbody></table><p><strong>Lint 的价值在 D 类</strong>——高频死链等于<strong>你自己反复觉得”这里该有一篇笔记”,但一直没写</strong>。这是知识网络给你的具体行动清单。不跑这一遍,你根本不知道自己欠了多少债。</p><h3 id="7-2-孤儿扫描：找出「知识组织结构上的洞」"><a href="#7-2-孤儿扫描：找出「知识组织结构上的洞」" class="headerlink" title="7.2 孤儿扫描：找出「知识组织结构上的洞」"></a>7.2 孤儿扫描：找出「知识组织结构上的洞」</h3><p><strong>Prompt</strong>:</p><blockquote><p>找出没有任何其他笔记通过 wikilink 引用它的孤儿笔记。排除 daily notes、入口文件、CLAUDE.md、空笔记。按文件大小排序,大文件优先。</p></blockquote><p>跑完你通常会看到三种模式:</p><ul><li><strong>成规模的孤儿集群</strong> —— 某个目录(某本书的章节笔记、某个专题的系列文章)整批全是孤儿 → <strong>缺一个 MOC 索引把系列串起来</strong>。这不是”补一条链接”的问题,是<strong>知识组织结构有洞</strong>。这种结构性问题只有跑 Lint 才看得出来,单独打开某一篇笔记时永远发现不了</li><li><strong>孤儿入口文件</strong> —— 名字像入口(<code>xx 总览</code>、<code>xx 看板</code>)但没有任何入链 → <strong>做了入口没人走</strong>,需要在其他笔记里显式引用它</li><li><strong>空 &#x2F; 单行笔记</strong> —— 未命名、只写了标题就没下文 → <strong>直接删</strong></li></ul><h3 id="7-3-定期跑-Lint-的收益：把-wiki-当-codebase-来-code-review"><a href="#7-3-定期跑-Lint-的收益：把-wiki-当-codebase-来-code-review" class="headerlink" title="7.3 定期跑 Lint 的收益：把 wiki 当 codebase 来 code review"></a>7.3 定期跑 Lint 的收益：把 wiki 当 codebase 来 code review</h3><p>一遍 Lint 能给你四类具体产出:</p><ul><li><strong>明确的下一步行动</strong>:高频死链 &#x3D; 该建的核心笔记清单</li><li><strong>结构性问题的暴露</strong>:成群的孤儿集群 &#x3D; 缺的 MOC 索引</li><li><strong>数据清洁</strong>:剪藏事故、拼写错误、空笔记</li><li><strong>入口文件的自我审计</strong>:名字是入口但没入链 &#x3D; 走不通的入口</li></ul><p><strong>建议节奏</strong>:每月一次,或每次大批量扔资料进 vault 之后跑一次。</p><p><strong>不做 Lint 的 vault,两三年后基本是个大坟场</strong>——链接密度看起来很高,但绝大多数是死链和孤岛。反过来,坚持做 Lint,vault 就永远保持”活的”——每一条链都能到达,每一个节点都被引用。这就是 Karpathy 说的 Lint。</p><hr><h2 id="八、小结：idea-负责码，Obsidian-Claudian-负责脑"><a href="#八、小结：idea-负责码，Obsidian-Claudian-负责脑" class="headerlink" title="八、小结：idea 负责码，Obsidian + Claudian 负责脑"></a>八、小结：idea 负责码，Obsidian + Claudian 负责脑</h2><ul><li><strong>idea</strong> 是代码的编辑器,它没变,依然好用</li><li><strong>Obsidian</strong> 是认知的承载层,补上 idea 缺失的另一半——设计意图、AI 对话历史、跨服务上下文、决策脉络</li><li><strong>Claudian</strong> 是粘合剂,让 AI 直接读写你的知识网络,不再是「一次性问答机」</li><li><strong><code>CLAUDE.md</code></strong> 是 schema,让 LLM 从聊天机器人升级为纪律性维护者</li><li><strong>Lint 操作</strong> 是自我维护的核心动作,让 vault 长期不烂</li></ul><p>组合起来,你才真正拥有一个「AI 时代的 IDE」——不是把 AI 塞进 idea 侧边栏的拼贴,而是<strong>围绕知识组织重新设计的开发工作流</strong>。</p>]]>
    </content>
    <id>https://xilidou.com/2026/07/15/obsidian-as-ide/</id>
    <link href="https://xilidou.com/2026/07/15/obsidian-as-ide/"/>
    <published>2026-07-15T21:23:41.000Z</published>
    <summary>用 Obsidian 替代 idea 作为 AI 时代的&quot;认知承载层&quot;——把 Andrej Karpathy 的「Obsidian is the IDE, LLM is the programmer, wiki is the codebase」落到实处：Claudian 插件 + 根 CLAUDE.md schema + Ingest / Query / Lint 三个操作，再加上一次两周跨度重构的实战复盘和两条可直接跑的 Vault Lint prompt。</summary>
    <title>使用 Obsidian 作为 AI 时代的 IDE：Karpathy「Wiki as Codebase」的落地实践——Claudian、CLAUDE.md 与 Vault Lint</title>
    <updated>2026-08-08T02:04:49.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="AI" scheme="https://xilidou.com/categories/AI/"/>
    <category term="Agent" scheme="https://xilidou.com/tags/Agent/"/>
    <category term="AI" scheme="https://xilidou.com/tags/AI/"/>
    <category term="Claude Code" scheme="https://xilidou.com/tags/Claude-Code/"/>
    <category term="Harness" scheme="https://xilidou.com/tags/Harness/"/>
    <category term="LLM" scheme="https://xilidou.com/tags/LLM/"/>
    <category term="Loop Engineering" scheme="https://xilidou.com/tags/Loop-Engineering/"/>
    <category term="受控循环" scheme="https://xilidou.com/tags/%E5%8F%97%E6%8E%A7%E5%BE%AA%E7%8E%AF/"/>
    <category term="系统设计" scheme="https://xilidou.com/tags/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/"/>
    <category term="总览" scheme="https://xilidou.com/tags/%E6%80%BB%E8%A7%88/"/>
    <content>
      <![CDATA[<h2 id="Harness-的本质是什么？"><a href="#Harness-的本质是什么？" class="headerlink" title="Harness 的本质是什么？"></a>Harness 的本质是什么？</h2><p>Harness 就是通过工程的手段，让 LLM 一轮一轮地挑选合适的 Tools，在受控的情况下完成工作。</p><span id="more"></span><h2 id="如何理解-Harness-各个模块"><a href="#如何理解-Harness-各个模块" class="headerlink" title="如何理解 Harness 各个模块"></a>如何理解 Harness 各个模块</h2><ul><li>为了能够告诉 LLM 有什么 tools，所以需要有 tools 管理模块。获取 tools 列表，把 tools 的名字和描述告诉模型。</li><li>Tools 需要在受控的情况使用，为 tools 增加权限管理模块。高风险操作需要用户同意。</li><li>为了增强 tools 的能力，增加了 hook。类似 Java 的 AOP 切面，在工具调用前、调用后扩展 tools 能力。</li><li>为了让 Agent 更好地驱动 LLM 完成复杂任务，提供了 todo write 工具和 task system，让 LLM 先出计划再干活。</li><li>为了隔离子任务的上下文，避免污染主 Agent，设计了 subAgent，用干净的上下文执行子任务。</li><li>有些场景需要多 tools 组合执行、遵循特定流程，为了把这类领域知识封装成”说明书”，设计了 skill 系统。</li><li>多轮调用以后可能造成上下文膨胀，设计了上下文压缩能力，缓解 LLM 注意力问题。</li><li>为了让 LLM 能够记录关键信息并跨会话共享，增加了 Memory 模块。</li><li>Tools 列表、Skill 说明、Memory 内容，这些能力都需要通过 System prompt “告诉” LLM，所以设计了 System prompt 系统。</li><li>大模型是远程调用，可能有各种各样的失败，需要处理 LLM 返回的各种异常，保证 Agent 能够从故障中恢复。</li><li>在某些场景中 tools 可能需要执行很长时间，可以生成一个 background task 在后台运行，不阻塞主 Agent。</li><li>结合后台任务和定时触发器，就能支持定时任务（Cron），让 Agent 按计划自主运行。</li><li>为了提升工作效率，引入了多 teammate 协作机制。</li><li>为了方便多 teammate 协作，规定了 teammate 之间的交流协议。</li><li>Teammate 完成工作后也不用闲着，可以去任务板自主领取新任务。</li><li>多个 teammate 并行工作时为了不互相干扰，增加了 Worktree Isolation，让大家各干各的。</li></ul>]]>
    </content>
    <id>https://xilidou.com/2026/07/10/harness4/</id>
    <link href="https://xilidou.com/2026/07/10/harness4/"/>
    <published>2026-07-10T21:12:17.000Z</published>
    <summary>AI in Harness 系列收官篇——把前三篇（Loop / Tools / Skill / Memory、Error Recovery / Task System / Background Task、Multi-Agent 协同）串起来给出 Harness 的完整定义：让 LLM 在&quot;受控循环&quot;中工作的一整套基础设施。附模块总览图、每个模块的职责边界、模块之间的调用关系，回答&quot;什么是 Agent Harness、和 Agent Framework 有何区别&quot;。</summary>
    <title>AI in Harness（四）——Harness 的本质与模块总览：让 LLM 在受控循环中工作</title>
    <updated>2026-08-08T02:04:49.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="AI" scheme="https://xilidou.com/categories/AI/"/>
    <category term="并发" scheme="https://xilidou.com/tags/%E5%B9%B6%E5%8F%91/"/>
    <category term="Agent" scheme="https://xilidou.com/tags/Agent/"/>
    <category term="AI" scheme="https://xilidou.com/tags/AI/"/>
    <category term="Claude Code" scheme="https://xilidou.com/tags/Claude-Code/"/>
    <category term="Harness" scheme="https://xilidou.com/tags/Harness/"/>
    <category term="LLM" scheme="https://xilidou.com/tags/LLM/"/>
    <category term="Multi-Agent" scheme="https://xilidou.com/tags/Multi-Agent/"/>
    <category term="Protocol" scheme="https://xilidou.com/tags/Protocol/"/>
    <category term="Autonomous" scheme="https://xilidou.com/tags/Autonomous/"/>
    <category term="Worktree" scheme="https://xilidou.com/tags/Worktree/"/>
    <content>
      <![CDATA[<h2 id="多Agent-协同-需要一个团队"><a href="#多Agent-协同-需要一个团队" class="headerlink" title="多Agent 协同 - 需要一个团队"></a>多Agent 协同 - 需要一个团队</h2><p>前面我们实现了 Subagent、Background Task，为什么还需要多 Agent 协同呢？</p><h3 id="一句话区分"><a href="#一句话区分" class="headerlink" title="一句话区分"></a>一句话区分</h3><ul><li><strong>Background Task</strong> &#x3D; “把这个工具调用<strong>派到后台跑,我等结果通知</strong>“</li><li><strong>Subagent</strong> &#x3D; “派一个新 agent <strong>干一件具体事,跑完销毁,我等摘要</strong>“</li><li><strong>Teammate</strong> &#x3D; “派一个<strong>长期协作的队友</strong>,我们持续异步通信”</li></ul><h3 id="对比表"><a href="#对比表" class="headerlink" title="对比表"></a>对比表</h3><table><thead><tr><th>维度</th><th>Background Task</th><th>Subagent</th><th>Teammate</th></tr></thead><tbody><tr><td><strong>被派的是什么</strong></td><td>一个工具调用(bash)</td><td>一个全新 agent(独立 LLM + messages)</td><td>一个全新 agent</td></tr><tr><td><strong>谁在执行</strong></td><td>工具 executor</td><td>新一轮 LLM 调用</td><td>新一轮 LLM 调用</td></tr><tr><td><strong>生命周期</strong></td><td>短(工具一次调用结束)</td><td>短(派一次跑完销毁)</td><td>长(教学版限 10 轮)</td></tr><tr><td><strong>同步 &#x2F; 异步</strong></td><td>异步(daemon thread)</td><td><strong>同步</strong>(父等子返回)</td><td>异步(daemon thread)</td></tr><tr><td><strong>父是否阻塞</strong></td><td>不阻塞,立即拿 placeholder</td><td><strong>阻塞</strong>,等子 agent 结果</td><td>不阻塞,立即拿”已派出”</td></tr><tr><td><strong>通信方式</strong></td><td>单向 — 后台→父 (<code>&lt;task_notification&gt;</code>)</td><td>单向 — 子→父(只回 last text)</td><td><strong>双向</strong> — <code>MessageBus</code> 文件邮箱</td></tr><tr><td><strong>能否多个并行</strong></td><td>是</td><td>否(父 spawn 时阻塞)</td><td>是</td></tr><tr><td><strong>能否互相通信</strong></td><td>不能</td><td>不能</td><td><strong>能</strong>(teammate 之间能 send)</td></tr><tr><td><strong>典型用途</strong></td><td>慢命令(<code>./mvnw test</code>)</td><td>“分析 X 模块,做完告诉我”</td><td>“重构后端 — 多 agent 长期协作”</td></tr><tr><td><strong>能调用工具</strong></td><td>不能(它<strong>就是</strong>工具)</td><td>能(白名单子集)</td><td>能(白名单 + send_message)</td></tr></tbody></table><p>人多力量大。遇到一个很大的任务时，从 Java 程序员的思路来看，可以多来几个分布式 Agent 来协同处理问题。</p><p>还有一个核心问题，多个Agent 之间怎么通信呢？</p><p>Background task 和 Subagent 都是单向通信：父向子安排工作后，子完成后单向地向父汇报结果，中间没有交互。</p><p>Teammate 之间是同事关系。双方之间通过 MessageBus 进行双向通信。</p><p>MessageBus 基于文件形式，Agent A 告诉 Agent B 应该干什么，Agent B 告诉 Agent A 我完成了。<br> <span id="more"></span></p><h3 id="Protocols-从”能聊”到”聊得清”"><a href="#Protocols-从”能聊”到”聊得清”" class="headerlink" title="Protocols - 从”能聊”到”聊得清”"></a>Protocols - 从”能聊”到”聊得清”</h3><p>MessageBus 让 Agent “能聊”，但只有字节流。Protocols 在 MessageBus 之上加一层结构化握手 + 状态机 + 类型校验，把”扔字符串靠 LLM 意会”升级为”业务级应答”。</p><h4 id="Protocols-强制的三件套"><a href="#Protocols-强制的三件套" class="headerlink" title="Protocols 强制的三件套"></a>Protocols 强制的三件套</h4><table><thead><tr><th>给了什么</th><th>没它会怎样</th></tr></thead><tbody><tr><td><strong>关联键</strong>（request_id）</td><td>多个并行请求的响应分不清谁回谁</td></tr><tr><td><strong>状态机</strong>（pending → approved&#x2F;rejected）</td><td>不知道某请求”现在到哪一步”</td></tr><tr><td><strong>类型校验</strong>（shutdown 只能被 shutdown_response 应）</td><td>响应误处理别的请求，状态错乱</td></tr></tbody></table><p>这三个是任何<strong>可靠请求-响应通信</strong>的最小公因子（HTTP、RPC、分布式系统都有），Protocols 只是把它搬到 LLM Agent 之间。</p><h4 id="相关-API"><a href="#相关-API" class="headerlink" title="相关 API"></a>相关 API</h4><table><thead><tr><th>API</th><th>作用</th><th>关键点</th></tr></thead><tbody><tr><td><code>request_shutdown(teammate)</code></td><td>请队友体面退出</td><td>自动分配 <code>req_id</code>，注册 pending 状态</td></tr><tr><td><code>submit_plan(plan)</code></td><td>队友提交计划待审批</td><td>拿到 <code>req_id</code> 后进入等待</td></tr><tr><td><code>review_plan(req_id, approve, feedback)</code></td><td>Lead 审批 &#x2F; 拒绝并附反馈</td><td>只改自己那一条 state</td></tr><tr><td><code>protocols.list()</code></td><td>查看所有请求当前状态</td><td>pending &#x2F; approved &#x2F; rejected</td></tr><tr><td><code>check_inbox()</code></td><td>LLM 主动收信；自动路由 <code>*_response</code></td><td>协议消息不给 LLM 看，普通消息才展示</td></tr></tbody></table><p>设计要点：<strong>协议消息自动路由</strong>，LLM 只看到”这个请求现在是什么状态”，不用自己去 parse <code>_response</code> 消息。</p><h4 id="定位：Protocols-是-RPC-那一层，不是-TCP-那一层"><a href="#定位：Protocols-是-RPC-那一层，不是-TCP-那一层" class="headerlink" title="定位：Protocols 是 RPC 那一层，不是 TCP 那一层"></a>定位：Protocols 是 RPC 那一层，不是 TCP 那一层</h4><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">MessageBus:   带外、双向、多次的通信通道           — 能聊（可靠传输，文件落盘）</span><br><span class="line">Protocols:    通道上的握手 / 关联 / 状态机          — 聊得清（业务级同意/拒绝）</span><br></pre></td></tr></table></figure><p>Protocols 的 ack 不是 TCP 那种”字节收到了”，而是 LLM 主动决定的”我同意&#x2F;拒绝”——语义级 ack，内容是 <code>request_id + approve + feedback</code>。</p><h4 id="Protocols-不解决的问题"><a href="#Protocols-不解决的问题" class="headerlink" title="Protocols 不解决的问题"></a>Protocols 不解决的问题</h4><p>教学版严格对齐 happy path，以下都留给生产化再补：</p><table><thead><tr><th>问题</th><th>当前表现</th><th>后续补什么</th></tr></thead><tbody><tr><td>响应丢了 &#x2F; 对方挂了</td><td>pending 状态永远卡着</td><td>timeout + 由 LLM 自己决定放弃</td></tr><tr><td>消息因故未送达</td><td>Lead 不知道也不会重发</td><td>重试 + 幂等</td></tr><tr><td>对方是否还活着</td><td>只能等下次 LLM 主动 check</td><td>心跳 &#x2F; 探活</td></tr></tbody></table><h4 id="一句话总结"><a href="#一句话总结" class="headerlink" title="一句话总结"></a>一句话总结</h4><p><strong>MessageBus 让 Agent “能聊”，Protocols 让 Agent “聊得清”——从 chat 升级到 RPC。</strong></p><h3 id="Autonomous-Agents-从”被派活”到”自己领活”"><a href="#Autonomous-Agents-从”被派活”到”自己领活”" class="headerlink" title="Autonomous Agents - 从”被派活”到”自己领活”"></a>Autonomous Agents - 从”被派活”到”自己领活”</h3><p>Teammate 已经能通信、能协商，但<strong>任务分配还是靠 Lead 手动 spawn 时指定</strong>。Autonomous Agents 让 teammate 从 TaskBoard 自己扫未认领任务、自动 claim、做完再找下一个 —— 从”被动接令”升级到”自组织 worker”。</p><h4 id="核心机制：WORK-↔-IDLE-双循环"><a href="#核心机制：WORK-↔-IDLE-双循环" class="headerlink" title="核心机制：WORK ↔ IDLE 双循环"></a>核心机制：WORK ↔ IDLE 双循环</h4><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></pre></td><td class="code"><pre><span class="line">outer loop (总 turn 上限 30):</span><br><span class="line">  ├── WORK 阶段 (inner 最多 10 LLM call)</span><br><span class="line">  │     消费 inbox → LLM 调用 → 执行工具 → 无 tool_use 就进 IDLE</span><br><span class="line">  └── IDLE 阶段 (idlePoll, 默认 60s 超时)</span><br><span class="line">        每 5s 轮询:</span><br><span class="line">          1. 优先看 inbox（可能有 shutdown_request）</span><br><span class="line">          2. inbox 空 → 扫看板 tryClaim</span><br><span class="line">          任一命中 → 回 WORK</span><br><span class="line">        超时 → shutdown</span><br></pre></td></tr></table></figure><p><strong>双层 turn 上限的意义</strong>：</p><ul><li>inner 10：一次 WORK 最多 10 LLM call，防止闷头干太久，定期”喘气”处理 inbox</li><li>outer 30：daemon thread 总 LLM 预算 ceiling，防永生</li></ul><h4 id="关键实现点"><a href="#关键实现点" class="headerlink" title="关键实现点"></a>关键实现点</h4><table><thead><tr><th>点</th><th>做法</th></tr></thead><tbody><tr><td><strong>不造新轮子</strong></td><td>scan&#x2F;claim 复用 TaskService 已有的 <code>list / canStart / claim</code></td></tr><tr><td><strong>owner 强制注入</strong></td><td><code>claim_task</code> 调用时代码层自动填 <code>owner=teammateName</code>，不给 LLM 传错的机会</td></tr><tr><td><strong>工具白名单</strong></td><td>只开 <code>list_tasks / claim_task / complete_task</code>；不给 <code>create_task</code>（task 创建是 Lead 职责）</td></tr><tr><td><strong>身份重注入</strong></td><td><code>messages.size() &lt;= 3</code> 时补 <code>&lt;identity&gt;</code>，防 compact 后失忆</td></tr><tr><td><strong>抢任务竞态</strong></td><td><code>tryClaim</code> 遇到 claim 失败自动跳下一个 task；同进程内 ConcurrentHashMap 语义安全，跨进程需要文件锁（未做）</td></tr></tbody></table><h3 id="Worktree-Isolation-从”共享目录”到”各干各的”"><a href="#Worktree-Isolation-从”共享目录”到”各干各的”" class="headerlink" title="Worktree Isolation - 从”共享目录”到”各干各的”"></a>Worktree Isolation - 从”共享目录”到”各干各的”</h3><p>多个 worker 自助领任务后，都在同一个 <code>user.dir</code> 干活 —— 两人都 <code>write_file(&quot;config.py&quot;)</code> 就互相覆盖。Worktree Isolation 让每个 task 绑定独立 git worktree + 独立分支，改同名文件互不冲突。</p><h4 id="一句话总结-1"><a href="#一句话总结-1" class="headerlink" title="一句话总结"></a>一句话总结</h4><p><strong>Worktree Isolation 改的是「在哪执行」这一格 —— 把工作目录变成显式的调用参数，让 teammate 在各自 git worktree 里独立干活，改同名文件互不冲突。</strong></p><hr><p>至此，多 Agent 协同的四个维度全部展开：</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">通信       → MessageBus       (能聊)</span><br><span class="line">可靠协商   → Protocols        (聊得清)</span><br><span class="line">自组织调度 → Autonomous Agents (自己领活)</span><br><span class="line">工作目录隔离 → Worktree Isolation (各干各的)</span><br></pre></td></tr></table></figure><p>未完待续。</p>]]>
    </content>
    <id>https://xilidou.com/2026/07/09/harness3/</id>
    <link href="https://xilidou.com/2026/07/09/harness3/"/>
    <published>2026-07-09T21:12:17.000Z</published>
    <summary>AI in Harness 系列第三篇——为什么 Subagent + Background Task 之后还需要多 Agent 协同：Agent 之间的通信 Protocols、Autonomous Agents（自主 Agent）如何让主控在长任务中&quot;托管&quot;给下游、以及用 git Worktree 做并发文件写入隔离避免相互覆盖，让多个 Agent 真正能同时改代码。</summary>
    <title>AI in Harness（三）——多 Agent 协同：Protocols、Autonomous Agents 与 Worktree 隔离</title>
    <updated>2026-08-08T02:04:49.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="AI" scheme="https://xilidou.com/categories/AI/"/>
    <category term="并发" scheme="https://xilidou.com/tags/%E5%B9%B6%E5%8F%91/"/>
    <category term="Agent" scheme="https://xilidou.com/tags/Agent/"/>
    <category term="AI" scheme="https://xilidou.com/tags/AI/"/>
    <category term="Claude Code" scheme="https://xilidou.com/tags/Claude-Code/"/>
    <category term="Harness" scheme="https://xilidou.com/tags/Harness/"/>
    <category term="LLM" scheme="https://xilidou.com/tags/LLM/"/>
    <category term="Error Recovery" scheme="https://xilidou.com/tags/Error-Recovery/"/>
    <category term="Task System" scheme="https://xilidou.com/tags/Task-System/"/>
    <category term="Background Task" scheme="https://xilidou.com/tags/Background-Task/"/>
    <category term="任务规划" scheme="https://xilidou.com/tags/%E4%BB%BB%E5%8A%A1%E8%A7%84%E5%88%92/"/>
    <content>
      <![CDATA[<p>书接上文</p><h3 id="Error-Recovery-从错误中恢复"><a href="#Error-Recovery-从错误中恢复" class="headerlink" title="Error Recovery - 从错误中恢复"></a>Error Recovery - 从错误中恢复</h3><p>让 agent loop 在面对 LLM 三类常见故障(输出截断 &#x2F; context 太长 &#x2F; 后端过载)时自我恢复,而不是把异常一路抛回出去。</p><p>对于一个 Java 程序员处理异常那就太熟悉不过了。给 LLM 的调用方法加上 try catch。</p><p>目前最常见的错误，输出太长，输入太长，调用太快，模型压力太大。</p><p>输出太长：</p><ol><li><strong>第一次</strong>:把 <code>max_tokens</code> 从默认 8000 → 升级到 64000(escalated),<strong>丢掉截断的输出,重新跑这一轮</strong>。模型重生成,通常这次能说完</li><li><strong>第二次还截断</strong>:已经 64k 还说不完 → 真的需要”分两次说”。<strong>append 截断输出 + 注入 <code>Continue.</code> user message</strong>,让模型续写</li><li><strong>超过 <code>maxRecoveryRetries=3</code> 次 continuation</strong>:放弃,Fatal</li></ol><p>输入太长：</p><ol><li>可以直接调用 <code>reactive_compact</code> 压缩模式。使用比较激进的压缩模式，只保留最近的若干条消息。</li></ol><p>调用太快、模型压力过大：</p><ol><li>调用 LLM 的时候才用指数退避的方式重试。若干次后依然报错，则抛出异常。 <span id="more"></span></li></ol><h3 id="Task-System-更强的规划能力"><a href="#Task-System-更强的规划能力" class="headerlink" title="Task System - 更强的规划能力"></a>Task System - 更强的规划能力</h3><p><strong>让 LLM 自己用文件级任务图把”大目标”拆成”有 DAG 依赖、可被 claim&#x2F;complete 跟踪、跨 agent loop 持久化”的小任务,而不是把所有计划塞 todo_write 在内存里。</strong></p><p> 已经有 <code>todo_write</code> 了 —— 内存里一个三态 todo 列表,LLM 用它做计划。但 todo_write 有 4 个<strong>致命缺陷</strong>,只要任务复杂一点就暴露:</p><table><thead><tr><th>todo_write</th><th>task system</th></tr></thead><tbody><tr><td>只活在当前 session,重启 → 全没</td><td><strong>文件级持久化</strong>(<code>.tasks/&#123;id&#125;.json</code>)</td></tr><tr><td>只有”线性列表”,没法表达”B 必须等 A 完成才能做”</td><td><strong><code>blockedBy</code> DAG 依赖</strong>, <code>canStart</code> 计算</td></tr><tr><td>只有 owner 隐式(“当前 agent”),没 claim 概念</td><td><strong><code>claim_task</code> 显式 owner + 状态机</strong></td></tr><tr><td>整体替换语义(每次都重写整个列表)</td><td><strong>单条 update</strong>(完成 A 不动 B&#x2F;C)</td></tr></tbody></table><p>更重要的是 task system 让<strong>子 agent 也能参与</strong>—— 父 agent 派子 agent 干活时,子可以读到 task 列表知道全局计划,而不是”接到 description 就盲跑”。</p><p>核心 API：</p><p>实际上 harness 只暴露了 <strong>4 个原语</strong>，claim&#x2F;complete&#x2F;block 都是 <code>TaskUpdate</code> 的不同用法 —— 靠状态字段区分，而不是独立接口。</p><table><thead><tr><th>API</th><th>作用</th><th>关键参数</th><th>返回</th></tr></thead><tbody><tr><td><code>TaskCreate</code></td><td>建节点</td><td><code>subject</code>, <code>description</code>, <code>activeForm</code></td><td>新任务 <code>id</code>（初始 status&#x3D;pending, owner&#x3D;””）</td></tr><tr><td><code>TaskList</code></td><td>看队列</td><td>无</td><td>所有任务的摘要（id&#x2F;subject&#x2F;status&#x2F;owner&#x2F;blockedBy）</td></tr><tr><td><code>TaskGet</code></td><td>读详情</td><td><code>taskId</code></td><td>单个任务的完整字段（含 description、blocks&#x2F;blockedBy 双向边）</td></tr><tr><td><code>TaskUpdate</code></td><td>改一切</td><td><code>taskId</code> + 变更字段</td><td>更新后的任务</td></tr></tbody></table><p><strong>为什么没有独立的 <code>claim</code> &#x2F; <code>complete</code> &#x2F; <code>block</code> ?</strong> 因为它们都能用 TaskUpdate 的字段组合表达，合并成一个接口反而简化了状态机：</p><table><thead><tr><th>语义动作</th><th>对应的 TaskUpdate 调用</th></tr></thead><tbody><tr><td><strong>claim</strong>（认领）</td><td><code>&#123;taskId, owner: &quot;self&quot;, status: &quot;in_progress&quot;&#125;</code></td></tr><tr><td><strong>complete</strong>（完成）</td><td><code>&#123;taskId, status: &quot;completed&quot;&#125;</code></td></tr><tr><td><strong>建依赖</strong>（我等 X）</td><td><code>&#123;taskId, addBlockedBy: [X]&#125;</code></td></tr><tr><td><strong>建依赖</strong>（我挡 Y）</td><td><code>&#123;taskId, addBlocks: [Y]&#125;</code></td></tr><tr><td><strong>让出</strong>（放弃 owner）</td><td><code>&#123;taskId, owner: &quot;&quot;&#125;</code></td></tr><tr><td><strong>删除</strong></td><td><code>&#123;taskId, status: &quot;deleted&quot;&#125;</code></td></tr></tbody></table><p>设计要点：</p><ul><li><strong>TaskCreate 只建节点，不连边</strong> —— 依赖必须先拿到 ID 再用 TaskUpdate 补</li><li><strong>TaskList 是决策入口</strong> —— agent 每轮 tick 先 List 找可 claim 的（owner&#x3D;”” &amp;&amp; blockedBy&#x3D;[]）</li><li><strong>TaskGet 是执行前的最后一步</strong> —— claim 后进入 in_progress，用 TaskGet 拿完整 description 再干活（因为 List 只返回摘要，为省 token）</li><li><strong>TaskUpdate 幂等</strong> —— 重复 mark completed 不会出错，方便 error recovery 场景重放</li></ul><p>核心工作流程：<br>1、需要将一个复杂任务拆解。并确认 DAG （有向无环图）依赖。使用 DAG 意味着任务可并行， 可拓扑排序，无死锁。</p><blockquote><p>为什么 DAG 而不是树&#x2F;线性队列？真实开发依赖不是树形的：任务 D 可能同时依赖 B 和 C（菱形），线性队列表达不了并行，树形结构表达不了合并。DAG 是最小的能表达”部分序”的结构。<br>2、任务拆解以后，将任务持久化，方便 agent 认领提交。<br>3、Agent 会通过 tools，调用TaskList()获取 pending 和 无 blockedBy 的任务。<br>4、LLM 挑选一个 task 执行。<br>5、调用 TaskUpdate 更新 task 的状态。</p></blockquote><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></pre></td><td class="code"><pre><span class="line">[Agent tick N]</span><br><span class="line">  TaskList()  → 拿到 pending / owner=&quot;&quot; / blockedBy=[] 的任务</span><br><span class="line">  ↓</span><br><span class="line">  LLM 挑一个（通常最小 ID）</span><br><span class="line">  ↓</span><br><span class="line">  TaskUpdate(&#123;taskId, owner: &quot;self&quot;, status: &quot;in_progress&quot;&#125;)</span><br><span class="line">  ↓</span><br><span class="line">  （可选）同一 tick 内立即开始第一步执行 —— 发起 Read/Bash 等工具调用</span><br><span class="line">  ↓</span><br><span class="line">[后续多个 tick] 反复执行工具调用推进任务</span><br><span class="line">  ↓</span><br><span class="line">[某个 tick] TaskUpdate(&#123;taskId, status: &quot;completed&quot;&#125;)</span><br><span class="line">  ↓</span><br><span class="line">  回到 TaskList 找下一个可 claim 的任务</span><br></pre></td></tr></table></figure><blockquote><p>注意：claim 和”开始执行”通常发生在<strong>同一个 tick</strong> 里，harness 不会在 TaskUpdate 之后强制切换 —— LLM 一轮可以发多个 tool call。</p></blockquote><h3 id="Background-Task-后台执行耗时任务"><a href="#Background-Task-后台执行耗时任务" class="headerlink" title="Background Task - 后台执行耗时任务"></a>Background Task - 后台执行耗时任务</h3><p>**让 LLM 调慢工具(<code>./mvnw test</code>、<code>docker build</code>、<code>pip install</code>)时,Agent 立即返回 placeholder 让 LLM 继续工作,后台 daemon 线程跑完后通过 <code>&lt;task_notification&gt;</code> 文本块注入下一轮 — 取消同步等待</p><h4 id="为什么需要-Background-Task"><a href="#为什么需要-Background-Task" class="headerlink" title="为什么需要 Background Task"></a>为什么需要 Background Task</h4><p>如果 agent loop 是完全同步的:</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">LLM tool_use(bash, &quot;./mvnw test&quot;)</span><br><span class="line">  ↓</span><br><span class="line">agent loop 进程同步等子进程(直到默认 2 分钟超时)</span><br><span class="line">  ↓</span><br><span class="line">返回 ToolResult,继续 loop</span><br></pre></td></tr></table></figure><p>LLM 这段时间里<strong>完全失去主动权</strong> —— 不能并行去做别的小任务,不能主动决定”要不要 abort 重新调”,甚至连”我此刻在等什么”都看不到。当<strong>单线程 RPC 客户端</strong>用。</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></pre></td><td class="code"><pre><span class="line">LLM tool_use(bash, &quot;./mvnw test&quot;, run_in_background=true)</span><br><span class="line">  ↓</span><br><span class="line">agent 派 daemon thread → 立即返回 &quot;[Background task bg_0001 started]&quot;</span><br><span class="line">  ↓</span><br><span class="line">LLM 看到 placeholder,可以:</span><br><span class="line">  - 想&quot;我先去看下其他文件&quot;</span><br><span class="line">  - 调别的工具(读文件、跑快命令)</span><br><span class="line">  - 安排接下来的步骤</span><br><span class="line">  ↓</span><br><span class="line">后台 thread 完成,harness 在下一轮 tool_results 时</span><br><span class="line">  追加 &lt;task_notification id=&quot;bg_0001&quot;&gt;...&lt;/task_notification&gt;</span><br><span class="line">  ↓</span><br><span class="line">LLM 看到通知,自然消费结果(&quot;噢 test 跑完了,结果是 X&quot;)</span><br></pre></td></tr></table></figure><p>LLM 重新拿回<strong>主导权</strong>:它决定什么时候等、等多久、要不要先做别的事。</p><h4 id="相关-API"><a href="#相关-API" class="headerlink" title="相关 API"></a>相关 API</h4><table><thead><tr><th>API</th><th>作用</th></tr></thead><tbody><tr><td><code>Bash(run_in_background=true)</code></td><td>派后台任务，立即返回 task_id</td></tr><tr><td><code>TaskOutput(task_id, block=true)</code></td><td>阻塞等结果（等价于同步）</td></tr><tr><td><code>TaskOutput(task_id, block=false)</code></td><td>立即返回当前状态（异步 peek）</td></tr><tr><td><code>TaskStop(task_id)</code></td><td>主动取消跑飞的后台任务</td></tr></tbody></table><blockquote><p><code>&lt;task_notification&gt;</code> <strong>不是特殊的 IPC 中断机制</strong>，而是 harness 在下一次 API 请求时拼在 user 消息里的一段文本。所以 LLM 是”在下一次思考中读到”通知，不是”被中断”—— 这就是它保持主导权的技术根源。</p></blockquote><p>github 开源地址 <a href="https://github.com/diaozxin007/jooj">https://github.com/diaozxin007/jooj</a></p>]]>
    </content>
    <id>https://xilidou.com/2026/07/08/harness2/</id>
    <link href="https://xilidou.com/2026/07/08/harness2/"/>
    <published>2026-07-08T21:12:17.000Z</published>
    <summary>AI in Harness 系列第二篇——LLM 长周期任务如何跑得稳、跑得远：错误恢复（Error Recovery）如何区分可重试/致命错误并降级、Task System 如何做任务规划与依赖调度、Background Task 如何在主循环之外并发执行长耗时子任务并回写结果。以 Java Harness 框架的实现代码作为落地参考。</summary>
    <title>AI in Harness（二）——错误恢复、任务规划与后台执行：Error Recovery、Task System 与 Background Task</title>
    <updated>2026-08-08T02:04:49.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="AI" scheme="https://xilidou.com/categories/AI/"/>
    <category term="Agent" scheme="https://xilidou.com/tags/Agent/"/>
    <category term="AI" scheme="https://xilidou.com/tags/AI/"/>
    <category term="Claude Code" scheme="https://xilidou.com/tags/Claude-Code/"/>
    <category term="Tools" scheme="https://xilidou.com/tags/Tools/"/>
    <category term="Harness" scheme="https://xilidou.com/tags/Harness/"/>
    <category term="LLM" scheme="https://xilidou.com/tags/LLM/"/>
    <category term="Skill" scheme="https://xilidou.com/tags/Skill/"/>
    <category term="Memory" scheme="https://xilidou.com/tags/Memory/"/>
    <category term="Loop Engineering" scheme="https://xilidou.com/tags/Loop-Engineering/"/>
    <category term="提示词工程" scheme="https://xilidou.com/tags/%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%B7%A5%E7%A8%8B/"/>
    <content>
      <![CDATA[<h1 id="AI-in-Harness（一）"><a href="#AI-in-Harness（一）" class="headerlink" title="AI in Harness（一）"></a>AI in Harness（一）</h1><p>最近系统性地研究了一下 Claude Code 的实现，作为 Java 程序员写了一个开源的 Loop-based Agent Harness 框架。从工程的角度，以 Java 语言构建一个 Harness 的学习框架，理解 Harness 实现的技术细节。</p><h2 id="从”写提示词”到”设计循环”再到”驾驭循环”"><a href="#从”写提示词”到”设计循环”再到”驾驭循环”" class="headerlink" title="从”写提示词”到”设计循环”再到”驾驭循环”"></a>从”写提示词”到”设计循环”再到”驾驭循环”</h2><p>在使用 AI 的初期，你是否遇到过这样一个场景。写一个提示词，AI 回复一次，然后人工评估 AI 的结果，修改提示词，重新再向 AI 提问。这个场景中人是 Loop 中的一个环节，人的效率决定了 Loop 的效率。</p><p>Loop Engineering 被提出。Loop 的核心是人设定目标，系统自动完成”执行-观察-评估-修正”的闭环，直到任务完成。人从”指挥者”变成了”系统设计者”。但是 Loop 带来了新的问题，Loop Engineering 解决了 “AI 能不能自己跑”，但没解决 “AI 能不能跑得久、跑得稳、跑得安全、跑得起”。</p><p>为了解决以上的问题，Harness Engineering 被提出。在 Loop Engineering 基础上增加了上下文工程、工具设计、权限管控、记忆管理、上下文压缩等能力。</p><p><img data-src="/images/prompt-loop-harness.png" alt="loop"></p> <span id="more"></span><h2 id="Harness-的核心逻辑实现"><a href="#Harness-的核心逻辑实现" class="headerlink" title="Harness 的核心逻辑实现"></a>Harness 的核心逻辑实现</h2><h3 id="首先需要一个-Loop"><a href="#首先需要一个-Loop" class="headerlink" title="首先需要一个 Loop"></a>首先需要一个 Loop</h3><p>Loop 的核心是：用户输入 → LLM 基于已有的 Tool 工具，规划任务，调用 Tools → 获取 Tools 的执行结果 → 判断 Loop 的退出条件。</p><p>几个核心点：</p><ul><li>Tools 的管理：<ul><li>Tools 的名称、功能描述、参数 Schema（JSON Schema）、具体的执行代码。参数 Schema 是 LLM 生成正确调用参数的依据，缺一不可。</li><li>需要将支持的 Tools 告诉 LLM。<ul><li>需要一个 tools 的管理模块。</li></ul></li><li>LLM 会识别用户的意图，规划调用的 Tool。</li></ul></li><li>MessageList 的管理<ul><li>MessageList 中通常包含三类消息：user message（用户输入）、assistant message（LLM 输出，可能包含 tool_use）、tool_result message（工具执行结果）。</li><li>每次执行完 tools 后，需要将 tool_result message 加入 MessageList，作为下一轮 LLM 推理的上下文。</li></ul></li><li>Loop 的退出<ul><li>正常退出：LLM 返回的响应中不再包含 tool_use 请求（stop_reason &#x3D; “end_turn”），只要 LLM 还想调用工具，循环就继续。</li><li>兜底机制：max_iterations 上限（防死循环）、token budget 上限（防成本失控）、用户中断信号（Ctrl+C 或前端取消）。</li></ul></li></ul><h3 id="Tools-管理模块-Agent-与现实的连接"><a href="#Tools-管理模块-Agent-与现实的连接" class="headerlink" title="Tools 管理模块- Agent 与现实的连接"></a>Tools 管理模块- Agent 与现实的连接</h3><p>Tools 管理模块的主要功能：</p><ul><li>工具的定义</li><li>处理工具的注册和分发</li></ul><p>工具的 Schema 校验、入参校验、PreToolUse hooks、权限校验，之后逐渐实现。后续还可以关注支持并发执行、结果持久化。</p><h3 id="权限管控-把-Tools-管起来"><a href="#权限管控-把-Tools-管起来" class="headerlink" title="权限管控-把 Tools 管起来"></a>权限管控-把 Tools 管起来</h3><p>LLM 是不可控的，如果模型收到调用 <code>rm -rf /</code> 的命令，无脑执行那是非常危险的。需要在工具执行之前进行一次检查。</p><p>可以简单将权限进行归类：</p><ul><li><strong>可以放过的</strong>：<code>wc -l</code>、<code>grep</code> 等只读操作</li><li><strong>需要询问的</strong>：<code>git commit</code>、<code>git push</code> 等</li><li><strong>绝对禁止的</strong>：<code>rm -rf /</code> 这种删除磁盘的</li><li><strong>暂时不表态的</strong>：交给下游判断</li></ul><h3 id="Hooks-增强-Tools"><a href="#Hooks-增强-Tools" class="headerlink" title="Hooks - 增强 Tools"></a>Hooks - 增强 Tools</h3><p>如果你需要在每次调用 tools 时都做一些固定的增强，比如记录工具的调用，比如上文的每次工具调用都需要进行权限检查，作为 Java 程序员很自然地想到 Spring 中的 AOP（切面的实现）。在 Harness 中，就以 Hook 的形式实现 —— 对 Agent 的增强，且不需要对 Loop 进行改动。</p><h3 id="TodoWrite-为-Agent-增加计划能力"><a href="#TodoWrite-为-Agent-增加计划能力" class="headerlink" title="TodoWrite - 为 Agent 增加计划能力"></a>TodoWrite - 为 Agent 增加计划能力</h3><p>循环可以完整地执行了，但是对一个复杂任务直接动手效果并不好，可以让模型先列计划，再分步执行。</p><p>上下文越长，模型的注意力会被稀释。一个 10 步的工作，模型做了 1-3 步之后，就开始即兴发挥了。</p><p>Agent 接收到任务，先调用 <code>todo_write</code> 生成多个子任务并标记为 <code>pending</code>。执行任务时标记为 <code>in_progress</code>，任务执行完标记为 <code>completed</code>，查看下一个 <code>pending</code> 任务。</p><p><strong>TodoWrite 提升了 Agent 的规划能力。</strong></p><h3 id="SubAgent-需要一个干净的的上下文"><a href="#SubAgent-需要一个干净的的上下文" class="headerlink" title="SubAgent - 需要一个干净的的上下文"></a>SubAgent - 需要一个干净的的上下文</h3><p>Agent 在处理一个相对复杂的任务时，经过多轮对话，大部分都是中间过程，和最终的目标无关。这些中间过程占用上下文，容易使 Agent 注意力不集中，逐渐忘记最终目标。</p><p>所以换个角度思考，完成一个相对独立的工作时候，可以开一个独立的 Agent 实例（隔离的 MessageList &#x2F; context），让它专心地做一件事情。</p><p>TodoWrite 用于规划；当某个子任务足够独立、上下文可以隔离时，主 Agent 可以选择 spawn 一个 SubAgent 去执行，并只回传最终结果。</p><blockquote><p><strong>核心洞察</strong>：LLM 的上下文是宝贵的。过长的上下文会导致 LLM 注意力涣散，所以之后的很多工作，都在想办法解决上下文过长的问题。</p></blockquote><h3 id="Skill-管理-为-LLM-增加-Tools-使用说明书"><a href="#Skill-管理-为-LLM-增加-Tools-使用说明书" class="headerlink" title="Skill 管理 - 为 LLM 增加 Tools 使用说明书"></a>Skill 管理 - 为 LLM 增加 Tools 使用说明书</h3><p>在 Tool 之外，Harness 还需要另一层能力抽象——Skill。</p><p><strong>Skill vs Tool 的核心区别：</strong></p><table><thead><tr><th>维度</th><th>Tool</th><th>Skill</th></tr></thead><tbody><tr><td>本质</td><td>一个可执行函数</td><td>一份方法论 &#x2F; 操作手册</td></tr><tr><td>调用方式</td><td>LLM 直接调用（tool_use）</td><td>LLM 先读 markdown → 再按指令行动</td></tr><tr><td>粒度</td><td>原子操作</td><td>组合流程</td></tr><tr><td>上下文占用</td><td>常驻 system prompt</td><td>按需加载（lazy）</td></tr><tr><td>组成</td><td>name + description + schema + code</td><td>markdown 指令（内部可组合调用多个 tool）</td></tr><tr><td>谁定义</td><td>引擎 &#x2F; 框架开发者</td><td>用户 &#x2F; 领域专家（纯 markdown 即可扩展）</td></tr></tbody></table><p>用 Java 视角类比：Tool 是 <code>@Service</code> 里的一个 method（如 <code>bash()</code>、<code>read()</code>），Skill 是一个 Service 类或一段 SOP 文档，内部会编排多个 method；Skill 的加载对应 Spring 的 <code>@Lazy</code> bean，首次被用到时才加载。</p><p><strong>为什么需要 Skill——回到”上下文是宝贵的”这个核心洞察：</strong></p><p>如果所有能力都作为 Tool 常驻 system prompt，100 个 tool 的定义会消耗几万 token，且大部分 tool 在当前任务里根本用不到。Context 被”能力目录”塞满，真正做事的空间就变少了。</p><p><strong>Skill loading 的机制：</strong></p><ol><li>启动时只加载 skill 的 name + 一句话描述（占用极小）</li><li>用户发起请求，LLM 判断需要哪个 skill</li><li>Harness 动态读取对应 skill 的 markdown 全文，注入 context</li><li>LLM 按 skill 里的步骤，调用底层 tool 完成任务</li></ol><p><strong>Skill 与 Tool 的层次关系：</strong></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"> Skill    ← 高层：方法论 / SOP（按需加载的 markdown）</span><br><span class="line">   │ 组合调用</span><br><span class="line">   ▼</span><br><span class="line"> Tool     ← 底层：原子执行单元（常驻注册的 function）</span><br><span class="line">   │ 落到</span><br><span class="line">   ▼</span><br><span class="line">真实操作（fs / shell / net）</span><br></pre></td></tr></table></figure><p>一个不太准确但直观的比喻：Tool 是厨房里的<strong>刀具、锅、火</strong>；Skill 是一份<strong>菜谱</strong>（教你何时用哪把刀、什么火候）；LLM 是<strong>厨师</strong>；Harness 是<strong>整个厨房</strong>（把菜谱柜、刀架、灶台组织起来的空间）。菜谱本身不切菜，但没有菜谱厨师就会乱来。</p><p>Skill 的引入，本质上是 Harness Engineering 中的<strong>上下文分层加载机制</strong>——把”能力目录”和”能力详情”分开，按需展开，从而在有限的 context 里承载更多的能力。</p><h3 id="上下文压缩-解决-LLM-注意力问题"><a href="#上下文压缩-解决-LLM-注意力问题" class="headerlink" title="上下文压缩 - 解决 LLM 注意力问题"></a>上下文压缩 - 解决 LLM 注意力问题</h3><p>上文多次提到，LLM 的上下文是宝贵的。随着 Loop 的轮次增加，MessageList 里面的信息越来越多，需要有一套机制处理上下文过长的问题，”上下文压缩”就被提了出来。</p><p><strong>上下文压缩手段：</strong></p><ul><li><strong>L1</strong>：单条信息太长的，落盘，留下索引，大模型需要的时候再查询。防止过长的结果撑爆上下文。</li><li><strong>L2</strong>：tools result 的历史结果使用占位符代替（历史的结果模型可能不关心）。过久的结果影响模型注意力，需要时再重新调用。</li><li><strong>L3</strong>：多轮对话太多的，去除中间。留下开始的对话（可能包含用户的指令）和最后的对话（模型需要知道最近在干啥）。</li><li><strong>L4</strong>：使用 LLM 总结，提炼上下文内容。</li></ul><p><strong>核心思路：</strong></p><ol><li>先基于规则，使用便宜的方式</li><li>压缩可回溯，需要时再查询</li><li>最后才用 LLM 总结</li></ol><h3 id="Memory-让-Agent-拥有记忆"><a href="#Memory-让-Agent-拥有记忆" class="headerlink" title="Memory - 让 Agent 拥有记忆"></a>Memory - 让 Agent 拥有记忆</h3><p>在 Loop 中随着信息不断添加与压缩，我们还需要设计一个机制，保存需要记住的东西。</p><blockquote><p><strong>Memory &#x3D; 把对话里值得永久记住的事实（fact）落到磁盘文件，跨 Compact 不丢、跨 session 仍可用。</strong></p></blockquote><p><strong>Memory 的核心模块：</strong></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">Storage        ── 纯 IO，提供增删改查 API（不智能）</span><br><span class="line">Selection      ── LLM side-query 挑相关 memory + 关键词回退</span><br><span class="line">Extraction     ── LLM 从对话提取 fact 写入 Storage（无回退，失败就跳过）</span><br><span class="line">Consolidation  ── LLM 去重合并，解决 Memory 过大问题</span><br></pre></td></tr></table></figure><p><strong>Storage 是基石</strong></p><p>Storage 是 IO 存储的抽象，提供 memory 的 CRUD 方法。目前使用文件的形式进行存储，后期可以使用数据库、RAG 等组件。</p><p><strong>记忆的生成</strong></p><p>每轮对话完成，通过 Extraction 组件调用 LLM 从 MessageList 里面抽取 fact，调用 Storage 的写入方法。Extraction 属于”锦上添花”型任务，无回退机制，失败即跳过，不阻塞主 Loop。</p><p><strong>记忆的整理</strong></p><p>Consolidation 会调用 LLM 对记忆进行整理和去重。由 memory 数量阈值触发（而非每轮同步执行），避免阻塞主 Loop。</p><p><strong>记忆的使用</strong></p><p>memory 的 catalog（索引 &#x2F; 摘要）常驻 system prompt，全文按需加载。LLM 通过 side-query 从 catalog 中挑选相关记忆，再查询记忆全文，注入 Loop。当 side-query 未命中时，用关键词匹配作为回退，保证召回率。</p><blockquote><p><strong>与 Skill 的呼应</strong>：Memory 的加载机制与 Skill 同构 —— catalog 常驻 system，全文按需注入。都是 Harness Engineering 中”分层加载”策略在不同数据形态上的应用：Skill 分层加载的是”能力”，Memory 分层加载的是”事实”。</p></blockquote><h3 id="System-prompt-运行时组装-不硬编码"><a href="#System-prompt-运行时组装-不硬编码" class="headerlink" title="System prompt - 运行时组装, 不硬编码"></a>System prompt - 运行时组装, 不硬编码</h3><p>前面几章讲了 Loop、Tool、Skill、Memory、上下文压缩，但都绕开了一个问题：<strong>这些能力是怎么”告诉” LLM 的？</strong> 答案就是 System prompt。</p><p><strong>从”写死的模板”到”运行时配置”</strong></p><p>初学者常把 System prompt 当成一个写死的字符串，随代码一起提交。但当 Harness 长大后，会遇到三个问题：</p><ol><li><strong>换项目要重写整个 prompt</strong>，不知道哪些该改、哪些该留（身份、工具、规范混在一起，牵一发动全身）</li><li><strong>修改一处可能影响全局</strong>，加一段工具描述可能跟前面的指令冲突</li><li><strong>每次请求都带全部内容</strong>，即使当前对话用不到某些段落也浪费 token，还打不中 prompt cache</li></ol><blockquote><p><strong>核心洞察</strong>：System prompt 不是”文档”，而是<strong>运行时根据当前状态组装的配置</strong>。哪些工具启用、哪些 skill 可见、哪些记忆相关、哪些内容必须保持稳定 —— 都应该在运行时决定。</p></blockquote><p><strong>System prompt 的组成结构</strong></p><p>一个合理的 System prompt 通常包含以下几层（从稳定到易变）：</p><table><thead><tr><th>层</th><th>内容</th><th>变化频率</th><th>是否命中 cache</th></tr></thead><tbody><tr><td>身份</td><td>Agent 的角色定位、行为准则</td><td>几乎不变</td><td>✓ 强 cache</td></tr><tr><td>Tools</td><td>当前启用的工具 schema</td><td>按会话变化</td><td>✓ 会话内 cache</td></tr><tr><td>Skill catalog</td><td>可用 skill 的索引（name + 一句话描述）</td><td>按会话变化</td><td>✓ 会话内 cache</td></tr><tr><td>Memory catalog</td><td>长期记忆的索引 &#x2F; 摘要</td><td>按会话变化</td><td>✓ 会话内 cache</td></tr><tr><td>Workspace</td><td>当前工作目录、项目上下文</td><td>按任务变化</td><td>✗ 每次变</td></tr></tbody></table><p><strong>排列顺序至关重要 —— 越稳定的内容放越前面。</strong> Claude、GPT 等主流 LLM 都支持 <strong>prompt caching</strong>：相同的前缀可以复用，只对增量部分收费。把 workspace 放到最后，前面几层就能最大化 cache 命中率，直接降低成本和延迟。</p><p>Java 程序员视角：这类似 Spring 的 bean 生命周期 —— 身份是 singleton（启动时构造一次），Tools &#x2F; Skill catalog &#x2F; Memory catalog 是 session-scoped（按会话构造），Workspace 是 request-scoped（每次请求构造）。层次越稳定，复用越充分。</p><p><strong>与前面章节的呼应</strong></p><p>System prompt 是前几章”分层加载”机制的<strong>汇聚点</strong>：</p><ul><li><strong>Tools schema</strong> —— 对应 Tools 管理章节的”需要将支持的 Tools 告诉 LLM”</li><li><strong>Skill catalog</strong> —— 对应 Skill 章节的”启动时只加载 name + 一句话描述”</li><li><strong>Memory catalog</strong> —— 对应 Memory 章节的”catalog 常驻 system prompt，全文按需加载”</li></ul><p>所有 catalog（能力目录）在 System prompt 汇总，而”详情”（skill 全文、memory 全文、tool 执行结果）通过按需加载注入 MessageList。</p><p><strong>一句话总结</strong>：System prompt 定义”你是谁、你能做什么、你在哪里工作”，MessageList 记录”你正在做什么”。前者相对稳定用于命中 cache，后者动态生长承载具体任务。</p><p>未完待续。</p>]]>
    </content>
    <id>https://xilidou.com/2026/07/07/harness1/</id>
    <link href="https://xilidou.com/2026/07/07/harness1/"/>
    <published>2026-07-07T21:12:17.000Z</published>
    <summary>AI in Harness 系列第一篇——从 Prompt Engineering 到 Loop Engineering 再到 Agent Harness 的演进：为什么&quot;能跑&quot;的 Loop 还不够，Harness 需要在此之上补齐 Tools、Skill、Memory 三块能力，以及 CLAUDE.md / 系统提示词 / 用户消息 / 工具结果的上下文分层加载策略。以 Java 视角剖析 Claude Code 实现，配可运行的开源 Java Harness 框架。</summary>
    <title>AI in Harness（一）——从 Loop 到 Agent Harness：Tools、Skill、Memory 与上下文分层加载</title>
    <updated>2026-08-08T02:04:49.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="SRE" scheme="https://xilidou.com/categories/SRE/"/>
    <category term="SRE" scheme="https://xilidou.com/tags/SRE/"/>
    <content>
      <![CDATA[<h1 id="负载均衡"><a href="#负载均衡" class="headerlink" title="负载均衡"></a>负载均衡</h1><h2 id="前端"><a href="#前端" class="headerlink" title="前端"></a>前端</h2><p>使用 DNS 进行负载均衡。在 DNS 回复中提供多个 A 记录或者 AAAA 记录。<br>虽然 DNS 看起来简单，但是存在不少问题。</p><ol><li>DNS 对客户端行为的约束很弱：记录是随机选择的。</li><li>客户端无法识别“最近”的地址</li><li>权威服务器不能主动清楚某个解析器的缓存，DNS 记录需要保持一个相对低的失效值（TTL）。</li></ol><span id="more"></span><p>需要在 DNS 负载后面增加一层虚拟 IP 地址，我们常说的 VIP。</p><p>使用 VIP 进行负载均衡<br>虚拟 IP（VIP） 不是绑定在某一个特定的网络接口上的。很多设备共享。外界看 VIP 是一个独立的普通 IP。VIP 是网络负载均衡器。负载均衡器接收网络数据包，转发给 背后的某个服务器。</p><p>负载的方案:</p><ul><li>对于无状态的服务，理论上说永远优先负载最小的后端服务</li><li>对于有转态的服务<ul><li>某个连接标识取模</li><li>一致性哈希</li></ul></li></ul><h2 id="后端"><a href="#后端" class="headerlink" title="后端"></a>后端</h2><h3 id="理想情况"><a href="#理想情况" class="headerlink" title="理想情况"></a>理想情况</h3><p>某个服务的负载会完全均匀的分发给所有的后端服务。任何时间点，最忙和最不忙的任务消耗相同数量的 CPU。</p><h3 id="识别异常任务"><a href="#识别异常任务" class="headerlink" title="识别异常任务"></a>识别异常任务</h3><ul><li>限流<ul><li>客户端限流</li><li>某个后端的活跃请求达到一定数量，客户端将后端标记为异常转态，不再发送请求。</li><li>正常情况下，后端请求很快完成，限流几乎不会触发</li><li>后端过载了，请求响应慢，客户端就会自动避开这个后端</li><li>缺点就是，不精确。后端很可能达到限额之前就过载了。反之亦然。</li></ul></li><li>坡脚鸭任务<ul><li>客户端视角来看，后端任务有以下几个状态<ul><li>健康</li><li>拒绝连接</li><li>坡脚鸭状态<ul><li>后端服务正常，也能服务请求。但是明确要求客户端停止发送请求。</li><li>某个请求进入坡脚鸭状态，需要广播给客户端</li><li>处于停止过程中的服务不会给正在处理的请求返回错误</li><li>可以实现优雅的下线服务</li></ul></li></ul></li></ul></li></ul><h3 id="利用划分子集限制连接池大小"><a href="#利用划分子集限制连接池大小" class="headerlink" title="利用划分子集限制连接池大小"></a>利用划分子集限制连接池大小</h3><p>子集划分：限制某个客户端任务需要连接的后端数量。</p><p>Google 的 RPC 框架对于每个客户端都会维持一个长连接。如果一个集群的规模过大，客户端就要维护很多长连接。</p><h4 id="子集选择算法"><a href="#子集选择算法" class="headerlink" title="子集选择算法"></a>子集选择算法</h4><ul><li>随机选择</li><li>确定性算法</li></ul><h3 id="负载均衡策略"><a href="#负载均衡策略" class="headerlink" title="负载均衡策略"></a>负载均衡策略</h3><h4 id="简单轮询"><a href="#简单轮询" class="headerlink" title="简单轮询"></a>简单轮询</h4><p>造成效果差的因素如下：</p><ol><li>子集过小</li><li>请求处理的成本不同</li><li>物理服务器的差异</li><li>无法预支的性能因素<ol><li>怀邻居（物理服务器上的其他进程）</li><li>任务重启</li></ol></li></ol><h4 id="最闲轮询策略"><a href="#最闲轮询策略" class="headerlink" title="最闲轮询策略"></a>最闲轮询策略</h4><p>客户端追踪子集中每个后端任务的活跃请求数量，在活跃请求最小的任务中进行轮询。</p><p>最危险的坑：如果一个任务不健康，可能 100% 返回错误。取决于错误的类型，错误回复可能延迟非常低。从而给异常任务分配的大量的请求。<br>需要将错误信息计算为活跃请求，剔除异常任务。</p><p>限制：</p><ul><li>活跃的请求数量不一定是后端容量的代表</li><li>每个客户端的活跃请求不包括其他客户端发往同一个后端的请求</li></ul><p>实践中发现，效果很差。</p><h4 id="加权轮询"><a href="#加权轮询" class="headerlink" title="加权轮询"></a>加权轮询</h4><p>每个客户端为子集中的每个后端任务保持一个“能力”值。请求仍以轮询方式分发，客户端按照能力值权重比例调节。</p><p>实践中效果较好。</p>]]>
    </content>
    <id>https://xilidou.com/2022/05/09/sre6/</id>
    <link href="https://xilidou.com/2022/05/09/sre6/"/>
    <published>2022-05-09T22:55:07.000Z</published>
    <summary>
      <![CDATA[<h1 id="负载均衡"><a href="#负载均衡" class="headerlink" title="负载均衡"></a>负载均衡</h1><h2 id="前端"><a href="#前端" class="headerlink" title="前端"></a>前端</h2><p>使用 DNS 进行负载均衡。在 DNS 回复中提供多个 A 记录或者 AAAA 记录。<br>虽然 DNS 看起来简单，但是存在不少问题。</p>
<ol>
<li>DNS 对客户端行为的约束很弱：记录是随机选择的。</li>
<li>客户端无法识别“最近”的地址</li>
<li>权威服务器不能主动清楚某个解析器的缓存，DNS 记录需要保持一个相对低的失效值（TTL）。</li>
</ol>]]>
    </summary>
    <title>《SRE google 运维解密》读书笔记 （六）</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="SRE" scheme="https://xilidou.com/categories/SRE/"/>
    <category term="SRE" scheme="https://xilidou.com/tags/SRE/"/>
    <content>
      <![CDATA[<h1 id="测试可靠性"><a href="#测试可靠性" class="headerlink" title="测试可靠性"></a>测试可靠性</h1><p>预测信息准确的前提：</p><ol><li>系统完全没有改变</li><li>充分描述整个系统的改变</li></ol><p>测试是一个用来证明变更前系统的某些领域相等的手段。</p><span id="more"></span><h2 id="软件测试的类型"><a href="#软件测试的类型" class="headerlink" title="软件测试的类型"></a>软件测试的类型</h2><ul><li>传统测试</li><li>生产测试</li></ul><h3 id="传统测试"><a href="#传统测试" class="headerlink" title="传统测试"></a>传统测试</h3><ul><li>单元测试</li><li>集成测试</li><li>系统测试<ul><li>冒烟测试</li><li>性能测试</li><li>回归测试</li></ul></li></ul><p><strong>每个测试都有成本，通常来说单元测试时间成本低</strong> 如果要将完整的功能架设起来测试，通常需要几个小时。关注测试成本，是软件提升效率的重要因素。</p><h3 id="生产测试"><a href="#生产测试" class="headerlink" title="生产测试"></a>生产测试</h3><p>生产测试和一个已经部署在生产环境的业务系统直接交互，而不是运行在封闭的测试环境。有时候称为黑盒测试</p><ul><li>配置测试</li><li>压力测试</li><li>金丝雀测试<ul><li>一小部分机器先升级，保持一定的孵化期。</li><li>将代码置于比较难以预测的用户流量下</li><li>需要能够快速的回滚</li></ul></li></ul><h2 id="创造一个构建和测试环境"><a href="#创造一个构建和测试环境" class="headerlink" title="创造一个构建和测试环境"></a>创造一个构建和测试环境</h2><ul><li>测试的重点集中在用最小力气得到最大收益的地方<ul><li>划分优先级</li><li>寻找关键函数关键类</li><li>寻找提供给其他团队的 API</li></ul></li><li>发布前，通过冒烟测试</li><li>寻找到的 bug 变成测试用例</li><li>建立良好的测试基础设施<ul><li>追踪代码变更</li><li>每次代码改变就进行构建</li></ul></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><h2 id="针对灾难的测试"><a href="#针对灾难的测试" class="headerlink" title="针对灾难的测试"></a>针对灾难的测试</h2><p>灾难恢复工具被精心设计为离线运行</p><ul><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><h2 id="集成"><a href="#集成" class="headerlink" title="集成"></a>集成</h2><p>使用解释性语言编写配置文件是有风险的。程序的执行时间没有上限，需要加入截止时间检查。</p><p>使用成熟的语法（YAML）和大量测试的解析器。</p><h2 id="生产环境探针"><a href="#生产环境探针" class="headerlink" title="生产环境探针"></a>生产环境探针</h2><p>测试机制是对确定的数据检验系统行为是否可以接受。<br>监控机制择时在未知数据输入下系统行为是否可以接受。</p><p>已知的正确请求应该成功，已知的错误请求应该失败。重放已知请求观察系统是否正常。</p><p>（感觉应该是书翻译的问题所谓的探针应该是 mock 服务。mock 服务部署在生产环境 。在确定的入参下，有确定的返回值。调用方可以使用这个探针进行测试）</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>测试是工程师提高可靠性投入回报比较高的手段。</p>]]>
    </content>
    <id>https://xilidou.com/2022/05/04/sre5/</id>
    <link href="https://xilidou.com/2022/05/04/sre5/"/>
    <published>2022-05-04T10:55:11.000Z</published>
    <summary>
      <![CDATA[<h1 id="测试可靠性"><a href="#测试可靠性" class="headerlink" title="测试可靠性"></a>测试可靠性</h1><p>预测信息准确的前提：</p>
<ol>
<li>系统完全没有改变</li>
<li>充分描述整个系统的改变</li>
</ol>
<p>测试是一个用来证明变更前系统的某些领域相等的手段。</p>]]>
    </summary>
    <title>《SRE google 运维解密》读书笔记 （五）</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="SRE" scheme="https://xilidou.com/categories/SRE/"/>
    <category term="SRE" scheme="https://xilidou.com/tags/SRE/"/>
    <content>
      <![CDATA[<h1 id="事后总结：从失败中学习"><a href="#事后总结：从失败中学习" class="headerlink" title="事后总结：从失败中学习"></a>事后总结：从失败中学习</h1><h2 id="哲学"><a href="#哲学" class="headerlink" title="哲学"></a>哲学</h2><p>保证事故能够被记录下来，理清所有根源问题。确保实施有效的措施是的未来重现的几率和影响得以降低，甚至避免。</p><p>书写事后总结不是一种惩罚，而是整个公司的一次学习机会。</p><p>需要书写的标准：</p><ul><li>用户可见的宕机或者服务质量下降到一定标准</li><li>任何形式的数据丢失</li><li>on-call 工程师需要人工介入</li><li>问题解决耗时超过一定限制</li><li>监控问题</li></ul><span id="more"></span><p>事后总结“对事不对人”。必须关注如何定位造成这次事件的根本问题。而不是指责某个人或者某个团队的错误或者不恰当。</p><p>事后总结系统性，逻辑性的讨论为什么会在事故过程中获得错误的的信息，才能更好的建立预防措施，防止问题再现。</p><p><strong>最佳实践：避免指责，提供建设性意见</strong></p><h2 id="协作和知识共享"><a href="#协作和知识共享" class="headerlink" title="协作和知识共享"></a>协作和知识共享</h2><ul><li>实时协作</li><li>开放的评论</li><li>邮件通知</li></ul><p>包含内容：</p><ul><li>关键的灾难数据是否收集保存起来了</li><li>本次事故的影响评估是否完整</li><li>造成事故的根源问题是否足够深入</li><li>文档记录的任务优先级是否合理，是否及时解决了根源问题</li><li>事故处理过程是否共享给了相关部门</li></ul><p><strong>最佳实践，所有的事后总结都要评审</strong></p><h2 id="建立事后总结文化"><a href="#建立事后总结文化" class="headerlink" title="建立事后总结文化"></a>建立事后总结文化</h2><ul><li>本月最佳总结</li><li>事后总结小组</li><li>事后总结阅读俱乐部</li><li>命运之轮<ul><li>可以对已经发生的事故进行演练</li></ul></li></ul><p><strong>最佳实践：公开奖励做正确事的人</strong><br><strong>最佳实践：收集关于事后总结有效性的反馈</strong></p><h1 id="跟踪故障"><a href="#跟踪故障" class="headerlink" title="跟踪故障"></a>跟踪故障</h1><ul><li>聚合</li><li>加标签</li><li>分析<ul><li>报告和公告</li></ul></li></ul>]]>
    </content>
    <id>https://xilidou.com/2022/05/04/srr4/</id>
    <link href="https://xilidou.com/2022/05/04/srr4/"/>
    <published>2022-05-04T10:54:07.000Z</published>
    <summary>
      <![CDATA[<h1 id="事后总结：从失败中学习"><a href="#事后总结：从失败中学习" class="headerlink" title="事后总结：从失败中学习"></a>事后总结：从失败中学习</h1><h2 id="哲学"><a href="#哲学" class="headerlink" title="哲学"></a>哲学</h2><p>保证事故能够被记录下来，理清所有根源问题。确保实施有效的措施是的未来重现的几率和影响得以降低，甚至避免。</p>
<p>书写事后总结不是一种惩罚，而是整个公司的一次学习机会。</p>
<p>需要书写的标准：</p>
<ul>
<li>用户可见的宕机或者服务质量下降到一定标准</li>
<li>任何形式的数据丢失</li>
<li>on-call 工程师需要人工介入</li>
<li>问题解决耗时超过一定限制</li>
<li>监控问题</li>
</ul>]]>
    </summary>
    <title>《SRE google 运维解密》读书笔记 （四）</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="SRE" scheme="https://xilidou.com/categories/SRE/"/>
    <category term="SRE" scheme="https://xilidou.com/tags/SRE/"/>
    <content>
      <![CDATA[<h1 id="应急事件响应"><a href="#应急事件响应" class="headerlink" title="应急事件响应"></a>应急事件响应</h1><h2 id="测试导致的事故"><a href="#测试导致的事故" class="headerlink" title="测试导致的事故"></a>测试导致的事故</h2><p>SRE 故意破坏系统，利用这些测试发现系统的薄弱地方。</p><p>在某次测试中发现了额外的系统依赖。</p><h3 id="响应"><a href="#响应" class="headerlink" title="响应"></a>响应</h3><ul><li>终止测试</li><li>用以前 <strong>测试过的方法</strong> 回滚了数据</li><li>找到开发者修复了相关问题</li><li>制定了<strong>周期性测试机制</strong>来保证问题不重现</li></ul><h3 id="事后总结"><a href="#事后总结" class="headerlink" title="事后总结"></a>事后总结</h3><p>好的方面：<br>事先沟通，有足够信息推测是测试造成的问题。<br>快速恢复了系统。<br>遗留一个代办，彻底修复问题。制定了周期性的测试流程。</p><span id="more"></span><p>不好的方面：<br>虽然评估了，但是还是发生了问题<br>没有正确遵守响应流程<br><strong>没有测试“回滚机制”</strong>，发生问题后回滚机制失效。</p><h2 id="变更导致的事故"><a href="#变更导致的事故" class="headerlink" title="变更导致的事故"></a>变更导致的事故</h2><p>某个周五，某个配置文件推送到所有的服务器。触发了 bug。</p><h3 id="响应-1"><a href="#响应-1" class="headerlink" title="响应"></a>响应</h3><ul><li>各个系统开始报警</li><li>on-call 工程师前往灾难安全屋。（有google 生产环境专线）</li><li>5 分钟以后发布这个配置的工程师发现问题，回滚发布</li><li>某些服务由于这次发布，触发了别的 bug 一小时后才回复</li></ul><h3 id="事后总结-1"><a href="#事后总结-1" class="headerlink" title="事后总结"></a>事后总结</h3><p>好的方面：</p><ul><li>监控系统及时报告问题</li><li>问题被检测后，应急流程处理得当。<strong>SRE 要保持一些可靠的，低成本的访问系统</strong></li><li>Google 还有命令行工具和其他访问方式确保能够在其他条件无法访问的时候进行更新和变更回滚。且要频繁测试，让工程师熟悉他们。</li><li>限速机制，限制了错误的扩散。抑制了崩溃的速度。</li></ul><p>从中学到的：</p><ul><li>变更经过了完整的部署测试没有触发 bug，评估并不危险，但是在全球部署的时候触发了 bug</li><li>不管风险看起来有多小，都需要严格测试</li><li>监控系统在灾难中，发出了很多报警，干扰了 on-call 工程师的工作</li></ul><h2 id="流程导致的严重事故"><a href="#流程导致的严重事故" class="headerlink" title="流程导致的严重事故"></a>流程导致的严重事故</h2><p>常规自动化测试，对一个集群发送了两次下线请求，触发 bug将全球所有数据中心的的所有机器加入到了磁盘销毁的队列</p><h3 id="响应-2"><a href="#响应-2" class="headerlink" title="响应"></a>响应</h3><ul><li>on-call工程师收到报警，将流量导入其他地区</li><li>停止了自动化工具</li><li>用户导入其他地方，响应时间变长，但是还是可以正常使用</li><li>恢复数据。</li></ul><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>好的地方</p><ul><li>反向代理可以迅速的切换用户流量。</li><li>自动化下线虽然一起下线了监控系统，on-call 工程师快速恢复系统。</li><li>工程师训练有素，多亏了应急事故管理系统和平时的训练。</li></ul><p>从中学到的：</p><ul><li>事故的根源在于自动化系统对发出的指令缺乏合适的合理性校验。</li></ul><h2 id="所有的问题都有解决方案"><a href="#所有的问题都有解决方案" class="headerlink" title="所有的问题都有解决方案"></a>所有的问题都有解决方案</h2><p>系统不但一定会出问题，而且会以没有人能够想到的方式出现问题。但是所有的问题都有对应的解决方案。如果想不到解决问题的方法，那就再更大的范围里面寻求帮助。很多时候触发事故的人对事故最了解。</p><p>一旦紧急事故过去之后，一定要留出时间书写事后报告。</p><h2 id="向过去学习，而不是重复它"><a href="#向过去学习，而不是重复它" class="headerlink" title="向过去学习，而不是重复它"></a>向过去学习，而不是重复它</h2><ul><li>为事故保留记录</li><li>提出那些大的，甚至不可能的问题：加入…</li><li>鼓励主动测</li></ul><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>遇到事故，不要惊慌失措，必要时引入其他人帮助，事后需要记录。把系统改善为能够更好的处理同类故障。</p><h1 id="紧急事故管理"><a href="#紧急事故管理" class="headerlink" title="紧急事故管理"></a>紧急事故管理</h1><h2 id="无流程管理事故的剖析"><a href="#无流程管理事故的剖析" class="headerlink" title="无流程管理事故的剖析"></a>无流程管理事故的剖析</h2><ul><li>过于关注技术问题</li><li>沟通不畅</li><li>不请自来</li></ul><h2 id="事故管理的要素"><a href="#事故管理的要素" class="headerlink" title="事故管理的要素"></a>事故管理的要素</h2><h3 id="嵌套式责任分离"><a href="#嵌套式责任分离" class="headerlink" title="嵌套式责任分离"></a>嵌套式责任分离</h3><p>事故处理中，让每个人清楚自己的职责。如果一个人处理的事务过多，就应该申请更多人力资源。把一部分任务交给别人。<br>事故中的角色：</p><ul><li>事故总控<br>负责组建事故处理团队，负责协调工作</li><li>事务处理团队<br>具体处理事故的团队，唯一能够对系统进行修改的团队</li><li>发言人<br>向事务处理团队和关心事故的人发送周期性的通知。维护事故文档。</li><li>规划负责人<br>为团队提供支持。如填写事故报告，定晚餐，安排交接。</li></ul><h3 id="控制中心"><a href="#控制中心" class="headerlink" title="控制中心"></a>控制中心</h3><p>很多时候可以设立一个”作战室“。</p><p>IRC 处理事故很有帮助。 IRC 很可靠，记录下所有沟通记录。</p><h3 id="实时的事故状态文档"><a href="#实时的事故状态文档" class="headerlink" title="实时的事故状态文档"></a>实时的事故状态文档</h3><p>事故总控人最重要的职责就是维护事故的实时文档。最好可以多人同时编辑。</p><h3 id="明确公开的职责交接"><a href="#明确公开的职责交接" class="headerlink" title="明确公开的职责交接"></a>明确公开的职责交接</h3><p>事故总控人的职责能够明确，公开的进行交接很重要。交接结果要宣布给正在处理事故的其他人。</p><h2 id="什么时候对外宣布事故"><a href="#什么时候对外宣布事故" class="headerlink" title="什么时候对外宣布事故"></a>什么时候对外宣布事故</h2><ul><li>是否需要引入第二个团队来帮助处理问题</li><li>是否时候正在影响最终客户</li><li>集中分析一个小时后，这个问题是不是依然没有得到解决</li></ul><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><ul><li>划分优先级</li><li>事前准备</li><li>信任</li><li>反思</li><li>考虑替代方案</li><li>练习</li><li>换位思考</li></ul>]]>
    </content>
    <id>https://xilidou.com/2022/05/01/sre3/</id>
    <link href="https://xilidou.com/2022/05/01/sre3/"/>
    <published>2022-05-01T22:27:54.000Z</published>
    <summary>
      <![CDATA[<h1 id="应急事件响应"><a href="#应急事件响应" class="headerlink" title="应急事件响应"></a>应急事件响应</h1><h2 id="测试导致的事故"><a href="#测试导致的事故" class="headerlink" title="测试导致的事故"></a>测试导致的事故</h2><p>SRE 故意破坏系统，利用这些测试发现系统的薄弱地方。</p>
<p>在某次测试中发现了额外的系统依赖。</p>
<h3 id="响应"><a href="#响应" class="headerlink" title="响应"></a>响应</h3><ul>
<li>终止测试</li>
<li>用以前 <strong>测试过的方法</strong> 回滚了数据</li>
<li>找到开发者修复了相关问题</li>
<li>制定了<strong>周期性测试机制</strong>来保证问题不重现</li>
</ul>
<h3 id="事后总结"><a href="#事后总结" class="headerlink" title="事后总结"></a>事后总结</h3><p>好的方面：<br>事先沟通，有足够信息推测是测试造成的问题。<br>快速恢复了系统。<br>遗留一个代办，彻底修复问题。制定了周期性的测试流程。</p>]]>
    </summary>
    <title>《SRE google 运维解密》读书笔记 （三）</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="SRE" scheme="https://xilidou.com/categories/SRE/"/>
    <category term="SRE" scheme="https://xilidou.com/tags/SRE/"/>
    <content>
      <![CDATA[<h2 id="有效的故障排查手段"><a href="#有效的故障排查手段" class="headerlink" title="有效的故障排查手段"></a>有效的故障排查手段</h2><h3 id="理论："><a href="#理论：" class="headerlink" title="理论："></a>理论：</h3><p>反复采用假设排除手段的过程：<br>不断提出一个造成系统问题的假设，进而针对这些假设进行测试和排除</p><blockquote><p>常见的陷阱</p><ul><li>关注的错误的系统现象，或者错误地理解了系统现象的含义。</li><li>不能正确的修改系统的配置信息，输入信息或者系统运行环境。</li><li>将问题过早的归结为极为不可能的因素，或者之前曾经发生过的问题</li><li>试图解决与当前问题相关的一些问题，却没有认识到只是巧合。</li></ul></blockquote><span id="more"></span><h3 id="实践"><a href="#实践" class="headerlink" title="实践"></a>实践</h3><h4 id="故障报告"><a href="#故障报告" class="headerlink" title="故障报告"></a>故障报告</h4><p>故障报告不鼓励直接汇报给具体的某个人，这样会导致压力集中在几个问题汇报人熟悉的团队成员。而不是质保人员。<br>需要保证每一个故障报告都有调查的历史和解决方案。</p><h4 id="定位"><a href="#定位" class="headerlink" title="定位"></a>定位</h4><p>大型问题，不要立即开始排查问题，尽快找到问题的根源。</p><p>正确的做法是，尽最大可能使系统回复。（同时尽量保存报错的现场供事后调查复盘）</p><h4 id="检查"><a href="#检查" class="headerlink" title="检查"></a>检查</h4><p>需要检查系统中每个组件的工作状态，以便了解系统是不是在正常工作。</p><p>理想情况下监控可以提供相应指标。</p><p>日志很重要，了解系统某个时间在干啥。</p><blockquote><p>将日志结构化，可以保存更长时间<br>多级记录日志很重要，尤其可以动态调整日志级别<br>在日志系统中支持过滤条件</p></blockquote><h4 id="诊断"><a href="#诊断" class="headerlink" title="诊断"></a>诊断</h4><ul><li>简化和缩略<ul><li>对于大型系统，逐级查询问题过于耗时，尝试使用二分法。</li></ul></li><li>What 、Where 、Why</li><li>最后一次变更<ul><li>变更是引起问题的最大来源</li></ul></li><li>有针对性的诊断</li></ul><h4 id="测试和修复"><a href="#测试和修复" class="headerlink" title="测试和修复"></a>测试和修复</h4><ul><li>理想的测试应该具有互斥性，一个测试可以推翻一组假设</li><li>先测试最可能的问题</li><li>某些测试可能带来误导性的结果</li><li>执行测试可能会带来副作用<blockquote><p>神奇的负面结果<br>所谓负面结果，就是一项试验中不符合预期的结果</p><ul><li>负面结果不应该被忽略</li><li>负面结果需要被记录，供后来人查阅。<ul><li>比如压测不通过的报告</li></ul></li><li>工具和方法可能超越目前的试验，为未来的工作提供帮助</li><li>公布负面结果有利于挺升行业的数据驱动风气</li><li>公布结果<ul><li>负面结果并不是失败</li><li>负面结果并非没有价值</li><li>良好设计的试验是有价值的，而不是有正向结果的试验才有价值</li></ul></li></ul></blockquote></li></ul><h4 id="治愈"><a href="#治愈" class="headerlink" title="治愈"></a>治愈</h4><p>理想情况下，可能把错误原因减少到了一个。<br>下一步复现问题。<br>然后修复问题</p><p>如果一旦解决了某个问题，需要将如何定位问题，如何修复问题，如何防止问题再次发生。进行记录作为事后总结记录。</p><h3 id="使故障排查更简单"><a href="#使故障排查更简单" class="headerlink" title="使故障排查更简单"></a>使故障排查更简单</h3><ul><li>增加系统的可观察性。为每个系统增加白盒监控和结构化日志</li><li>利用成熟的，观察性好的组件接口设计系统</li></ul>]]>
    </content>
    <id>https://xilidou.com/2022/04/17/ser2/</id>
    <link href="https://xilidou.com/2022/04/17/ser2/"/>
    <published>2022-04-17T22:00:24.000Z</published>
    <summary>
      <![CDATA[<h2 id="有效的故障排查手段"><a href="#有效的故障排查手段" class="headerlink" title="有效的故障排查手段"></a>有效的故障排查手段</h2><h3 id="理论："><a href="#理论：" class="headerlink" title="理论："></a>理论：</h3><p>反复采用假设排除手段的过程：<br>不断提出一个造成系统问题的假设，进而针对这些假设进行测试和排除</p>
<blockquote>
<p>常见的陷阱</p>
<ul>
<li>关注的错误的系统现象，或者错误地理解了系统现象的含义。</li>
<li>不能正确的修改系统的配置信息，输入信息或者系统运行环境。</li>
<li>将问题过早的归结为极为不可能的因素，或者之前曾经发生过的问题</li>
<li>试图解决与当前问题相关的一些问题，却没有认识到只是巧合。</li>
</ul>
</blockquote>]]>
    </summary>
    <title>《SRE google 运维解密》读书笔记 （二）</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="SRE" scheme="https://xilidou.com/categories/SRE/"/>
    <category term="SRE" scheme="https://xilidou.com/tags/SRE/"/>
    <content>
      <![CDATA[<p>新财年换了领导，管理风格也有一些区别。在团队内增加了一个 SRE 的职位。这一财年我将会承担一部分 SRE 的工作。</p><p>之前作为开发者，总的来说从开发的角度来思考系统的稳定性。现在需要从更高更全面的角度来思考和理解站点的稳定性。上网研究了一番，SRE 是 google 的一个职位同时 SRE 也是一套 google 总结出来的站点稳定性的方法论。所以找来了 <a href="https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAMsJK1olXDYCVl1fCEgQAF9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFksVAG0IGFwWQl9HCANtahdjURhUfR51IQF4BAcicUxcQjlra1cZbQcyVF9cCU8SCm4PG2slXQEyAjBdCUoWAm4NG14WbQcyVFlZCEwQBGoKE10UXjYFVFdtXRNHXSRBQwR7ATYyZF1tD0seF2l6WgkBW3QyZF5tC3tVbWoKElITW1NSAFldW04QCjtbGAlFVQRRBA1ZXRhCVjtdK1kUXAILZA">《SRE google 运维解密》</a>。这本书成书比较早，里面有些章节介绍的技术栈可能过时。具体我也不了解 google 内部是否还在使用。但是方法论还是很合理、科学的。</p><p>一直以来我工作过的团队对于风险的态度都是，预防和杜绝。但是在这本书里面，google 对于风险的态度就变成了管理，合理使用，甚至利用风险来保证项目的迭代。</p><span id="more"></span><h2 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h2><p>SER 是指 Site Reliability Engineer（站点可靠性工程师）。SRE 在 google 中有一套比较成熟的方法论包括如下：</p><ul><li>可用性改造</li><li>延迟优化</li><li>性能优化</li><li>效率优化</li><li>变更管理</li><li>监控</li><li>紧急事务处理</li><li>容量规划与管理</li></ul><h3 id="SRE-方法论："><a href="#SRE-方法论：" class="headerlink" title="SRE 方法论："></a>SRE 方法论：</h3><h4 id="确保长期关注研发"><a href="#确保长期关注研发" class="headerlink" title="确保长期关注研发"></a>确保长期关注研发</h4><p>SRE 只有 50% 的时间投入运维工作，如果超过就需要将任务分配至研发团队，形成良性循环，激励研发团队设计构建出不需要人工干预，自主运行的系统。<br>出现事故需要推动事后总结。</p><h4 id="保证服务在-SLO-的前提下最大化迭代"><a href="#保证服务在-SLO-的前提下最大化迭代" class="headerlink" title="保证服务在 SLO 的前提下最大化迭代"></a>保证服务在 SLO 的前提下最大化迭代</h4><ul><li>正确认识“错误预算”，系统不能 100% 可用，也不应该追求 100 %可用。</li><li>业务系统可用利用错误预算，上新功能，黑度，AB test 等。</li><li>SRE 目标并不是 0 事故，而是与业务团队一起管理好“错误预算”</li></ul><h4 id="监控系统"><a href="#监控系统" class="headerlink" title="监控系统"></a>监控系统</h4><ul><li>监控是 SRE 了解系统的重要手段</li><li>监控只有三类输出<ul><li>紧急报警：收到报警的用户必须<strong>立即</strong>采取某些操作，解决问题或者避免即将发生的问题</li><li>工单：收到报警的用户可以采取某些操作非立即，只需要在时效内完成。系统不会受到影响</li><li>日志：平时无需关注日志，日志作为调试或者事后分析使用</li></ul></li></ul><h4 id="应急事件处理"><a href="#应急事件处理" class="headerlink" title="应急事件处理"></a>应急事件处理</h4><ul><li>可靠性是 MTTF（平均失败时间） 和 MTTR （平均回复时间）的函数</li><li>人工操作的事情会延长回复时间<ul><li>运维手册</li><li>事故演练</li></ul></li></ul><p> 可以缩短恢复时间</p><h4 id="变更的管理"><a href="#变更的管理" class="headerlink" title="变更的管理"></a>变更的管理</h4><ul><li>采用渐进式的发布</li><li>迅速检测出问题的机制</li><li>出现问题可以快速回滚</li></ul><h4 id="需求的预测和容量规划"><a href="#需求的预测和容量规划" class="headerlink" title="需求的预测和容量规划"></a>需求的预测和容量规划</h4><ul><li>有明确的自然增加的预测</li><li>规划中还要考虑非自然增涨的需求来源的统计</li><li>定期压测，了解系统</li></ul><h4 id="资源部署"><a href="#资源部署" class="headerlink" title="资源部署"></a>资源部署</h4><p>资源是变更和规划的产物</p><ul><li>快速正确的部署资源是基本的要求</li></ul><h4 id="效率和性能"><a href="#效率和性能" class="headerlink" title="效率和性能"></a>效率和性能</h4><p>改善利用率，降低成本。<br>从三个因素推动效率提升</p><ul><li>用户需求</li><li>可用容量</li><li>资源利用率</li></ul><h2 id="Google-的生产环境"><a href="#Google-的生产环境" class="headerlink" title="Google 的生产环境"></a>Google 的生产环境</h2><p>成书较早，参考价值不大（略）</p><h2 id="拥抱风险"><a href="#拥抱风险" class="headerlink" title="拥抱风险"></a>拥抱风险</h2><h3 id="管理风险"><a href="#管理风险" class="headerlink" title="管理风险"></a>管理风险</h3><p>可靠性的提升，投入并不是线性的</p><h4 id="冗余"><a href="#冗余" class="headerlink" title="冗余"></a>冗余</h4><ul><li>设备的冗余</li><li>计算的冗余，增加一些空间进行奇偶校验</li></ul><h4 id="机会成本"><a href="#机会成本" class="headerlink" title="机会成本"></a>机会成本</h4><ul><li>如果工程师投入到可靠性建设，就不能从事为用户开发的工作中了</li></ul><p>所以，可靠性的管理是通过风险的管理进行的。提升系统可靠性和服务故障的耐受水平同等重要。努力提升服务可靠性，但是不超过服务需要的可靠性。否则将会付出更多的成本。</p><h3 id="度量服务的风险"><a href="#度量服务的风险" class="headerlink" title="度量服务的风险"></a>度量服务的风险</h3><p>按时间：</p><blockquote><p>可用性&#x3D; 正常时间&#x2F;（正常时间+ 不可用时间）</p></blockquote><p>四个九 一年宕机 52 分钟<br>合计次数</p><blockquote><p>可用性 &#x3D; 成功次数&#x2F;总调用次数</p></blockquote><p>对于分布式系统按时间是不合理的，总有部分系统在线，所以 google 倾向使用按次统计</p><h3 id="服务的风险容忍度"><a href="#服务的风险容忍度" class="headerlink" title="服务的风险容忍度"></a>服务的风险容忍度</h3><ul><li>客户对服务失败的容忍度<ul><li>toB 要比 toC 低很多</li><li>付费要比免费低</li><li>关系到收入的要低</li></ul></li><li>故障的类型</li><li>成本</li><li>其他服务指标</li></ul><h3 id="基础设施容忍度"><a href="#基础设施容忍度" class="headerlink" title="基础设施容忍度"></a>基础设施容忍度</h3><ul><li>可用性目标水平<ul><li>高可用性很贵</li><li>要看人下菜碟，合理保障</li></ul></li><li>故障类型</li><li>成本</li></ul><h2 id="错误预算使用的目的"><a href="#错误预算使用的目的" class="headerlink" title="错误预算使用的目的"></a>错误预算使用的目的</h2><p>错误预算的构建：</p><ol><li>产品管理层定义一个 SLO，确定服务的预计正常运行时间</li><li>通过监控来度量</li><li>而知差值就是不可靠预算</li><li>如果预算为正就能够进行发布和变更。</li></ol><h3 id="好处"><a href="#好处" class="headerlink" title="好处"></a>好处</h3><p>创新和可靠性的平衡点。</p><p>使用这个控制回路来调节发布的速度，有预算就快速迭代，如果频繁违反 SLO 或者错误预算被耗尽，就需要暂停发布，在测试和开发环节投入更多资源，提升系统可用性。</p><p>如果客观的故障发生比如光缆被挖断，影响了 SLO 需要扣减错误预算么？需要的，每个人都有义务保障服务正常运行。</p><p>利用错误预算机制，还能够找到定的过高的可用性指标。如果预算耗尽，团队无法发布，就可以考虑降低 SLO 来提升创新速度。</p><p>注：SLO 并非越高越好，稳定和创新通常是矛盾的。使用错误预算机制，闭环平衡稳定和创新的关系。</p><h2 id="服务质量目标"><a href="#服务质量目标" class="headerlink" title="服务质量目标"></a>服务质量目标</h2><h3 id="术语"><a href="#术语" class="headerlink" title="术语:"></a>术语:</h3><ul><li>SLI (indicator) 服务的某一个量化指标。比如<ul><li>延迟（rt）</li><li>错误（error）</li><li>吞吐量（qps）</li><li>可用性</li></ul></li><li>SLO (Objective) 可用性目标，通常指：</li></ul><p>范围下限 &lt;&#x3D; SLI &lt;&#x3D; 范围上限</p><ul><li>SLA (Agreement) 服务质量协议，指达到或者没有达到某个 SLO 的后果。</li></ul><h3 id="SLI-的实践中的应用"><a href="#SLI-的实践中的应用" class="headerlink" title="SLI 的实践中的应用"></a>SLI 的实践中的应用</h3><h4 id="关心什么指标"><a href="#关心什么指标" class="headerlink" title="关心什么指标"></a>关心什么指标</h4><ul><li>用户可见的系统：<ul><li>可用性</li><li>延迟</li><li>吞吐</li></ul></li><li>存储系统：<ul><li>延迟</li><li>可用性</li><li>持久性</li></ul></li><li>大数据系统：<ul><li>吞吐</li><li>延迟</li><li>时间</li></ul></li><li>所有系统都有关注延迟</li></ul><h4 id="收集"><a href="#收集" class="headerlink" title="收集"></a>收集</h4><h4 id="汇总"><a href="#汇总" class="headerlink" title="汇总"></a>汇总</h4><h4 id="标准化"><a href="#标准化" class="headerlink" title="标准化"></a>标准化</h4><h3 id="SLO-在实践中的应用"><a href="#SLO-在实践中的应用" class="headerlink" title="SLO 在实践中的应用"></a>SLO 在实践中的应用</h3><h4 id="目标的定义"><a href="#目标的定义" class="headerlink" title="目标的定义"></a>目标的定义</h4><ul><li>指出如何被度量</li><li>有效的条件</li></ul><h4 id="目标的选择"><a href="#目标的选择" class="headerlink" title="目标的选择"></a>目标的选择</h4><ul><li>不要仅以目前的状态为基础选择（要用发展的眼光）</li><li>保持简单</li><li>避免绝对值</li><li>SLO 越少越好</li><li>不要追求完美</li></ul><h4 id="控制手段"><a href="#控制手段" class="headerlink" title="控制手段"></a>控制手段</h4><ol><li>监控并度量 SLI</li><li>是否需要人工干预</li><li>如果需要干预，决定怎么干预</li><li>执行具体干预措施</li></ol><h4 id="SLO-建立用户预期"><a href="#SLO-建立用户预期" class="headerlink" title="SLO 建立用户预期"></a>SLO 建立用户预期</h4><ul><li>留有余量</li><li>实际 SLO 不要过高</li></ul><h3 id="SLA-的使用"><a href="#SLA-的使用" class="headerlink" title="SLA 的使用"></a>SLA 的使用</h3><h2 id="减少琐事"><a href="#减少琐事" class="headerlink" title="减少琐事"></a>减少琐事</h2><h3 id="琐事的定义"><a href="#琐事的定义" class="headerlink" title="琐事的定义"></a>琐事的定义</h3><ul><li>手动性</li><li>重复性</li><li>可被自动化的</li><li>战术性的（突然出现的，非策略驱动和主动安排的）</li><li>没有持久价值的</li><li>与服务同步线性增长的（良好的设计至少是有数量级增长的）</li></ul><h3 id="SRE-工作内容"><a href="#SRE-工作内容" class="headerlink" title="SRE 工作内容"></a>SRE 工作内容</h3><p>50% 琐事，50% 工程项目</p><h3 id="工程工作"><a href="#工程工作" class="headerlink" title="工程工作"></a>工程工作</h3><p>工程工作，是新颖的，本质上需要主观判断的工作。战略性的。有创新性和创造性的。通过设计来解决问题，越通用越好。</p><h3 id="琐事的危害"><a href="#琐事的危害" class="headerlink" title="琐事的危害"></a>琐事的危害</h3><ul><li>职业停滞</li><li>士气低落</li><li>造成误解</li><li>进展缓慢</li><li>开创先例（如果愿意接受琐事，那就会有更多的琐事）</li><li>产生摩擦</li><li>违反承诺</li></ul><h2 id="分布式系统的监控"><a href="#分布式系统的监控" class="headerlink" title="分布式系统的监控"></a>分布式系统的监控</h2><h3 id="术语定义"><a href="#术语定义" class="headerlink" title="术语定义"></a>术语定义</h3><ul><li>监控</li><li>白盒监控<ul><li>对系统暴露的性能指标进行监控</li></ul></li><li>黑盒监控<ul><li>通过测试某种外部用户可见的系统进行监控</li></ul></li><li>dashboard</li><li>警报</li><li>根源问题<ul><li>某个缺陷被修复，就可以保证这种缺陷不再发生以同样的方式发生。</li></ul></li><li>节点或者机器</li><li>推送</li></ul><h3 id="为什么要监控"><a href="#为什么要监控" class="headerlink" title="为什么要监控"></a>为什么要监控</h3><ul><li>分析长期趋势</li><li>跨世纪范围的比较，或者实验组和对照组之间的区别</li><li>报警</li><li>构建监控 dashboard</li><li>临时性问题的回溯分析</li></ul><p>监控可以在系统发生故障或者将要发生故障的时候通知我们。<br>处理报警会占用员工的时间，报警太频繁会造成“狼来了”效应</p><h3 id="对监控系统设置合理预期"><a href="#对监控系统设置合理预期" class="headerlink" title="对监控系统设置合理预期"></a>对监控系统设置合理预期</h3><p>Google 倾向于使用简单和快速的监控，配合高效的工具进行分析。避免使用“魔方”系统-试图自动学习或者自动检查故障的系统。<br>监控系统的规则越简单约好。<br>监控系统信噪比应该很高，发出报警的组件应该简单可靠。</p><h3 id="黑盒和白盒监控"><a href="#黑盒和白盒监控" class="headerlink" title="黑盒和白盒监控"></a>黑盒和白盒监控</h3><p>白盒监控应该要作为监控的主要手段。<br>黑盒监控是面向现象的-现在发生的，而非即将发生的。<br>白盒监控大量依赖对系统内部信息的检测。白盒监控可以检测到即将发生的问题和重试严掩盖问题。白盒系统既可以面向原因也可以面向现象。</p><h3 id="4-个黄金指标"><a href="#4-个黄金指标" class="headerlink" title="4 个黄金指标"></a>4 个黄金指标</h3><ul><li>延迟（rt）</li><li>流量</li><li>错误</li><li>饱和度<ul><li>通常是系统中最为受限的某个具体指标的度量。</li><li>复杂系统里面，可以配合其他搞层次的负载度量使用。使用一个简介的指标。</li></ul></li></ul><h3 id="长尾"><a href="#长尾" class="headerlink" title="长尾"></a>长尾</h3><p>只使用平均值是不足以描述系统的。需要区分平均值的慢，或者长尾值的慢。可以对数据进行分组统计。</p><h3 id="简化直到不能再简化"><a href="#简化直到不能再简化" class="headerlink" title="简化直到不能再简化"></a>简化直到不能再简化</h3><ul><li>最能反应正式故障的规则越简单越好</li><li>不常见的报警就要删除（定时删除没有用到的报警）</li><li>没有被报警规则使用的信息，就应该</li></ul><h3 id="报警的深层次理论"><a href="#报警的深层次理论" class="headerlink" title="报警的深层次理论"></a>报警的深层次理论</h3><ul><li>每当收到报警，需要立即进行某种操作，每天次数有限，过多会有“狼来了”效应</li><li>每个紧急报警都应该是可以具体操作的</li><li>报警的回复都应该是需要某种智力过程的，如果只需要固定的机械操作，那就不应该是紧急报警</li><li>每个紧急报警都应该是正交的，不应该彼此重叠</li></ul><h3 id="监控系统的长期维护"><a href="#监控系统的长期维护" class="headerlink" title="监控系统的长期维护"></a>监控系统的长期维护</h3><p>系统不断演变，软件经常重构，负载和性能目标也经常变化。所以监控系统的的设计和决策充分考虑长期目标。每一个报警都会占用优化系统的时间。花时间投入监控，换取未来系统的稳定是值得的。</p><p>短期和长期的可用性经常冲突。通过一些“暴力”因素，可以使一个摇摇欲坠系统保持一定的高可用性，这种方案不能长久，且依赖个人英雄主义。</p><p>短期接受稳定性的降级获得长期的可用性提升。</p><h2 id="Google-自动化演进"><a href="#Google-自动化演进" class="headerlink" title="Google 自动化演进"></a>Google 自动化演进</h2><h3 id="自动化的价值"><a href="#自动化的价值" class="headerlink" title="自动化的价值"></a>自动化的价值</h3><ul><li>一致性</li><li>平台性<ul><li>自动化的系统可以提供一个可以扩展的、广泛适用的。</li><li>同时会将错误集中化、意味着修复的缺陷是一劳永逸的。</li></ul></li><li>修复速度更快</li><li>行动速度快</li><li>节约时间</li></ul><h2 id="发布工程"><a href="#发布工程" class="headerlink" title="发布工程"></a>发布工程</h2><h3 id="哲学"><a href="#哲学" class="headerlink" title="哲学"></a>哲学</h3><ul><li>自服务模型</li><li>追求速度</li><li>密闭性 <ul><li>构建工具必须确保一致性和可重复性</li></ul></li><li>强调策略和流程</li></ul><h3 id="持续构建和部署"><a href="#持续构建和部署" class="headerlink" title="持续构建和部署"></a>持续构建和部署</h3><ul><li>构建</li><li>分支<ul><li>所有代码默认提交到主分支上。</li><li>构建一个发布分支</li><li>发布分支不会并入主分支</li><li>代码从主分支 cherry pick 到发布分支</li></ul></li><li>测试<ul><li>单测</li></ul></li><li>打包</li><li>部署</li><li>部署</li></ul><h3 id=""><a href="#" class="headerlink" title=""></a></h3><h3 id="配置管理"><a href="#配置管理" class="headerlink" title="配置管理"></a>配置管理</h3><h3 id="一开始就进行发布工程"><a href="#一开始就进行发布工程" class="headerlink" title="一开始就进行发布工程"></a>一开始就进行发布工程</h3><p>不要做时候诸葛亮</p><h2 id="简单化"><a href="#简单化" class="headerlink" title="简单化"></a>简单化</h2><p>软件系统是本质上是动态和不稳定的</p><ul><li><p>系统的稳定性和灵活性</p><ul><li>为了灵活性牺牲稳定性是有意义的。</li></ul></li><li><p>乏味是一种美德</p></li><li><p>定期删除无用代码</p></li><li><p>“负代码行”作为指标</p><ul><li>臃肿的软件置管术是不可取的</li><li>添加代码可能引入新的缺陷</li><li>小的代码容易理解，也容易测试，缺陷就越少</li></ul></li><li><p>最小 API</p><ul><li>书写一个明确的，最小的 API 是软件系统简单的必要部分</li><li>方法越少，参数越少也容易理解</li></ul></li><li><p>模块化</p></li><li><p>发布简单化</p></li></ul><p>软件的简单是可靠性的前提。</p>]]>
    </content>
    <id>https://xilidou.com/2022/04/11/sre1/</id>
    <link href="https://xilidou.com/2022/04/11/sre1/"/>
    <published>2022-04-11T10:57:08.000Z</published>
    <summary>
      <![CDATA[<p>新财年换了领导，管理风格也有一些区别。在团队内增加了一个 SRE 的职位。这一财年我将会承担一部分 SRE 的工作。</p>
<p>之前作为开发者，总的来说从开发的角度来思考系统的稳定性。现在需要从更高更全面的角度来思考和理解站点的稳定性。上网研究了一番，SRE 是 google 的一个职位同时 SRE 也是一套 google 总结出来的站点稳定性的方法论。所以找来了 <a href="https://union-click.jd.com/jdc?e=618%7Cpc%7C&p=JF8BAMsJK1olXDYCVl1fCEgQAF9MRANLAjZbERscSkAJHTdNTwcKBlMdBgABFksVAG0IGFwWQl9HCANtahdjURhUfR51IQF4BAcicUxcQjlra1cZbQcyVF9cCU8SCm4PG2slXQEyAjBdCUoWAm4NG14WbQcyVFlZCEwQBGoKE10UXjYFVFdtXRNHXSRBQwR7ATYyZF1tD0seF2l6WgkBW3QyZF5tC3tVbWoKElITW1NSAFldW04QCjtbGAlFVQRRBA1ZXRhCVjtdK1kUXAILZA">《SRE google 运维解密》</a>。这本书成书比较早，里面有些章节介绍的技术栈可能过时。具体我也不了解 google 内部是否还在使用。但是方法论还是很合理、科学的。</p>
<p>一直以来我工作过的团队对于风险的态度都是，预防和杜绝。但是在这本书里面，google 对于风险的态度就变成了管理，合理使用，甚至利用风险来保证项目的迭代。</p>]]>
    </summary>
    <title>《SRE google 运维解密》读书笔记 （一）</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="随笔" scheme="https://xilidou.com/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="总结" scheme="https://xilidou.com/tags/%E6%80%BB%E7%BB%93/"/>
    <content>
      <![CDATA[<p>2021 就这么结束了。</p><h2 id="家"><a href="#家" class="headerlink" title="家"></a>家</h2><p>今年我做爸爸了。<br>今年九月，迎来了我们家的小朋友。豆嫂从怀孕一路走来。如打怪升级一样。一关一关的过，颇为不容易。</p><p><img data-src="/images/jia.png" alt="jia"></p><span id="more"></span><ul><li>豆嫂孕早期孕吐严重，某天在地铁上没吃早饭，低血糖晕倒在地铁上，还好我在身边。周围的好心人都把自己的零食给了我们。下车后豆嫂把手里的一捧零食吃完以后，才重新坐上地铁上去上班。</li><li>小朋友在肚子里总是不安分，总是脐带绕颈。时而一圈，时而两圈。</li><li>九月的最后一次产检。调皮的小朋友臀位。没有正常入盆，只能剖腹产。</li><li>小朋友出生那天，五点起来给豆嫂煮了小米粥。吃完以后，又睡了一会。八点把豆嫂送到医院准备手术。</li><li>产科大夫，麻醉大夫分别告知了风险。我在知情同意书上签了字</li><li>豆嫂进了手术室，我在手术室外面焦虑的不行，一圈一圈的走。就在彻底走不动的时候。手术室的门打开了，护士把我叫了过去，轻轻的掀开了蓝色的无菌布。一个粉白粉白的小生命出现在我的眼前。</li><li>看了一眼小朋友。小朋友就被推进了新生儿室。我站在门口，隔着毛玻璃看着里面护士忙碌的影子，突然一股暖流充满了全身，眼睛也湿润了，我做爸爸了。</li><li>由于疫情，医院全封闭管理，我并没有看到豆嫂。豆嫂说，麻醉过后特别冷，伤口特别疼。</li><li>本以为一切结束了。稍微放松一点准备回家。在路上又被叫回了医院。医生上来就交代，血氧不足，哭声不正常，有风险。瞬间天旋地转，脚如灌铅。艰难的挪到了楼梯边，坐在楼梯上，给家里人打电话。</li><li>下午，吸了氧的小朋友终于恢复正常。</li><li>欢迎这个小生命来到这个世界，希望你能够健康快乐的成长，身体强壮，取小名“壮壮”。</li></ul><p>之后，去月子中心。出月子。满一百天去“中国照相馆”拍了纪念照。现在小朋友正躺在自己的小床上沉沉的睡去。呼吸均匀。大概再过两个小时又会饿得哇哇大哭，要喝奶了。</p><h2 id="车"><a href="#车" class="headerlink" title="车"></a>车</h2><p>今年我在北京有自己的车了。<br>今年为了照顾孕妇的出行。买了一辆车。还记得提车那天激动的几乎睡不着。坐在家里都忍不住到地库看了又看。现在回想起来自己小时候最爱的的汽车玩具，应该就是一辆 E30 平台的宝马三系。爱车的我终于在 31 岁生日前有了自己的第一辆车，而且是在北京。</p><p><img data-src="/images/che.png" alt="che"></p><p>前几天，某人把车撞到了路边的墩子，更换前杠，保险没白买。</p><h2 id="旅行"><a href="#旅行" class="headerlink" title="旅行"></a>旅行</h2><p>今年只去了稻城亚丁。<br>海拔 4700 米的壮美雪山。无限风光在险峰。吸光了 4 瓶氧气，终于爬到了牛奶海。总体来说一路高反都是值得的。</p><p><img data-src="/images/4700.png" alt="4700"></p><h2 id="投资"><a href="#投资" class="headerlink" title="投资"></a>投资</h2><p>今年我没有亏钱。<br>今年的投资居然没有亏钱。没有最热点新能源赛道。跟着长赢计划慢慢布局。一种踏实的感觉。不急不躁，踏踏实实的，多大点事。中丐互怜被锤，但是中证 500 在涨啊。配置分散，降低风险，控制回撤，是我今年的投资的体会。</p><h2 id="内心平和"><a href="#内心平和" class="headerlink" title="内心平和"></a>内心平和</h2><p>今年我没有去年焦虑。<br>信息焦虑在 2020 年给我带来了极大的痛苦。而今年想明白了一些事情反而内心平和了很多。</p><ul><li><p>没看到的就是没有，没注意的的就是不重要。主动离开那些生怕错过的信息渠道。万一群里的信息对我有用、万一这个短视频说得我用得上、万一这个人人脉以后我用得上。这些万一其实消耗着我们的精力，但是没有什么意义。事情不重要，就不需要知道。如果事情足够重要，那我一定会知道。</p></li><li><p>全情的长时间的投入精力在某一件事情，在网络的社会中是很奢侈的事情。当投入时间到某样事情的时候，我们感知到的机会成本就在上升，所谓机会成本，就是当你做某件事前的时候，不得不放弃别的事情带来的好处。一旦投入时间精力做的事情，出现了挫折，损失就是没有做好这件事情加上没做那件更好事情的收益。现在的互联网就是如此，优秀的作品那么多，但是视频只能一个一个看，文章只能一篇一篇读。这就带来了一个悖论，内容越丰富，机会成本就越高。毕竟因为选择做某件事情，投入了时间。错过的优质信息就越多。这个就是信息爆炸带来的焦虑。</p></li><li><p>抑制这种焦虑可以从几个角度进行</p><ul><li>正视这个问题，为焦虑的情绪寻找出口，比如“收藏了就是读了，买了就是学了”。</li><li>回归现实，现实世界中，人的感官处理的事务没有那么多选择，选择某件事情的机会成本，感觉上没有那么大。</li></ul></li></ul><h2 id="期待"><a href="#期待" class="headerlink" title="期待"></a>期待</h2><p>明年没有什么过高的期待。</p><ul><li>疫情可以结束</li><li>小朋友可以健康成长</li><li>工作顺利</li><li>家人健康</li></ul><p>2021 再见。</p>]]>
    </content>
    <id>https://xilidou.com/2022/01/01/2021/</id>
    <link href="https://xilidou.com/2022/01/01/2021/"/>
    <published>2022-01-01T21:12:17.000Z</published>
    <summary>
      <![CDATA[<p>2021 就这么结束了。</p>
<h2 id="家"><a href="#家" class="headerlink" title="家"></a>家</h2><p>今年我做爸爸了。<br>今年九月，迎来了我们家的小朋友。豆嫂从怀孕一路走来。如打怪升级一样。一关一关的过，颇为不容易。</p>
<p><img src="/images/jia.png" alt="jia"></p>]]>
    </summary>
    <title>2021 总结</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="多线程" scheme="https://xilidou.com/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    <category term="线程池" scheme="https://xilidou.com/tags/%E7%BA%BF%E7%A8%8B%E6%B1%A0/"/>
    <content>
      <![CDATA[<p>终于有一个 Java 版的微信机器人了。</p><p>公众号很久没有更新了。主要两个原因，换了工作之后，第一，要花更多的时间去了解和学习新的业务。第二，我最近把几乎所有的业余时间都来写这个 Java 版的微信机器人了。</p><p><img data-src="/images/java-wechaty.png" alt="java-wechaty"></p><h2 id="Wechaty-是什么"><a href="#Wechaty-是什么" class="headerlink" title="Wechaty 是什么"></a>Wechaty 是什么</h2><p>官网的描述是：</p><ul><li>A Conversational AI RPA SDK for Chatbot</li></ul><p>其实就是一个能够快速构建聊天机器人的开源 SDK。最早的时候，Wechaty 只是一个基于服务于微信工具库，现在逐渐的发展到可以对接世面上的主流聊天软件包括不限于：微信，企业微信，钉钉，Line 等。</p><p>编程语言也由原来的单一语言（TypeScript） 发展到，Java，Scala，Python，Go 等多语言实现的工具库了，同时社区生态还在不断的壮大。</p><p>Github 地址：<a href="https://github.com/wechaty/wechaty">https://github.com/wechaty/wechaty</a> 目前已经有 7.9k 的 star 了。</p><span id="more"></span><h2 id="与-Wechaty-结缘"><a href="#与-Wechaty-结缘" class="headerlink" title="与 Wechaty 结缘"></a>与 Wechaty 结缘</h2><p>之前的工作，老板有一个要求，是就每天下班后，发一封邮件日报简单描述一下今天工作进展。如果忘记发日报，第二天就负责整理 全组人的日报。作为一个健忘的人，忘记发日报简直就是家常便饭。</p><p>于是就考虑需要一个机制：</p><ul><li>每天提醒我发日报</li><li>动作尽可能简单，且自动化。</li></ul><p>当时就想能不能在微信上有一个机器人，每天定时提醒我发日报，而且只要回复这个机器人，他就能够把我回复的消息，按照固定模板生成日报并发送给老板。这样既不会忘记，也能简单自动化的完成这个工作。</p><p>一顿 Google 还真找到了 Wechaty 这个工具。尝试写了一个日报机器人满足了我的需求。于是再接再厉，又写了一个提醒女朋友吃饭的工具，但是因为不熟悉 TypeScript。写出的机器人没法停止，变成了一个信息轰炸机，差点被拉黑。<a href="https://mp.weixin.qq.com/s?__biz=MzU2NTQ1NTAxNQ==&mid=2247483767&idx=1&sn=ca72401e514dded0c84b1220f887cdf4&chksm=fcba30bfcbcdb9a98e8c455357b38fda66f7af203ce09101597f23ae6a5d1eb133c48c7f63d3&token=656593281&lang=zh_CN#rd">居然有人能忘记吃饭？写个微信机器人提醒他</a></p><p>就是因为这篇文章，还结识了 Wechaty 的作者李佳芮。现在她的公司已经估值很多个 0 了。</p><p>由于我的主要工作语言是 Java ，对 TypeScript 还是了解不多，就暂时放下了。</p><h2 id="Java-版的-Wechaty"><a href="#Java-版的-Wechaty" class="headerlink" title="Java 版的 Wechaty"></a>Java 版的 Wechaty</h2><p>在 Wechaty 的某个版本后，开始支持 GRPC 作为传输协议。这个时候我觉得多语言开发的环境就比较成熟了。于是我就开始尝试写一个 Java 版的 wechaty。</p><h3 id="Java-vs-Kotlin"><a href="#Java-vs-Kotlin" class="headerlink" title="Java vs Kotlin"></a>Java vs Kotlin</h3><p>Wechaty 使用 TypeScripe 开发，在移植的过程中，发现要实现 TS 版对应的功能，Java 所需要的模板代码就太多了，开发起来效率不够快。于是就考虑可不可以使用 Kotlin 来构建 Java-wechaty sdk。</p><p>Kotlin 有以下特性感觉比较适合 Wechaty 的开发：</p><ul><li>Java 和 Kotlin 之间可以无障碍的互相操作</li><li>在 Kotlin 中，函数也是第一公民，可以脱离类的存在，这一点在移植 TS 代码的时候优势就比较明显了。</li><li>空指针安全，之前写 Java 的时候，受够了一步一检查。Kotlin 在语言层面就解决了空指针安全的问题。写起来有效的减少心智负担。</li><li>Kotlin 是务实的，更有表现力的语言。语法更加接近 TS 和 GO，相对 Java 来说更加简洁。</li></ul><h3 id="事件驱动"><a href="#事件驱动" class="headerlink" title="事件驱动"></a>事件驱动</h3><p>TS 版的 Wechaty 是基于 Nodejs 开发的，一个典型的事件驱动的架构。在开发初期我就自然想到了使用 <code>Vertx</code> 框架来开发。但是开发一段时间后发现，其实 <code>Vertx</code> 是一个事件驱动的网络框架。主要解决的还是网络相关的问题，放到 Java-wechaty 中还是太重了。</p><p>于是移除了代码中的 Vertx 框架，自己参考 Nodejs 中的 EventEmitter 实现了 Kotlin 版的事件驱动组件。</p><h3 id="整体架构"><a href="#整体架构" class="headerlink" title="整体架构"></a>整体架构</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><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><br><span class="line">  |                          | |                          |</span><br><span class="line">  |   Wechaty (TypeScript)   | |     Wechaty (Java)       |</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">  |                 Wechaty Puppet Hostie                 |</span><br><span class="line">  |                                                       |</span><br><span class="line">  |                (wechaty-puppet-hostie)                |</span><br><span class="line">  +-------------------------------------------------------+</span><br><span class="line"></span><br><span class="line">+---------------------  @chatie/grpc  ----------------------+</span><br><span class="line"></span><br><span class="line">  +-------------------------------------------------------+</span><br><span class="line">  |                Wechaty Puppet Abstract                |</span><br><span class="line">  |                                                       |</span><br><span class="line">  |                   (wechaty-puppet)                    |</span><br><span class="line">  +-------------------------------------------------------+</span><br><span class="line"></span><br><span class="line">  +--------------------------+ +--------------------------+</span><br><span class="line">  |      Pad Protocol        | |      Web Protocol        |</span><br><span class="line">  |                          | |                          |</span><br><span class="line">  | wechaty-puppet-padplus   | |(wechaty-puppet-puppeteer)|</span><br><span class="line">  +--------------------------+ +--------------------------+</span><br><span class="line">  +--------------------------+ +--------------------------+</span><br><span class="line">  |    Windows Protocol      | |       Mac Protocol       |</span><br><span class="line">  |                          | |                          |</span><br><span class="line">  | (wechaty-puppet-windows) | | (wechaty-puppet-macpro)  |</span><br><span class="line">  +--------------------------+ +--------------------------+</span><br></pre></td></tr></table></figure><p>通过这个图可看到，Wechaty 的结构设计还比清晰。利用 Puppet 的架构，将真正的通信协议和具体的 IM 软件进行了隔离。基于这一点不同的语言基于 Puppet 的协议就可以进行多语言开发。</p><h3 id="好用么"><a href="#好用么" class="headerlink" title="好用么"></a>好用么</h3><p>感谢 Wechaty 前期良好的 API 设计几行代码就可以开发自己聊天机器人：</p><p>Demo 1：</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="class"><span class="keyword">class</span> <span class="title">Bot</span></span>&#123;</span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String args[])</span></span>&#123;</span><br><span class="line">    Wechaty bot = Wechaty.instance()</span><br><span class="line">      .onScan((qrcode, statusScanStatus, data) -&gt; System.out.println(QrcodeUtils.getQr(qrcode)))</span><br><span class="line">      .onLogin(user -&gt; System.out.println(<span class="string">&quot;User logined :&quot;</span> + user))</span><br><span class="line">      .onMessage(message -&gt; System.out.println(<span class="string">&quot;Message:&quot;</span> + message))</span><br><span class="line">      .start(<span class="keyword">true</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这个 Demo 6 行代码，就实现了机器人的扫码登录，接受消息的功能。同时现在 Java-wechaty 还支持可插拔的插件。利用插件，可以更简单的构建机器人。</p><p>Demo 2：</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="class"><span class="keyword">class</span> <span class="title">Bot</span></span>&#123;</span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String args[])</span></span>&#123;</span><br><span class="line">    Wechaty bot = Wechaty.instance()</span><br><span class="line">            .use(</span><br><span class="line">                WechatyPlugins.ScanPlugin(),</span><br><span class="line">                WechatyPlugins.DingDongPlugin(<span class="keyword">null</span>)</span><br><span class="line">            )</span><br><span class="line">            .start(<span class="keyword">true</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>随着插件的原来越丰富，可能以后，用户只需要组合各种插件，就能达成自己的需求，尽量的做到低代码开发。</p><h3 id="现在达到什么程度了"><a href="#现在达到什么程度了" class="headerlink" title="现在达到什么程度了"></a>现在达到什么程度了</h3><p>目前 Java-wechaty 已经完成了 TS 版的功能的移植。</p><p>实现了基础的的聊天，好友管理，群管理功能。接下来的开发就会集中在 API 的打磨，稳定性的提升。同时也期待你的加入为 Java-wechaty 贡献代码。</p><h3 id="从-Java-wechaty-中能得到什么"><a href="#从-Java-wechaty-中能得到什么" class="headerlink" title="从 Java-wechaty 中能得到什么"></a>从 Java-wechaty 中能得到什么</h3><ol><li>真正的参与开源代码的贡献。</li><li>在 Maven 中央库，发布了自己的 Jar 包。</li><li>认识了各种各样小伙伴，包括写了 25 年程序的天使投资人 @Huan。</li><li>在写 Java-wechaty 的时候，不断的参考伙伴们的 TypeScript，Go，Python 代码，从实际的角度去审视各种编程语言的特性。探寻语言各个特性设计的初衷。</li></ol><h2 id="期待你的加入"><a href="#期待你的加入" class="headerlink" title="期待你的加入"></a>期待你的加入</h2><p>Wechtay 社区加入了由 <strong>中科院软件所</strong> 与 <strong>openEuler 社区</strong> 共同举办的一项面向高校学生的暑期活动《开源软件供应链点亮计划-暑期2020》。</p><p>详情见： <a href="https://github.com/wechaty/summer-of-code">https://github.com/wechaty/summer-of-code</a></p><p>Wechaty 给学生们提供了很多有意思的题目，比如：</p><ol><li>利用 AI 技术，开发一个 AI 斗图机器人</li><li>利用 Wechaty 的插件技术，开发一个“每日一句”插件，替你向妹子嘘寒问暖的”撩妹“机器人</li><li>还有偏向工程的，代码移植工作，让学生真正的参与到开源项目其中</li></ol><p>开发语言涉及，TypeScript，Go，Java，Kotlin，Python 甚至还有 Scala，总有一个适合你。</p><p>希望看到这里的你，可以把篇文章，转发给学习计算机，或者对编程感兴趣的学生朋友，期待他们加入。</p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>Java-wechaty <a href="https://github.com/wechaty/java-wechaty">项目地址</a>。 加入我们你也可以六行代码写一个微信机器人。</p>]]>
    </content>
    <id>https://xilidou.com/2020/06/03/java-wechaty/</id>
    <link href="https://xilidou.com/2020/06/03/java-wechaty/"/>
    <published>2020-06-03T00:39:17.000Z</published>
    <summary>
      <![CDATA[<p>终于有一个 Java 版的微信机器人了。</p>
<p>公众号很久没有更新了。主要两个原因，换了工作之后，第一，要花更多的时间去了解和学习新的业务。第二，我最近把几乎所有的业余时间都来写这个 Java 版的微信机器人了。</p>
<p><img src="/images/java-wechaty.png" alt="java-wechaty"></p>
<h2 id="Wechaty-是什么"><a href="#Wechaty-是什么" class="headerlink" title="Wechaty 是什么"></a>Wechaty 是什么</h2><p>官网的描述是：</p>
<ul>
<li>A Conversational AI RPA SDK for Chatbot</li>
</ul>
<p>其实就是一个能够快速构建聊天机器人的开源 SDK。最早的时候，Wechaty 只是一个基于服务于微信工具库，现在逐渐的发展到可以对接世面上的主流聊天软件包括不限于：微信，企业微信，钉钉，Line 等。</p>
<p>编程语言也由原来的单一语言（TypeScript） 发展到，Java，Scala，Python，Go 等多语言实现的工具库了，同时社区生态还在不断的壮大。</p>
<p>Github 地址：<a href="https://github.com/wechaty/wechaty">https://github.com/wechaty/wechaty</a> 目前已经有 7.9k 的 star 了。</p>]]>
    </summary>
    <title>终于有一个 Java 可以用的微信机器人了</title>
    <updated>2026-08-08T02:04:49.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="vertx" scheme="https://xilidou.com/tags/vertx/"/>
    <category term="dingding" scheme="https://xilidou.com/tags/dingding/"/>
    <content>
      <![CDATA[<p>最近研究 Vetrx 简直爱不释手。迫不及待的想给大家介绍一下。</p><p><img data-src="/images/carbon.png" alt="carbon"></p><h2 id="Vertx-是什么"><a href="#Vertx-是什么" class="headerlink" title="Vertx 是什么"></a>Vertx 是什么</h2><ul><li>Vertx 是一个运行在 JVM 上，用来构建响应式应用的工具集。</li><li>基于 netty 的高性能的，异步的网络库。</li><li>对 netty 进行了封装，提供更加友好的 API。</li><li>同时实现了一些基于异步调用的库，包括database connection, monitoring, authentication, logging, service discovery, clustering support, etc。</li></ul><span id="more"></span><h2 id="为什么我推荐学习"><a href="#为什么我推荐学习" class="headerlink" title="为什么我推荐学习"></a>为什么我推荐学习</h2><p>其实随着技术的发展。异步调用其实越来越普及了。</p><p>1、现在随着 RPC 的普及。类似 Dubbo 这样的框架都是基于 NIO 的概念带来的，了解异步编程有助于学习理解框架。</p><p>2、响应式编程逐渐由客户端，前端向后端渗透。</p><p>3、更容易的编写出高性能的异步服务。</p><h2 id="Vertx-的几个重要概念"><a href="#Vertx-的几个重要概念" class="headerlink" title="Vertx 的几个重要概念"></a>Vertx 的几个重要概念</h2><h3 id="Event-Loop"><a href="#Event-Loop" class="headerlink" title="Event Loop"></a>Event Loop</h3><p>Event Loop 顾名思义，就是事件循环的。在 Vertx 的生命周期内，会不断的轮询查询事件。</p><p>传统的多线程编程模型，每个请求就 fork 一个新的线程对请求进行处理。这样的编程模型有实现起来比较简单，一个连接对应一个线程，如果有大量的请求需要处理，就需要 fork 出大量的线程进行处理，对于操作系统来说调度大量线程造成系统 load 升高。</p><p>所以为了能够处理大量请求，就需要过渡到基于 Roactor 模型的 Event Loop上。</p><p>Eventloop 不断的轮训，获取事件然后安排上不同的 Handler 处理对应的Event。</p><p>这里要注意的是为了保证程序的正常运行，event 必须是非阻塞的。否则就会造成 eventloop 的阻塞，影响Vertx 的表现。但是现实中的程序肯定不能保证都是非阻塞的，Vertx 也提供了相应的处理阻塞的方法的机制。我们在下面会继续介绍。</p><h3 id="Verticle"><a href="#Verticle" class="headerlink" title="Verticle"></a>Verticle</h3><p>在 Vertx 中我们经常可以看见 Vertical 组件。</p><p><img data-src="/images/verticle-threading-config.png" alt="verticle"></p><p>Verticle 是由 Vert.x 部署和运行的代码块。默认情况一个 Vert.x 实例维护了N（默认情况下N &#x3D; CPU核数 x 2）个 Event Loop 线程。Verticle 实例可使用任意 Vert.x 支持的编程语言编写，而且一个简单的应用程序也可以包含多种语言编写的 Verticle。</p><p>您可以将 Verticle 想成 <a href="https://en.wikipedia.org/wiki/Actor_model">Actor Model</a> 中的 Actor。</p><p>一个应用程序通常是由在同一个 Vert.x 实例中同时运行的许多 Verticle 实例组合而成。不同的 Verticle 实例通过向 Event Bus 收发送消息来相互通信。</p><h3 id="Event-bus"><a href="#Event-bus" class="headerlink" title="Event bus"></a>Event bus</h3><p>Vertx 中的 Event bus 如果类比后端常用的 MQ 就更加容易理解了。实际上 Event Bus 就是 Verticle 之间传递 信息的桥梁。</p><p>换句话说，就是 Java 通用设计模式中的监听模式，或者是我们常说的 基于 MQ 消息开发模式。</p><p><img data-src="/images/event-bus.png" alt="Event bus"></p><h2 id="回到-Vertx"><a href="#回到-Vertx" class="headerlink" title="回到 Vertx"></a>回到 Vertx</h2><p>上文我们讨论了 vertx 的模型和机制，现在人们就看看怎么使用 vertx 开发一个程序。</p><p>我会结合之前写的 暴打钉三多的来进行讲解,一切从 Vertx 开始。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">val</span> vertx = Vertx.vertx()</span><br></pre></td></tr></table></figure><p>vertx 是整个 vert.x 框架的核心。通常来说 Vertx 所有的行为就是从 vertx 这个类中产生的。</p><h3 id="Don’t-call-us-we’ll-call-you"><a href="#Don’t-call-us-we’ll-call-you" class="headerlink" title="Don’t call us, we’ll call you"></a>Don’t call us, we’ll call you</h3><p>Vert.x 是一个事件驱动框架。所谓事件驱动是指当某件事情发生以后，就做这个动作。</p><p>我们再回到标题， “Don’t call us, we’ll call you” 这个原则，其实就是当我们 发现你能完成这项工的时候，我们会找你的。你不需要主动来联系我。</p><p>我们通过代码来理解一下 Vertx 是怎么实现这个原则的 ：</p><figure class="highlight kotlin"><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">server.requestHandler(request -&gt; &#123;</span><br><span class="line">  request.response().end(<span class="string">&quot;hello world!&quot;</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>这个代码块的意思是，每当 server 的 request 被调用的时候，就返回一个 <code>hello world</code> 。</p><p>所以 Vertx 中的 ‘you’ j就是各种各样的 Handler 。大多数时候我们编写 Vertx 的程序，实际上就是在编写Handler 的行为。然后再告诉 Vertx ，每当 XXX 事件触发以后，你就调用 XXX Handler。</p><h3 id="Don’t-block-me"><a href="#Don’t-block-me" class="headerlink" title="Don’t block me"></a>Don’t block me</h3><p>Vertx 是基于事件的，上文我们提到了 Event Loop ，在 Vertx 中，EventLoop 就是一个勤劳的小蜜蜂，不断的去寻找，到底有哪些事件被触发了。然后再执行对应的 Handler。假如执行 Hanlder 的线程，就是 Event Loop 线程。如过 Handler 执行的时间过长。就会阻塞 Event Loop 。造成别的事件触发的时候。Event Loop 还在处理时间花费较长的 Handler。Event loop就不及时的响应其他的事。</p><p>但是现实中，不可能所有的事件 都是非阻塞的。比如查询数据库，调用远程接口等等，那怎么办呢？</p><p>在事件驱动模型中，大概有两种套路解决，这个问题，比如在 Redis 中，Redis 会十分小心的维护一个时间分片。当某个人物执行事件过长的话，就保存当前事件的状态，然后暂停当前事件，重新由 Event loop 进行调度。防止 Event Loop 被事件阻塞。</p><p>还有一种套路，就是把阻塞的事件，交给别的线程来来执行。Event Loop 就可以继续进行事件的循环，防止被阻塞。事实上 Vertx 就是这么操作的。</p><figure class="highlight kotlin"><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">vertx.executeBlocking(promise -&gt; &#123;</span><br><span class="line">  <span class="comment">// Call some blocking API that takes a significant amount of time to return</span></span><br><span class="line">  String result = someAPI.blockingMethod(<span class="string">&quot;hello&quot;</span>);</span><br><span class="line">  promise.complete(result);</span><br><span class="line">&#125;, res -&gt; &#123;</span><br><span class="line">  System.<span class="keyword">out</span>.println(<span class="string">&quot;The result is: &quot;</span> + res.result());</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>如果我们开发的时候意识到这个 Handler 是一个阻塞的，就需要告诉 vertx 这是是一个 Blocking 的需要交给别的线程来处理。</p><h2 id="协调异步处理"><a href="#协调异步处理" class="headerlink" title="协调异步处理"></a>协调异步处理</h2><p>上文提到. Vertx 是通过 Handler 来处理事件的，但是，很多时候，某个操作，通常需要不止一个 Handler 来对数据进行处理。如果一直使用 callback 的写法，就会形成箭头代码。产生地狱回调的问题。</p><p>作为一个异步框架，Vertx 一般使用 Future 来解决回调地狱的问题。理解 Vertx 中的 Future 是编写好的代码的核心。</p><p>通常我们理解 Future 只是一个占位符，代表某个操作未来某个时候的结果。不太清楚的可以看我以前写文章。</p><p>这里需要特别指出的是 Vertx 的 Future 和 Jdk 里面的 <code>CompletableFuture</code> 原理和理念类似，但是使用起来有很大的区别的。</p><p>Jdk 里面的 <code>CompletableFuture</code> 是可以直接使用 <code>result()</code> 阻塞的等待结果，但是 Vertx 中的 Future 如果直接使用 <code>result()</code> ，就会立刻从 Future 中取出结果，而不是阻塞的等待结果，就很容易收获一个 Null。</p><p>明确这个区别以后，写起代码就不会出错了。</p><h2 id="Event-Bus"><a href="#Event-Bus" class="headerlink" title="Event Bus"></a>Event Bus</h2><p>如果在日常开发中使用过消息系统，就很容易理解 Vertx 中的 Event bus 了。官方文档把 Event bus 比作 Vertx 的神经系统，其实我们就认为，Event bus是 Vertx 的消息系统，就好了。</p><h2 id="钉钉内网穿透代理的的开发"><a href="#钉钉内网穿透代理的的开发" class="headerlink" title="钉钉内网穿透代理的的开发"></a>钉钉内网穿透代理的的开发</h2><p>这个小 Demo 麻雀虽小但是包含了 Vertx 几个关键组件的使用。写这个 Demo 的时候，正好在学习 Kotlin 所以顺手就用 kotlin 写了。如果写过 Java 或者 Typescript 那你也能很容易的看懂。</p><p>项目包含了</p><ul><li>Http Service 用于接收钉钉的回调</li><li>WebSocket Service 用于向 Client 推送收到的回调，达到内网穿透的目的。</li><li>Vertx Config 用于配置项目相关参数，便于使用</li><li>Event Bus 的使用，用于 Http Service 和 WebSocket 之间传递消息。</li></ul><h3 id="先来一个-Verticle"><a href="#先来一个-Verticle" class="headerlink" title="先来一个 Verticle"></a>先来一个 Verticle</h3><p>Gradle 配置文件如下先引入包：</p><figure class="highlight groovy"><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">implementation (<span class="string">&quot;io.vertx:vertx-core:3.8.5&quot;</span>)</span><br><span class="line">implementation (<span class="string">&quot;io.vertx:vertx-web:3.8.5&quot;</span>)</span><br><span class="line">implementation (<span class="string">&quot;io.vertx:vertx-lang-kotlin:3.8.5&quot;</span>)</span><br></pre></td></tr></table></figure><p>上文我我们已经介绍了 Verticle 是什么了，为了方便开发，Vertx 给我们提供了一个  AbstractVerticle 抽象类。直接继承：</p><figure class="highlight kotlin"><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="class"><span class="keyword">class</span> <span class="title">DingVerticle</span> : <span class="type">AbstractVerticle</span></span>() &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>AbstractVerticle</code> 中包含了 Vericle 常用的一些方法。</p><p>我们可以重写 <code>start()</code> 方法,来初始化我们 Verticle 的行为。</p><h3 id="HttpService-的创建"><a href="#HttpService-的创建" class="headerlink" title="HttpService 的创建"></a>HttpService 的创建</h3><figure class="highlight kotlin"><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">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">start</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">val</span> httpServer = vertx.createHttpServer()</span><br><span class="line">    <span class="keyword">val</span> router = Router.router(vertx)</span><br><span class="line">    router.post(<span class="string">&quot;/ding/api&quot;</span>).handler&#123;event -&gt;</span><br><span class="line">        <span class="keyword">val</span> request = event.request()</span><br><span class="line">        request.bodyHandler &#123; t -&gt;</span><br><span class="line">            println(t)</span><br><span class="line">        &#125;</span><br><span class="line">        event.response().end();</span><br><span class="line">    &#125;</span><br><span class="line">    httpServer.requestHandler(router);</span><br><span class="line">    httpServer.listen(<span class="number">8080</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码比较简单:</p><ol><li>创建一个 httpService</li><li>设置一个 Router，如果写过 Spring Mvc 相关的代码。这里的 Router 就类似 Controller 里面的 RequestMapping 。用于指定一个 Http 请求 URI 和 Method 对应的 Handler。这里的 Handler 是一个 lambda 表达式。只是简单的把请求的 body 打印出来。</li><li>将 Router 加入到 httpService 中，并监听 8080 端口。</li></ol><h3 id="WebSocketService"><a href="#WebSocketService" class="headerlink" title="WebSocketService"></a>WebSocketService</h3><p>webSocket协议是这个 proxy 的关键，因为 WebSocket 不同于 Http，是双向通通信的。依赖这个特性我们可以把消息“推到”内网。达到内网“穿透”的目的。</p><figure class="highlight kotlin"><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">httpServer.webSocketHandler &#123; webSocket: ServerWebSocket -&gt;</span><br><span class="line">    <span class="keyword">val</span> binaryHandlerID = webSocket.binaryHandlerID()</span><br><span class="line">    webSocket.endHandler() &#123;</span><br><span class="line">        log.info(<span class="string">&quot;end&quot;</span>, binaryHandlerID)</span><br><span class="line">    &#125;</span><br><span class="line">    webSocket.writeTextMessage(<span class="string">&quot;欢迎使用 xilidou 钉钉 代理&quot;</span>)</span><br><span class="line">    webSocket.writeTextMessage(<span class="string">&quot;连接成功&quot;</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码也比较简单，就是向 Vertx 注册一个处理 WebSocket 的 Handler。</p><h3 id="Event-Bus-的使用"><a href="#Event-Bus-的使用" class="headerlink" title="Event Bus 的使用"></a>Event Bus 的使用</h3><p>作为代理最核心的功能就是转发钉钉的回调消息，前面我说到，Event Bus 在 Vertx 中起到了“神经系统的作用”实际上 ，换句话说，就是http 服务收到回调的时候，可以通过 Event Bus 发出消息。WebSocket 在收到 Event Bus 发来的消息的时候，推送给客户端。如下图看图：</p><p>为了方便理解，我们就使用 MQ 里面通常的概念生产者和消费者。</p><p>所以我们使用在  HttpService 中注册一个生产者，收到钉钉的回调以后，把消息转发出来。</p><p>为了便于编写，我们可以单独写一个 HttpHandler</p><figure class="highlight kotlin"><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></pre></td><td class="code"><pre><span class="line"><span class="comment">//1</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HttpHandler</span></span>(<span class="keyword">private</span> <span class="keyword">val</span> eventBus: EventBus) : Handler&lt;RoutingContext&gt; &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> log = LoggerFactory.getLogger(<span class="keyword">this</span>.javaClass);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">handle</span><span class="params">(event: <span class="type">RoutingContext</span>)</span></span> &#123;</span><br><span class="line">        <span class="keyword">val</span> request = event.request()</span><br><span class="line">        request.bodyHandler &#123; t-&gt;</span><br><span class="line">                <span class="keyword">val</span> jsonObject = JsonObject(t)</span><br><span class="line">                <span class="keyword">val</span> toString = jsonObject.toString()</span><br><span class="line">                log.info(<span class="string">&quot;request is &#123;&#125;&quot;</span>,toString);</span><br><span class="line">                <span class="comment">// 2</span></span><br><span class="line">                eventBus.publish(<span class="string">&quot;callback&quot;</span>, toString)</span><br><span class="line">        &#125;</span><br><span class="line">        event.response().end(<span class="string">&quot;ok&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里需要注意几个问题:</p><ol><li>我们需要使用 Event Bus 发送消息，所以需要在构造函数里面传入一个 Event Bus</li><li>我们在收到消息以后，可以先将数据转换为 Json 字符串，然后发送消息，注意这里使用的是 <code>publish()</code> 是广播的意思，这样所有订阅的客户端都能收到新消息。</li></ol><p>有了生产者，并发出了数据，我们就可以，在 WebSocket 里面消费这个消息，然后推送给客户端了</p><p>再来写一个 WebSocket 的 Handler</p><figure class="highlight kotlin"><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="comment">//1</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">WebSocketHandler</span></span>(<span class="keyword">private</span> <span class="keyword">val</span> eventBus: EventBus) : Handler&lt;ServerWebSocket&gt; &#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">val</span> log = LoggerFactory.getLogger(<span class="keyword">this</span>.javaClass)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">handle</span><span class="params">(webSocket: <span class="type">ServerWebSocket</span>)</span></span> &#123;</span><br><span class="line">        <span class="keyword">val</span> binaryHandlerID = webSocket.binaryHandlerID()</span><br><span class="line"></span><br><span class="line">        <span class="comment">//2</span></span><br><span class="line">        <span class="keyword">val</span> consumer = eventBus.consumer&lt;String&gt;(<span class="string">&quot;callback&quot;</span>) &#123; message -&gt;</span><br><span class="line">            <span class="keyword">val</span> body = message.body()</span><br><span class="line">            log.info(<span class="string">&quot;send message &#123;&#125;&quot;</span>, body)</span><br><span class="line">            <span class="comment">//3</span></span><br><span class="line">            webSocket.writeTextMessage(body)</span><br><span class="line">        &#125;</span><br><span class="line">        webSocket.endHandler() &#123;</span><br><span class="line">            log.info(<span class="string">&quot;end&quot;</span>, binaryHandlerID)</span><br><span class="line">            <span class="comment">//4</span></span><br><span class="line">            consumer.unregister();</span><br><span class="line">        &#125;</span><br><span class="line">        webSocket.writeTextMessage(<span class="string">&quot;欢迎使用 xilidou 钉钉 代理&quot;</span>)</span><br><span class="line">        webSocket.writeTextMessage(<span class="string">&quot;连接成功&quot;</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里需要注意几个问题:</p><ol><li>初始化的时候需要注入 eventBus</li><li>写一个 <code>consumer()</code> 消费 HttpHandler 发来的消息</li><li>将消息写入到 webSocket 中，发送给 Client</li><li>WebSocket 断开后需要回收 consumer</li></ol><h3 id="初始化-Vertx"><a href="#初始化-Vertx" class="headerlink" title="初始化 Vertx"></a>初始化 Vertx</h3><p>做了那么多准备终于可以初始化我们的 Vertx 了</p><figure class="highlight kotlin"><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="class"><span class="keyword">class</span> <span class="title">DingVerticleV2</span>: <span class="type">AbstractVerticle</span></span>()&#123;</span><br><span class="line">    <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">start</span><span class="params">()</span></span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//2</span></span><br><span class="line">        <span class="keyword">val</span> eventBus = vertx.eventBus()</span><br><span class="line">        <span class="keyword">val</span> httpServer = vertx.createHttpServer()</span><br><span class="line"></span><br><span class="line">        <span class="keyword">val</span> router = Router.router(vertx);</span><br><span class="line">        <span class="comment">//3</span></span><br><span class="line">        router.post(<span class="string">&quot;/api/ding&quot;</span>).handler(HttpHandler(eventBus));</span><br><span class="line">        httpServer.requestHandler(router);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//4</span></span><br><span class="line">        httpServer.webSocketHandler(WebSocketHandler(eventBus));</span><br><span class="line">        httpServer.listen(<span class="number">8080</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">//1</span></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">val</span> vertx = Vertx.vertx()</span><br><span class="line">    vertx.deployVerticle(DingVerticleV2())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里需要注意几个问题:</p><ol><li>初始化 Vertx 并部署他</li><li>初始化 eventBus</li><li>注册 HttpHandler</li><li>注册 WebSocketHandler</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ul><li>Vertx 是一个工具，不是框架，所以可以很方便的与其他框架组合。</li><li>Vertx 是一个基于 Netty 的异步框架。我们可以向编写同步代码一样，编写异步代码。</li><li>vertx 在代码中主要有两个作用，一个是初始化组件，比如 ：</li></ul><figure class="highlight kotlin"><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="keyword">val</span> eventBus = vertx.eventBus()</span><br><span class="line"><span class="keyword">val</span> httpServer = vertx.createHttpServer()</span><br></pre></td></tr></table></figure><p>还有一个是注册 <code>Handler</code>:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">httpServer.webSocketHandler(WebSocketHandler(eventBus));</span><br></pre></td></tr></table></figure><ul><li>Event Bus 是一个消息系统。用于不同的 Handler 直接传递数据，简化开发。</li></ul><h2 id="相关连接"><a href="#相关连接" class="headerlink" title="相关连接"></a>相关连接</h2><ol><li>使用教程 <a href="https://xilidou.com/2020/03/25/dingsanduo/">钉钉机器人回调内网穿透代理–使用篇</a></li><li>Github 地址: <a href="https://github.com/diaozxin007/DingTalkProxy">Github</a></li><li>官网教程：<a href="https://vertx.io/docs/guide-for-java-devs/">A gentle guide to asynchronous programming with Eclipse Vert.x for Java developers</a>;</li><li><a href="https://vertx.io/docs/vertx-core/kotlin/">Vert.x Core Manual</a></li></ol><p>欢迎关注我的微信公众号:</p><p><img data-src="/images/2019-04-25-022202.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2020/04/12/vertx-dingding/</id>
    <link href="https://xilidou.com/2020/04/12/vertx-dingding/"/>
    <published>2020-04-12T16:39:16.000Z</published>
    <summary>
      <![CDATA[<p>最近研究 Vetrx 简直爱不释手。迫不及待的想给大家介绍一下。</p>
<p><img src="/images/carbon.png" alt="carbon"></p>
<h2 id="Vertx-是什么"><a href="#Vertx-是什么" class="headerlink" title="Vertx 是什么"></a>Vertx 是什么</h2><ul>
<li>Vertx 是一个运行在 JVM 上，用来构建响应式应用的工具集。</li>
<li>基于 netty 的高性能的，异步的网络库。</li>
<li>对 netty 进行了封装，提供更加友好的 API。</li>
<li>同时实现了一些基于异步调用的库，包括database connection, monitoring, authentication, logging, service discovery, clustering support, etc。</li>
</ul>]]>
    </summary>
    <title>Vertx入门到实战—实现钉钉机器人内网穿透代理</title>
    <updated>2026-08-08T02:04:49.447Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="工具" scheme="https://xilidou.com/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="dingtalk" scheme="https://xilidou.com/tags/dingtalk/"/>
    <category term="钉钉" scheme="https://xilidou.com/tags/%E9%92%89%E9%92%89/"/>
    <content>
      <![CDATA[<p>“山川异域，风月同钉”，被钉钉暴打的你，是不是已经想写一个机器人调戏一下钉钉了。在写机器人的时候，钉钉机器人的回调需要填写一个公网 http 地址。</p><p>这还没开发机器人，就没有 http 服务，没有 http 服务就收不到钉钉的回调，没有回调就不能调试机器人。不能调试机器人，就不能上线。</p><p><img data-src="/images/black.jpg" alt="black"></p><span id="more"></span><p>又一次陷入了被钉钉暴打的死循环，办法总比问题多，所以为了解决这个问题。我们就需要一个公网代理。所以我们就来撸一个。</p><p>这里注意一下，由于一般开发人员都处在内网环境。要想让代理做内网穿透，技术比较复杂。所以我们就换个思路。我们可以利用 Websocket 的双工的特性。接入代理，当代理收到钉钉的回调的时候，把消息推到我们本地开发环境。提升我们开发的效率。见下图：</p><p><img data-src="/images/dingproxy.jpg" alt="dingproxy.jpg"></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><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">git <span class="built_in">clone</span> https://github.com/diaozxin007/DingTalkProxy</span><br><span class="line"><span class="built_in">cd</span> DingProxyServer</span><br><span class="line">./gradlew build</span><br><span class="line">java -jar build/libs/dingWs-all.jar</span><br><span class="line"><span class="comment"># 如果需要在后台运行</span></span><br><span class="line">nohup java -jar build/libs/dingWs-1.0.0-all.jar &amp;&gt;&gt; nohup.out &amp; tailf nohup.out</span><br></pre></td></tr></table></figure><p>可以修改 resources 下的 <code>server.properties</code></p><figure class="highlight shell"><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">#</span><span class="bash"> 监听端口</span></span><br><span class="line">server.port=8080</span><br><span class="line"><span class="meta">#</span><span class="bash"> 钉钉回调的 uri</span></span><br><span class="line">server.api=/ding/api</span><br></pre></td></tr></table></figure><p>然后重新运行:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">./gradlew build</span><br></pre></td></tr></table></figure><p>这个时候，proxy 已经开始正常运行了。</p><p>如果只是想看看一看钉钉回调的报文，那就可以直接使用 [websock-test] (<a href="http://www.websocket-test.com/">http://www.websocket-test.com/</a>) GUI 调试工具。</p><p>如果想在代码里面使用可以参考 DingProxyClinet 里面的代码。</p><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><p>Q:1、为什么我连不上服务？</p><p>A:确认服务是否只开启了 https，如果开启了 https, 需要把协议头修改为 wss。</p><p>Q:2、我还是连不上？</p><p>A:需要确认 nginx 的配置，是否支持 WebSocket。</p><p>可以在 nginx 的配置中增加</p><figure class="highlight shell"><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">proxy_set_header Upgrade $http_upgrade;</span><br><span class="line">proxy_set_header Connection &quot;Upgrade&quot;;</span><br><span class="line"><span class="meta">#</span><span class="bash"> 如果频繁超时断开可以配置</span></span><br><span class="line">proxy_connect_timeout 7d;</span><br><span class="line">proxy_send_timeout 7d;</span><br><span class="line">proxy_read_timeout 7d;</span><br></pre></td></tr></table></figure><p>Q:3、除了做钉钉的代理，还能干什么？</p><p>A: 理论上可以代理一切请求，然后转换为 String 通过 WebSocket 推送到客户端。</p><p>Q:4、我懒得部署服务了</p><p>A：可以使用我提供的公益服务</p><p>在回调接口中填写：</p><ul><li><a href="https://api.xilidou.com/ding/api">https://api.xilidou.com/ding/api</a></li></ul><p>WebSocket 地址为:</p><ul><li>wss:&#x2F;&#x2F;api.xilidou.com</li></ul><p>为了防止滥用，每个客户端每次连接只能接收 10 条消息，然后会被断开。</p><p>Github <a href="https://github.com/diaozxin007/DingTalkProxy">传送门</a></p><p>下一篇文章将会具体讲解，如何使用 vertx 实现这个代理。敬请期待。</p>]]>
    </content>
    <id>https://xilidou.com/2020/03/25/dingsanduo/</id>
    <link href="https://xilidou.com/2020/03/25/dingsanduo/"/>
    <published>2020-03-25T23:26:21.000Z</published>
    <summary>
      <![CDATA[<p>“山川异域，风月同钉”，被钉钉暴打的你，是不是已经想写一个机器人调戏一下钉钉了。在写机器人的时候，钉钉机器人的回调需要填写一个公网 http 地址。</p>
<p>这还没开发机器人，就没有 http 服务，没有 http 服务就收不到钉钉的回调，没有回调就不能调试机器人。不能调试机器人，就不能上线。</p>
<p><img src="/images/black.jpg" alt="black"></p>]]>
    </summary>
    <title>钉钉机器人回调内网穿透代理--使用篇</title>
    <updated>2026-08-08T02:04:49.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="随笔" scheme="https://xilidou.com/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="算法" scheme="https://xilidou.com/tags/%E7%AE%97%E6%B3%95/"/>
    <category term="trie" scheme="https://xilidou.com/tags/trie/"/>
    <content>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>是的，最近我又换工作了，在看新团队的代码的时候发现，同事们为了追求服务的响应时间，在项目中大量的使用了很多高级的数据结构。</p><p>作为传统 Curd 程序员，对算法和数据结构已经比较生疏了。如今看到这些”高级的代码“有点汗颜。所以趁周末好好的在家补课，重新复习一下。</p><p>文章将会是一个系列，慢慢的查缺补漏。</p><p><img data-src="/images/workhard.jpg" alt="Trie/TrieTree1.png"></p><span id="more"></span><h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>Trie 树又叫字典查找树。顾名思义，字典查找树，主要解决的就是字符串的查找。有以下两个优势。</p><ul><li>查找命中的时间复杂度是 O(k)，k指的是需要查询的 key 的长度。这里注意和字库的大小无关。</li><li>对于未命中的字符，只需要查询若干字符就可。</li></ul><h2 id="基本数据结构"><a href="#基本数据结构" class="headerlink" title="基本数据结构"></a>基本数据结构</h2><p>首先 Trie 树，是一棵树。树是由需要建立的所有词构成。</p><p>假设我们有，bee 、sea、 shells，she，sells，几个单词。我们可以使用这几个单词构建一棵树。</p><p>通过图片我们就可以直观的看出 Trie 的数据结构。这个棵树是由若干节点，链接而成，节点可以指向下一个节点，也可以指向空。从 root 节点开始，顺着链接随便找某个链接往下，直到最低端，经过的路径正好是上文的单词。</p><p><img data-src="/images/TrieTree1.png" alt="Trie/TrieTree1.png"></p><h3 id="数据的代码表示"><a href="#数据的代码表示" class="headerlink" title="数据的代码表示"></a>数据的代码表示</h3><p>为了方便使用代码表示。可以考虑每个节点使用数组表示。每个节点都含有一个数组，数组的大小为R，R 是数组的基数，对应每个可能出现的字符。R 的选取取决于报错的字符的类型，如果只包含英文则256 就可以了。如果是中文就需要 65536。</p><p>字符和键值都保存在数据结构中。</p><p><img data-src="/images/TrieTree3.png" alt="Trie/TrieTree3.png"></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><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TrieST</span>&lt;<span class="title">Value</span>&gt; </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">final</span> <span class="keyword">int</span> R = <span class="number">256</span>;</span><br><span class="line"> <span class="keyword">private</span> Node root;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">Node</span> </span>&#123;</span><br><span class="line">  <span class="keyword">public</span> Object val; <span class="comment">// 键值</span></span><br><span class="line">  <span class="keyword">public</span> Node[] next = <span class="keyword">new</span> Node[R];</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Get-和-Put-方法"><a href="#Get-和-Put-方法" class="headerlink" title="Get 和 Put 方法"></a>Get 和 Put 方法</h3><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><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> Node <span class="title">get</span><span class="params">(Node x, String key, <span class="keyword">int</span> d)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 1</span></span><br><span class="line">  <span class="keyword">if</span> (x == <span class="keyword">null</span>) &#123;</span><br><span class="line">   <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//2</span></span><br><span class="line">  <span class="keyword">if</span> (d == key.length()) &#123;</span><br><span class="line">   <span class="keyword">return</span> x;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//3</span></span><br><span class="line">  <span class="keyword">char</span> c = key.charAt(d);</span><br><span class="line"></span><br><span class="line">  <span class="comment">//4</span></span><br><span class="line">  <span class="keyword">return</span> get(x.next[c], key, d + <span class="number">1</span>);</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Value <span class="title">get</span><span class="params">(String key)</span> </span>&#123;</span><br><span class="line">  Node x = get(root, key, <span class="number">0</span>);</span><br><span class="line">  <span class="keyword">if</span> (x == <span class="keyword">null</span>) &#123;</span><br><span class="line">   <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> (Value) x.val;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>对于递归的我们需要考虑两个问题。递归的退出的条件是什么，如何进入下一层递归。</p><p>对于 <code>Node get(Node x, String key, int d)</code>，入参 <code>x</code> 是当前的节点，key 是需要查找的字字符串，d 是目前递归到的层数，也可以理解为，我们逐个遍历 key 的时候的下标。</p><p>我们按照注释逐行讲解一下：</p><ol><li>递归跳出的条件之一，就是发现上一次查询指向的节点是空的，说明没有找到匹配的字符串。所以直接返回一个 null，表示没有匹配上。</li><li>递归跳出的条件之二，就是key值已经遍历完了。并且找到了对应的 value。可喜可贺。</li><li>这里的 c 表示的就是key在下标为 d 的时候对应的字符。因为我们的 root 是第 0 个，所以遍历 key的 c 是从1开始。</li><li>递归调用 get 方法。将 x 的下一个节点传入方法，同时下标 d 加 1。</li></ol><p>我们再来看 put 方法：</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> Node <span class="title">put</span><span class="params">(Node x, String key, Value val,<span class="keyword">int</span> d)</span> </span>&#123;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">//1</span></span><br><span class="line">  <span class="keyword">if</span>(x == <span class="keyword">null</span>) &#123;</span><br><span class="line">   x= <span class="keyword">new</span> Node();</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//2</span></span><br><span class="line">  <span class="keyword">if</span>(d == key.length())&#123;</span><br><span class="line">   x.val = val;</span><br><span class="line">   <span class="keyword">return</span> x;</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">//3</span></span><br><span class="line">  <span class="keyword">char</span> c = key.charAt(d);</span><br><span class="line"></span><br><span class="line">  <span class="comment">//4</span></span><br><span class="line">  x.next[c] = put(x.next[c],key,val,d + <span class="number">1</span>);</span><br><span class="line">  <span class="keyword">return</span> x;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">put</span><span class="params">(String key,Value val)</span></span>&#123;</span><br><span class="line">  root = put(root,key,val,<span class="number">0</span>);</span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><p>put 方法和 get 方法非常类似，习惯上来说我们在保存数据的时候，都需要先查询一下看看数据存不存在，如果存在直接返回，如果不存在再插入数据。trie 数的插入也是这个思路。</p><p>我们按照注释逐行讲解一下：</p><ol><li>如果当前节点为空，则在当前节点插入一个空 value。注意：这里是新建一个节点，在这个新节点上插入空的 value，而不是插入一个空节点，注意区分。</li><li>同理，如果d &#x3D;&#x3D; key 的长度，表示已经将 key 遍历完了，需要把 key 对应的值保存在节点上了。</li><li>和 Get 一致，略。</li><li>递归调用 put 方法，将 x 的下一个节点传入方法，同时下标 d 加 1。然后逐层放回。</li></ol><p>看完这 Put 和 Get 方法。我们再回顾一下 trid 的性质。</p><p>查询的次数，只和代码中的 key 的长度有关，与字典的大小没有关系。</p><p>如果没有命中的数据，查询的次数小于等于 key 的长度 。</p><h2 id="应用"><a href="#应用" class="headerlink" title="应用"></a>应用</h2><p>这里先着重介绍一下 trie 树的其中一个应用 ”前缀匹配“。</p><p>我们在搜索框里面输入一个词的时候，通常会收到提示的列表如下图：</p><p><img data-src="/images/suggest.png" alt="Trie/Untitled.png"></p><p>输入 flink 的时候，搜索引擎会提示联想出用户可能的输入,提升用户体验。</p><p>有了上面的 Trie 树的介绍。具体实现这个功能就比较简单了。</p><p>回到我们原有的例子，假设词库里面有单词 bee 、sea、 shells，she，sells。如果用户输入 se 两个字符，我们应该会向用户提示 se 开始的词： sea 和 sells。</p><p><img data-src="/images/TrieTree2.png" alt="Trie/TrieTree2.png"></p><p>结合图片，我们要找到 se 开头的字符。我们首先要定位出图中红色的链条，然后把红色 e 的所有子链找出来。当然如果 e 的子链特别多，我们就需要考虑对子链进行截断。具体怎么截断我们以后会的文章里面可能会讲解。</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><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="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">collect</span><span class="params">(Node x, String pre, Queue&lt;String&gt; q)</span></span>&#123;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">//3</span></span><br><span class="line">  <span class="keyword">if</span>(x == <span class="keyword">null</span>)&#123;</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="comment">//4</span></span><br><span class="line">  <span class="keyword">if</span>(x.val != <span class="keyword">null</span>)&#123;</span><br><span class="line">   q.add(pre);</span><br><span class="line">  &#125;</span><br><span class="line">  </span><br><span class="line">  <span class="comment">//5</span></span><br><span class="line">  <span class="keyword">for</span>(<span class="keyword">char</span> c = <span class="number">0</span>;c &lt; R; c++)&#123;</span><br><span class="line">   collect(x.next[c],pre + c, q);</span><br><span class="line">  &#125;</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Iterable&lt;String&gt; <span class="title">keysWithPrefix</span><span class="params">(String pre)</span></span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">//1</span></span><br><span class="line">  Queue&lt;String&gt; q = <span class="keyword">new</span> LinkedList&lt;String&gt;();</span><br><span class="line">  </span><br><span class="line">  <span class="comment">//2</span></span><br><span class="line">  collect(get(root,pre,<span class="number">0</span>),pre,q);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> q;</span><br><span class="line"></span><br><span class="line"> &#125;</span><br></pre></td></tr></table></figure><p>逐条解释一下：</p><ol><li>初始化找一个容器存储起来。</li><li>其中的 <code>get(root,pre,0)</code> 就是为了找出上图中标红的 e节点。然后把 e 节点放到 <code>collect()</code> 方法中。</li><li>递归的退出条件就是到达某一个链的最子节点。</li><li>如果 x 节点的 val 不为空就加入到容器中。</li><li>暴力的遍历节点上的数组并 c 拼接到 pre 前缀上，递归查找。</li></ol><p>我们只需要调用方法 <code>keysWithPrefix(&quot;se&quot;)</code> 即可。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>trie 树在查询的时间复杂度是 O(k) 与词库的大小无关。<br>但是，有利必有弊。<br>利用数组表示节点实现的 Trie 树非常占用空间。</p><p>如果运用在英文文本处理中，假设单词的平均长度是 11 个字符，R 的大小是 256，100万个键构成的树大约有 2亿5千万个链接数。</p><p>是典型的空间换时间应用。</p><p>欢迎关注我的微信公众号:</p><p><img data-src="/images/2019-04-25-022202.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2020/03/07/trie/</id>
    <link href="https://xilidou.com/2020/03/07/trie/"/>
    <published>2020-03-07T18:12:03.000Z</published>
    <summary>
      <![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>是的，最近我又换工作了，在看新团队的代码的时候发现，同事们为了追求服务的响应时间，在项目中大量的使用了很多高级的数据结构。</p>
<p>作为传统 Curd 程序员，对算法和数据结构已经比较生疏了。如今看到这些”高级的代码“有点汗颜。所以趁周末好好的在家补课，重新复习一下。</p>
<p>文章将会是一个系列，慢慢的查缺补漏。</p>
<p><img src="/images/workhard.jpg" alt="Trie/TrieTree1.png"></p>]]>
    </summary>
    <title>周末补习（一）trie 树</title>
    <updated>2026-08-08T02:04:49.447Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="ArrayList" scheme="https://xilidou.com/tags/ArrayList/"/>
    <category term="transient" scheme="https://xilidou.com/tags/transient/"/>
    <content>
      <![CDATA[<p>上周在群里有小盆友问 <code>transient</code> 关键字是干什么的。这篇文章就以此为契机介绍一下 <code>transient</code> 的作用，以及在 ArrayList 里面的应用。</p><p>要了解 transient 我们先聊聊 Java 的序列化。</p><h2 id="复习序列化"><a href="#复习序列化" class="headerlink" title="复习序列化"></a>复习序列化</h2><p>所谓序列化是指，把对象转化为字节流的一种机制。同理，反序列化指的就是把字节流转化为对象。</p><p><img data-src="/images/serializable.jpg" alt="serializable"></p><span id="more"></span><ul><li>对于 Java 对象来说，如果使用 JDK 的序列化实现。对象需要实现 <code>java.io.Serializable</code> 接口。</li><li>可以使用 <code>ObjectOutputStream()</code> 和 <code>ObjectInputStream()</code> 对对象进行序列化和反序列化。</li><li>序列化的时候会调用 <code>writeObject()</code> 方法，把对象转换为字节流。</li><li>反序列化的时候会调用 <code>readObject()</code> 方法，把字节流转换为对象。</li><li>Java 在反序列化的时候会校验字节流中的 <code>serialVersionUID</code>  与对象的 <code>serialVersionUID</code> 时候一致。如果不一致就会抛出 <code>InvalidClassException</code> 异常。官方强烈推荐为序列化的对象指定一个固定的 <code>serialVersionUID</code>。否则虚拟机会根据类的相关信息通过一个摘要算法生成，所以当我们改变类的参数的时候虚拟机生成的 <code>serialVersionUID</code> 是会变化的。</li><li><code>transient</code> 关键字修饰的变量 <strong>不会</strong> 被序列化为字节流</li></ul><h2 id="复习ArrayList"><a href="#复习ArrayList" class="headerlink" title="复习ArrayList"></a>复习ArrayList</h2><p>1、ArrayList 是基于数组实现的，是一个动态数组，容量支持自动自动增长<br>2、ArrayList 线程不安全<br>3、ArrayList 实现了 Serializable，支持序列化</p><h2 id="勤俭持家"><a href="#勤俭持家" class="headerlink" title="勤俭持家"></a>勤俭持家</h2><p>上文我们说到 ArrayList 是基于数组实现，我们看看源码：</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="comment">/**</span></span><br><span class="line"><span class="comment"> * The array buffer into which the elements of the ArrayList are stored.</span></span><br><span class="line"><span class="comment"> * The capacity of the ArrayList is the length of this array buffer. Any</span></span><br><span class="line"><span class="comment"> * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA</span></span><br><span class="line"><span class="comment"> * will be expanded to DEFAULT_CAPACITY when the first element is added.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">transient</span> Object[] elementData; <span class="comment">// non-private to simplify nested class access</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * The size of the ArrayList (the number of elements it contains).</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@serial</span></span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">int</span> size;</span><br></pre></td></tr></table></figure><p>有几个重要的信息：</p><ul><li>ArraryList 是动态数组，这个 elementData 就是存储对象的数据。</li><li>这个数组居然使用了 transient 来修饰。</li><li>数组的长度等于 ArrayList 的容量。而不是 ArrayList 的元素数量。</li><li>size 是指的 ArrayList 中元素的数量，不是动态数组的长度。</li><li>size 没有被 transient 修饰，是可以被序列化的。</li></ul><p>这，怎么回事。ArrayList 存储数据的数组，居然不需要序列化？</p><p><img data-src="/images/black.jpg" alt="black"></p><p>莫慌，我们继续往下看代码。上文我们说过，对象的序列化和反序列化是通过调用方法 writeObject() 和 readObject() 完成了，我们发现，ArrayList 自己实现这两个方法看代码：</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Save the state of the &lt;tt&gt;ArrayList&lt;/tt&gt; instance to a stream (that</span></span><br><span class="line"><span class="comment"> * is, serialize it).</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@serialData</span> The length of the array backing the &lt;tt&gt;ArrayList&lt;/tt&gt;</span></span><br><span class="line"><span class="comment"> *             instance is emitted (int), followed by all of its elements</span></span><br><span class="line"><span class="comment"> *             (each an &lt;tt&gt;Object&lt;/tt&gt;) in the proper order.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">writeObject</span><span class="params">(java.io.ObjectOutputStream s)</span></span></span><br><span class="line"><span class="function">    <span class="keyword">throws</span> java.io.IOException</span>&#123;</span><br><span class="line">    <span class="comment">// Write out element count, and any hidden stuff</span></span><br><span class="line">    <span class="keyword">int</span> expectedModCount = modCount;</span><br><span class="line">    s.defaultWriteObject();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Write out size as capacity for behavioural compatibility with clone()</span></span><br><span class="line">    s.writeInt(size);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Write out all elements in the proper order.</span></span><br><span class="line">    <span class="keyword">for</span> (<span class="keyword">int</span> i=<span class="number">0</span>; i&lt;size; i++) &#123;</span><br><span class="line">        s.writeObject(elementData[i]);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (modCount != expectedModCount) &#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> ConcurrentModificationException();</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"> * Reconstitute the &lt;tt&gt;ArrayList&lt;/tt&gt; instance from a stream (that is,</span></span><br><span class="line"><span class="comment"> * deserialize it).</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">readObject</span><span class="params">(java.io.ObjectInputStream s)</span></span></span><br><span class="line"><span class="function">    <span class="keyword">throws</span> java.io.IOException, ClassNotFoundException </span>&#123;</span><br><span class="line">    elementData = EMPTY_ELEMENTDATA;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Read in size, and any hidden stuff</span></span><br><span class="line">    s.defaultReadObject();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Read in capacity</span></span><br><span class="line">    s.readInt(); <span class="comment">// ignored</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (size &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        <span class="comment">// be like clone(), allocate array based upon size not capacity</span></span><br><span class="line">        <span class="keyword">int</span> capacity = calculateCapacity(elementData, size);</span><br><span class="line">        SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);</span><br><span class="line">        ensureCapacityInternal(size);</span><br><span class="line">        Object[] a = elementData;</span><br><span class="line">        <span class="comment">// Read in all elements in the proper order.</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i=<span class="number">0</span>; i&lt;size; i++) &#123;</span><br><span class="line">            a[i] = s.readObject();</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>注意，在 writeObject() 方法中，</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="comment">// Write out all elements in the proper order.</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">int</span> i=<span class="number">0</span>; i&lt;size; i++) &#123;</span><br><span class="line">     s.writeObject(elementData[i]);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="keyword">int</span> i=<span class="number">0</span>; i&lt;size; i++) &#123;</span><br><span class="line">     a[i] = s.readObject();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有几个读几个。</p><p>总结一下：</p><ul><li>被 <code>transient</code> 修饰的变量不会被序列化。</li><li>ArrayList 的底层数组 <code>elementData</code> 被 <code>transient</code> 修饰，不会直接被序列化。</li><li>为了实现 ArrayList 元素的序列化，ArrayList 重写了 <code>writeObject()</code> 和 <code>readObject()</code> 方法。</li><li>按需序列化数组，只序列化存在的数据，而不是序列化整个 <code>elementData</code> 数组。</li></ul><p>用多少，序列化多少，真是勤俭持家的 ArrayList。</p><h2 id="有趣的代码系列"><a href="#有趣的代码系列" class="headerlink" title="有趣的代码系列"></a>有趣的代码系列</h2><p><a href="https://xilidou.com/2019/10/15/tomcat-threadpool/">那些有趣的代码(一)–有点萌的 Tomcat 的线程池</a><br><a href="https://xilidou.com/2019/10/27/tomcat-classloader/">那些有趣的代码(二)–偏不听父母话的 Tomcat 类加载器</a></p><p>欢迎关注我的微信公众号<br><img data-src="/images/2019-04-25-022226.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2019/12/05/arraylist-serializable/</id>
    <link href="https://xilidou.com/2019/12/05/arraylist-serializable/"/>
    <published>2019-12-05T15:11:02.000Z</published>
    <summary>
      <![CDATA[<p>上周在群里有小盆友问 <code>transient</code> 关键字是干什么的。这篇文章就以此为契机介绍一下 <code>transient</code> 的作用，以及在 ArrayList 里面的应用。</p>
<p>要了解 transient 我们先聊聊 Java 的序列化。</p>
<h2 id="复习序列化"><a href="#复习序列化" class="headerlink" title="复习序列化"></a>复习序列化</h2><p>所谓序列化是指，把对象转化为字节流的一种机制。同理，反序列化指的就是把字节流转化为对象。</p>
<p><img src="/images/serializable.jpg" alt="serializable"></p>]]>
    </summary>
    <title>那些有趣的代码(三)--勤俭持家的 ArrayList</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="tomcat" scheme="https://xilidou.com/tags/tomcat/"/>
    <category term="classloader" scheme="https://xilidou.com/tags/classloader/"/>
    <content>
      <![CDATA[<p>看 Tomcat 的源码越看越有趣。Tomcat 的代码总有一种处处都有那么一点调皮的感觉。今天就聊一聊 Tomcat 的类加载机制。</p><p>了解过 JVM 的类加载一定知道，JVM 类加载的双亲委派机制。但是 Tomcat 却打破了 JVM 固有的双亲委派加载机制。</p><span id="more"></span><h2 id="JVM-的类加载"><a href="#JVM-的类加载" class="headerlink" title="JVM 的类加载"></a>JVM 的类加载</h2><p>首先需要明确一下类加载是什么？</p><ul><li>Java虚拟机把描述类的数据从Class文件加载到内存，并对数据进行校验、转换解析和初始化，最终形成可以被虚拟机直接使用的Java类型，这就是虚拟机的加载机制。</li></ul><p>JVM 预定义的三个加载器:</p><ul><li><strong>启动类加载器（Bootstrap ClassLoader）</strong>：是用本地代码实现的类装入器，它负责将 <code>&lt;Java_Runtime_Home&gt;/lib</code>下面的类库加载到内存中（比如<code>rt.jar</code>）。由于引导类加载器涉及到虚拟机本地实现细节，开发者无法直接获取到启动类加载器的引用，所以不允许直接通过引用进行操作。</li><li><strong>标准扩展类加载器（Extension ClassLoader）</strong>：是由 Sun 的 <code>ExtClassLoader（sun.misc.Launcher$ExtClassLoader）</code>实现的。它负责将<code>&lt; Java_Runtime_Home &gt;/lib/ext</code>或者由系统变量 <code>java.ext.dir</code>指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。</li><li><strong>应用程序类加载器（Application ClassLoader）</strong>：是由 Sun 的 <code>AppClassLoader（sun.misc.Launcher$AppClassLoader）</code>实现的。它负责将系统类路径（<code>CLASSPATH</code>）中指定的类库加载到内存中。开发者可以直接使用系统类加载器。</li></ul><p>双亲委派机制：</p><p>所谓双亲委派机制，这里要指出的是，其实双亲委派来源于英文的 ”parents delegate“，仅仅表示的只是”父辈“，可见翻译的人不但英文是半吊子，而且也不了解 JVM 的类加载策略，造成了很大的误解。尤其是这个”双“字在初学的时候给我造成了极大的干扰。所以换个说法，应该是”父辈代理“。</p><p>类加载的时候，把加载的这个动作递归的委托给父辈，由父辈代劳，只有父辈无法加载时，才会由自己加载。</p><p>双亲委派加载模型：</p><p><img data-src="/images/parents.jpg" alt="parents"></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">static</span> <span class="class"><span class="keyword">class</span> <span class="title">ExtClassLoader</span> <span class="keyword">extends</span> <span class="title">URLClassLoader</span></span>&#123;</span><br><span class="line">    ... ...</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">AppClassLoader</span> <span class="keyword">extends</span> <span class="title">URLClassLoader</span></span>&#123;</span><br><span class="line">    ... ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>二者同时继承了 URLClassLoader ，继承关系如下：</p><p><img data-src="/images/Jietu20191027-235532.jpg" alt="appClassLoader"></p><p>怎么实现委托机制呢？在 ClassLoader 里面有几处比较重要的代码：</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">ClassLoader</span> </span>&#123;</span><br><span class="line">      <span class="comment">// The parent class loader for delegation</span></span><br><span class="line">      <span class="comment">// Note: VM hardcoded the offset of this field, thus all new fields</span></span><br><span class="line">      <span class="comment">// must be added *after* it.</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> ClassLoader parent;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> Class&lt;?&gt; loadClass(String name, <span class="keyword">boolean</span> resolve)</span><br><span class="line">          <span class="keyword">throws</span> ClassNotFoundException</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">synchronized</span> (getClassLoadingLock(name)) &#123;</span><br><span class="line">            <span class="comment">// First, check if the class has already been loaded</span></span><br><span class="line">            Class&lt;?&gt; c = findLoadedClass(name);</span><br><span class="line">            <span class="keyword">if</span> (c == <span class="keyword">null</span>) &#123;</span><br><span class="line">                <span class="keyword">long</span> t0 = System.nanoTime();</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="keyword">if</span> (parent != <span class="keyword">null</span>) &#123;</span><br><span class="line">                    <span class="comment">// 尝试使用 父辈的 loadClass 方法</span></span><br><span class="line">                        c = parent.loadClass(name, <span class="keyword">false</span>);</span><br><span class="line">                    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="comment">// 如果没有 父辈的 classLoader 就使用 bootstrap classLoader</span></span><br><span class="line">                        c = findBootstrapClassOrNull(name);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">catch</span> (ClassNotFoundException e) &#123;</span><br><span class="line">                    <span class="comment">// ClassNotFoundException thrown if class not found</span></span><br><span class="line">                    <span class="comment">// from the non-null parent class loader</span></span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                <span class="keyword">if</span> (c == <span class="keyword">null</span>) &#123;</span><br><span class="line">                    <span class="comment">// If still not found, then invoke findClass in order</span></span><br><span class="line">                    <span class="comment">// to find the class.</span></span><br><span class="line">                    <span class="keyword">long</span> t1 = System.nanoTime();</span><br><span class="line">                    <span class="comment">// 父辈没法加载这个 class，就自己尝试加载</span></span><br><span class="line">                    c = findClass(name);</span><br><span class="line">                    <span class="comment">// this is the defining class loader; record the stats</span></span><br><span class="line">                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);</span><br><span class="line">                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);</span><br><span class="line">                    sun.misc.PerfCounter.getFindClasses().increment();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (resolve) &#123;</span><br><span class="line">                resolveClass(c);</span><br><span class="line">            &#125;</span><br><span class="line">                <span class="keyword">return</span> c;</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">// 根据类名 寻找 class。我们在之前我们讲过，不通过的 classLoader 加载的 class 的位置不同。</span></span><br><span class="line">    <span class="keyword">protected</span> Class&lt;?&gt; findClass(String name) <span class="keyword">throws</span> ClassNotFoundException &#123;</span><br><span class="line">        <span class="keyword">return</span> defineClass(name, res);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol><li>首先在初始化 ClassLoader 的时候需要指定自己的 parent 是谁？（这很重要）</li><li>先检查类有没被加载，如果类已经被加载了，直接返回。</li><li>如果没有被加载，则通过 parent 的 loadClass 来尝试加载类。（双亲委派的核心逻辑）</li><li>找不到 parent 的时候使用 bootstrap ClassLoader 进行加载。</li><li>如果委托的 parent 没法加载类，那就自己加载。</li></ol><h2 id="Tomcat-的类加载"><a href="#Tomcat-的类加载" class="headerlink" title="Tomcat 的类加载"></a>Tomcat 的类加载</h2><p>Tomcat 自己实现了自己的类加载器 WebAppClassLoader。类图关系图如下：</p><p><img data-src="/images/Jietu20191027-212824.jpg" alt="WebAppClassLoader"></p><p>我们就来看看 Tomcat 的类加载器是怎么打破双亲委派的机制的。我们先看代码：</p><h3 id="findClass"><a href="#findClass" class="headerlink" title="findClass"></a>findClass</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Class&lt;?&gt; findClass(String name) <span class="keyword">throws</span> ClassNotFoundException &#123;</span><br><span class="line">    <span class="comment">// Ask our superclass to locate this class, if possible</span></span><br><span class="line">    <span class="comment">// (throws ClassNotFoundException if it is not found)</span></span><br><span class="line">    Class&lt;?&gt; clazz = <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 先在自己的 Web 应用目录下查找 class</span></span><br><span class="line">    clazz = findClassInternal(name);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 找不到 在交由父类来处理</span></span><br><span class="line">    <span class="keyword">if</span> ((clazz == <span class="keyword">null</span>) &amp;&amp; hasExternalRepositories) &#123;  </span><br><span class="line">        clazz = <span class="keyword">super</span>.findClass(name);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (clazz == <span class="keyword">null</span>) &#123;</span><br><span class="line">         <span class="keyword">throw</span> <span class="keyword">new</span> ClassNotFoundException(name);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> clazz;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于 Tomcat 的类加载的 findClass 方法:</p><ul><li>首先在 web 目录下查找。（重要）</li><li>找不到再交由父类的 findClass 来处理。</li><li>都找不到，那就抛出 ClassNotFoundException。</li></ul><h3 id="loadClass-方法"><a href="#loadClass-方法" class="headerlink" title="loadClass 方法"></a>loadClass 方法</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Class&lt;?&gt; loadClass(String name, <span class="keyword">boolean</span> resolve) <span class="keyword">throws</span> ClassNotFoundException &#123;</span><br><span class="line">    <span class="keyword">synchronized</span> (getClassLoadingLock(name)) &#123;</span><br><span class="line">        Class&lt;?&gt; clazz = <span class="keyword">null</span>;</span><br><span class="line">        <span class="comment">//1. 先在本地cache查找该类是否已经加载过</span></span><br><span class="line">        clazz = findLoadedClass0(name);</span><br><span class="line">        <span class="keyword">if</span> (clazz != <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (resolve)</span><br><span class="line">                resolveClass(clazz);</span><br><span class="line">            <span class="keyword">return</span> clazz;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//2. 从系统类加载器的cache中查找是否加载过</span></span><br><span class="line">        clazz = findLoadedClass(name);</span><br><span class="line">        <span class="keyword">if</span> (clazz != <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (resolve)</span><br><span class="line">                resolveClass(clazz);</span><br><span class="line">            <span class="keyword">return</span> clazz;</span><br><span class="line">        &#125;</span><br><span class="line">       <span class="comment">// 3. 尝试用ExtClassLoader类加载器类加载</span></span><br><span class="line">        ClassLoader javaseLoader = getJavaseClassLoader();</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            clazz = javaseLoader.loadClass(name);</span><br><span class="line">            <span class="keyword">if</span> (clazz != <span class="keyword">null</span>) &#123;</span><br><span class="line">                <span class="keyword">if</span> (resolve)</span><br><span class="line">                    resolveClass(clazz);</span><br><span class="line">                <span class="keyword">return</span> clazz;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (ClassNotFoundException e) &#123;</span><br><span class="line">            <span class="comment">// Ignore</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 4. 尝试在本地目录搜索class并加载</span></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            clazz = findClass(name);</span><br><span class="line">            <span class="keyword">if</span> (clazz != <span class="keyword">null</span>) &#123;</span><br><span class="line">                <span class="keyword">if</span> (resolve)</span><br><span class="line">                    resolveClass(clazz);</span><br><span class="line">                <span class="keyword">return</span> clazz;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (ClassNotFoundException e) &#123;</span><br><span class="line">            <span class="comment">// Ignore</span></span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// 5. 尝试用系统类加载器(也就是AppClassLoader)来加载</span></span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                clazz = Class.forName(name, <span class="keyword">false</span>, parent);</span><br><span class="line">                <span class="keyword">if</span> (clazz != <span class="keyword">null</span>) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (resolve)</span><br><span class="line">                        resolveClass(clazz);</span><br><span class="line">                    <span class="keyword">return</span> clazz;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; <span class="keyword">catch</span> (ClassNotFoundException e) &#123;</span><br><span class="line">                <span class="comment">// Ignore</span></span><br><span class="line">            &#125;</span><br><span class="line">       &#125;</span><br><span class="line">    <span class="comment">//6. 上述过程都加载失败，抛出异常</span></span><br><span class="line">    <span class="keyword">throw</span> <span class="keyword">new</span> ClassNotFoundException(name);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>总结一下加载的步骤：</p><ol><li>先在本地cache查找该类是否已经加载过，看看 Tomcat 有没有加载过这个类。</li><li>如果Tomcat 没有加载过这个类，则从系统类加载器的cache中查找是否加载过。</li><li>如果没有加载过这个类，尝试用ExtClassLoader类加载器类加载，重点来了，这里并没有首先使用 AppClassLoader 来加载类。这个Tomcat 的 WebAPPClassLoader 违背了双亲委派机制，直接使用了 ExtClassLoader来加载类。这里注意 ExtClassLoader 双亲委派依然有效，ExtClassLoader 就会使用 Bootstrap ClassLoader 来对类进行加载，保证了 Jre 里面的核心类不会被重复加载。 比如在 Web 中加载一个 Object 类。WebAppClassLoader → ExtClassLoader → Bootstrap ClassLoader，这个加载链，就保证了 Object 不会被重复加载。</li><li>如果 BoostrapClassLoader，没有加载成功，就会调用自己的 findClass 方法由自己来对类进行加载，findClass 加载类的地址是自己本 web 应用下的 class。</li><li><strong>加载依然失败，才使用 AppClassLoader 继续加载。</strong></li><li>都没有加载成功的话，抛出异常。</li></ol><p>总结一下以上步骤，WebAppClassLoader 加载类的时候，故意打破了JVM 双亲委派机制，绕开了 AppClassLoader，直接先使用 ExtClassLoader 来加载类。</p><ul><li>保证了基础类不会被同时加载。</li><li>由保证了在同一个 Tomcat 下不同 web 之间的 class 是相互隔离的。</li></ul><h2 id="more"><a href="#more" class="headerlink" title="more"></a>more</h2><p>准备把有趣的代码这个系列慢慢写下去，发现编程的乐趣：</p><p><a href="https://xilidou.com/2019/10/15/tomcat-threadpool/">那些有趣的代码(一)–有点萌的 Tomcat 的线程池</a></p>]]>
    </content>
    <id>https://xilidou.com/2019/10/27/tomcat-classloader/</id>
    <link href="https://xilidou.com/2019/10/27/tomcat-classloader/"/>
    <published>2019-10-27T23:32:30.000Z</published>
    <summary>
      <![CDATA[<p>看 Tomcat 的源码越看越有趣。Tomcat 的代码总有一种处处都有那么一点调皮的感觉。今天就聊一聊 Tomcat 的类加载机制。</p>
<p>了解过 JVM 的类加载一定知道，JVM 类加载的双亲委派机制。但是 Tomcat 却打破了 JVM 固有的双亲委派加载机制。</p>]]>
    </summary>
    <title>那些有趣的代码(二)--偏不听父母话的 Tomcat 类加载器</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="多线程" scheme="https://xilidou.com/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    <category term="线程池" scheme="https://xilidou.com/tags/%E7%BA%BF%E7%A8%8B%E6%B1%A0/"/>
    <content>
      <![CDATA[<p>最近抓紧时间看看了看tomcat 和 jetty 的源代码。发现了一些有趣的代码，这里和大家分享一下。</p><p>Tomcat 作为一个老牌的 servlet 容器，处理多线程肯定得心应手，为了能保证多线程环境下的高效，必然使用了线程池。</p><p>但是，Tomcat 并没有直接使用 j.u.c 里面的线程池，而是对线程池进行了扩展，首先我们回忆一下，j.u.c 中的线程池的几个核心参数是怎么配合的：</p><ol><li>如果当前运行的线程，少于corePoolSize，则创建一个新的线程来执行任务。</li><li>如果运行的线程等于或多于 corePoolSize，将任务加入 BlockingQueue。</li><li>如果 BlockingQueue 内的任务超过上限，<strong>则创建新的线程来处理任务。</strong></li><li>如果创建的线程超出 maximumPoolSize，任务将被拒绝策略拒绝。</li></ol><p>这个时候我们来仔细看看 Tomcat 的代码：</p><p>首先写了一个 TaskQueue 继承了非阻塞无界队列 <code>LinkedBlockingQueue&lt;Runnable&gt;</code> 并重写了的 offer 方法：</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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">offer</span><span class="params">(Runnable o)</span> </span>&#123;</span><br><span class="line">    <span class="comment">//we can&#x27;t do any checks</span></span><br><span class="line">    <span class="keyword">if</span> (parent==<span class="keyword">null</span>) <span class="keyword">return</span> <span class="keyword">super</span>.offer(o);</span><br><span class="line">    <span class="comment">//we are maxed out on threads, simply queue the object</span></span><br><span class="line">    <span class="keyword">if</span> (parent.getPoolSize() == parent.getMaximumPoolSize())&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">super</span>.offer(o);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//we have idle threads, just add it to the queue</span></span><br><span class="line">    <span class="keyword">if</span> (parent.getSubmittedCount()&lt;=(parent.getPoolSize())) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">super</span>.offer(o);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//if we have less threads than maximum force creation of a new thread</span></span><br><span class="line">    <span class="keyword">if</span> (parent.getPoolSize()&lt;parent.getMaximumPoolSize()) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;  </span><br><span class="line">    <span class="comment">//if we reached here, we need to add it to the queue</span></span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">super</span>.offer(o);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在提交任务的时候，增加了几个分支判断。</p><p>首先我们看看 parent 是什么:</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="keyword">private</span> <span class="keyword">transient</span> <span class="keyword">volatile</span> ThreadPoolExecutor parent = <span class="keyword">null</span>;</span><br></pre></td></tr></table></figure><p>这里需要特别注意这里的 ThreadPoolExecutor 并不是 jdk里面的 java.util.concurrent.ThreadPoolExecutor 而是 tomcat 自己实现的。</p><p>我们分别来看 offer 中的几个 if 分支。</p><p>首先我们需要明确一下，<strong>当一个线程池需要调用阻塞队列的 offer 的时候，说明线程池的核心线程数已经被占满了。（记住这个前提非常重要）</strong></p><p>要理解下面的代码，首先需要复习一下线程池的 getPoolSize() 获取的是什么？我们看源码：</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Returns the current number of threads in the pool.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> the number of threads</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">getPoolSize</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">final</span> ReentrantLock mainLock = <span class="keyword">this</span>.mainLock;</span><br><span class="line">    mainLock.lock();</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">// Remove rare and surprising possibility of</span></span><br><span class="line">        <span class="comment">// isTerminated() &amp;&amp; getPoolSize() &gt; 0</span></span><br><span class="line">        <span class="keyword">return</span> runStateAtLeast(ctl.get(), TIDYING) ? <span class="number">0</span></span><br><span class="line">            : workers.size();</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        mainLock.unlock();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>需要注意的是，workers.size()  <strong>包含了 coreSize 的核心线程和临时创建的小于 maxSize 的临时线程。</strong></p><p>先看第一个 if</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="comment">// 如果线程池的工作线程数等于 线程池的最大线程数，这个时候没有工作线程了，就尝试加入到阻塞队列中</span></span><br><span class="line"><span class="keyword">if</span> (parent.getPoolSize() == parent.getMaximumPoolSize())&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">super</span>.offer(o);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>经过第一个 if 之后，线程数必然在核心线程数和最大线程数之间。</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> (parent.getSubmittedCount()&lt;=(parent.getPoolSize())) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">super</span>.offer(o);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>对于 parent.getSubiitedCount() ,我们要先搞清楚 submiitedCount 是什么</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="comment">/**</span></span><br><span class="line"><span class="comment"> * The number of tasks submitted but not yet finished. This includes tasks</span></span><br><span class="line"><span class="comment"> * in the queue and tasks that have been handed to a worker thread but the</span></span><br><span class="line"><span class="comment"> * latter did not start executing the task yet.</span></span><br><span class="line"><span class="comment"> * This number is always greater or equal to &#123;<span class="doctag">@link</span> #getActiveCount()&#125;.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> AtomicInteger submittedCount = <span class="keyword">new</span> AtomicInteger(<span class="number">0</span>);</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><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//if we have less threads than maximum force creation of a new thread</span></span><br><span class="line"><span class="keyword">if</span> (parent.getPoolSize()&lt;parent.getMaximumPoolSize()) &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>由于上一个 if 条件的存在，走到这个 if 条件的时候，提交的线程数已经大于核心线程数了，且没有空闲线程，所以返回一个 false 标明，表示任务添加到阻塞队列失败。线程池就会认为阻塞队列已经无法继续添加任务到队列中了，根据默认线程池的工作逻辑，线程池就会创建新的线程直到最大线程数。</p><p>回忆一下 jdk 默认线程池的实现，如果阻塞队列是无界的，任务会无限的添加到无界的阻塞队列中，线程池就无法利用核心线程数和最大线程数之间的线程数了。</p><p>Tomcat 的实现就是为了，线程池即使核心线程数满了以后，且使用无界队列的时候，线程池依然有机会创建新的线程，直到达到线程池的最大线程数。</p><p>Tomcat 对线程池的优化并没结束，Tomcat 还重写了线程池的 execute 方法：</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">execute</span><span class="params">(Runnable command, <span class="keyword">long</span> timeout, TimeUnit unit)</span> </span>&#123;</span><br><span class="line">    <span class="comment">//提交任务数加一</span></span><br><span class="line">    submittedCount.incrementAndGet();</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="keyword">super</span>.execute(command);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (RejectedExecutionException rx) &#123;</span><br><span class="line">        <span class="comment">// 被拒绝以后尝试，再次向阻塞队列中提交任务</span></span><br><span class="line">        <span class="keyword">if</span> (<span class="keyword">super</span>.getQueue() <span class="keyword">instanceof</span> TaskQueue) &#123;</span><br><span class="line">            <span class="keyword">final</span> TaskQueue queue = (TaskQueue)<span class="keyword">super</span>.getQueue();</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> (!queue.force(command, timeout, unit)) &#123;</span><br><span class="line">                submittedCount.decrementAndGet();</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> RejectedExecutionException(sm.getString(<span class="string">&quot;threadPoolExecutor.queueFull&quot;</span>));</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (InterruptedException x) &#123;</span><br><span class="line">            submittedCount.decrementAndGet();</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> RejectedExecutionException(x);</span><br><span class="line">        &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            submittedCount.decrementAndGet();</span><br><span class="line">            <span class="keyword">throw</span> rx;</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>终于到整篇文章的萌点了，就是提交线程的时候，如果被线程池拒绝了，Tomcat 的线程池，还会厚着脸皮再次尝试，调用 force() 方法”强行”的尝试向阻塞队列中添加任务。</p><p><img data-src="/images/tomcat.png" alt="tomcat"></p><p>在群里和朋友讲完 Tomcat 线程池的实现，帆哥给了一个特别厉害的例子。</p><p>总结一下：</p><p>Tomcat 线程池的逻辑：</p><ol><li>如果当前运行的线程，少于corePoolSize，则创建一个新的线程来执行任务。</li><li>如果线程数大于 corePoolSize了，Tomcat 的线程不会直接把线程加入到无界的阻塞队列中，而是去判断，submittedCount（已经提交线程数）是否等于 maximumPoolSize。</li><li>如果等于，表示线程池已经满负荷运行，不能再创建线程了，直接把线程提交到队列，</li><li>如果不等于，则需要判断，是否有空闲线程可以消费。</li><li>如果有空闲线程则加入到阻塞队列中，等待空闲线程消费。</li><li>如果没有空闲线程，尝试创建新的线程。<strong>（这一步保证了使用无界队列，仍然可以利用线程的 maximumPoolSize）。</strong></li><li>如果总线程数达到 maximumPoolSize，则继续尝试把线程加入 BlockingQueue 中。</li><li>如果 BlockingQueue 达到上限（假如设置了上限），被默认线程池启动拒绝策略，tomcat 线程池会 catch 住拒绝策略抛出的异常，再次把尝试任务加入中 BlockingQueue 中。</li><li>再次加入失败，启动拒绝策略。</li></ol><p>如此努力的 Tomcat 线程池，有点萌啊。</p>]]>
    </content>
    <id>https://xilidou.com/2019/10/15/tomcat-threadpool/</id>
    <link href="https://xilidou.com/2019/10/15/tomcat-threadpool/"/>
    <published>2019-10-15T00:39:17.000Z</published>
    <summary>
      <![CDATA[<p>最近抓紧时间看看了看tomcat 和 jetty 的源代码。发现了一些有趣的代码，这里和大家分享一下。</p>
<p>Tomcat 作为一个老牌的 servlet 容器，处理多线程肯定得心应手，为了能保证多线程环境下的高效，必然使用了线程池。</p>
<p>但是，Tomcat 并没有直接使用 j.u.c 里面的线程池，而是对线程池进行了扩展，首先我们回忆一下，j.u.c 中的线程池的几个核心参数是怎么配合的：</p>
<ol>
<li>如果当前运行的线程，少于corePoolSize，则创建一个新的线程来执行任务。</li>
<li>如果运行的线程等于或多于 corePoolSize，将任务加入 BlockingQueue。</li>
<li>如果 BlockingQueue 内的任务超过上限，<strong>则创建新的线程来处理任务。</strong></li>
<li>如果创建的线程超出 maximumPoolSize，任务将被拒绝策略拒绝。</li>
</ol>
<p>这个时候我们来仔细看看 Tomcat 的代码：</p>
<p>首先写了一个 TaskQueue 继承了非阻塞无界队列 <code>LinkedBlockingQueue&lt;Runnable&gt;</code> 并重写了的 offer 方法：</p>]]>
    </summary>
    <title>那些有趣的代码(一)--有点萌的 Tomcat 的线程池</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="随笔" scheme="https://xilidou.com/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="杂谈，知乎" scheme="https://xilidou.com/tags/%E6%9D%82%E8%B0%88%EF%BC%8C%E7%9F%A5%E4%B9%8E/"/>
    <content>
      <![CDATA[<p>恭喜知乎 F 轮融资成功，今天不谈技术，谈谈别的。</p><p><img data-src="/images/lks.jpg" alt="刘看山"></p><span id="more"></span><h2 id="从需求第三定律谈起"><a href="#从需求第三定律谈起" class="headerlink" title="从需求第三定律谈起"></a>从需求第三定律谈起</h2><p>最近一直在“得到”上学习《薛兆丰的经济学课》，其中一节讲到了需求第三定律。</p><blockquote><p>每当消费者必须支付一笔附加费用的时候，高品质的产品就变得便宜了，这笔附加费用越高，高品质的的产品就就变得越便宜，也叫”好东西运到远方定律“。</p></blockquote><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><p>随着社区逐渐的开放，用户可以自由注册以后，门槛降低，但是初期形成的”精英气质“，还是要求回答者，需要用较高的成本维护自己的精英属性，才能获得较高的认同。所以社区内容的贡献者会尽量的产出优秀内容，来满足”精英社区“对答题者的人设要求，可以说就是回答需要付出额外的成本。所以这个时候看来，平台的总体回答质量还不错。</p><p>直至最近的下沉，2.2 亿用户的涌入。一方面，社区的精英气质，逐渐消散，用户维护自己精英人设，变得不如之前那么迫切，降低了回答者自我要求的门槛。另一方面，平台对于 DAU（每日活跃用户） 的渴望，吸引用户注册知乎，然后主动引导用户回答问题，以降低回答的成本和门槛。这个时候第三需求定律发挥了它的威力。回答的附加成本下降，不需要对内容进行筛选，低质量回答的数量必然增加，从用户的感受上来说，自然觉得总体上知乎的社区的回答质量下降了。</p><h2 id="但附加成本上升未必是好事"><a href="#但附加成本上升未必是好事" class="headerlink" title="但附加成本上升未必是好事"></a>但附加成本上升未必是好事</h2><p>很多人抱怨，由于回答的成本和门槛降低，造成了知乎的平均水平下降，确实如此。早期知乎的一系列门槛的存在，只有优秀的人才能回答问题，所以早期的知乎社区的平均水平就很高。</p><p>但是，知乎的平均水平下降了是不是坏事？不见得坏，这个问题分开来看，从平均水平来看，确实不如从前，优秀信息的浓度下降，造成用户的筛选优质信息成本高，提高了使用成本。这是坏事。</p><p>注意，我们一直强调的是平均，是总体。并没有讨论优质答案的绝对值。从另一个角度来看，由于网络外部性的存在，更多的用户使用知乎，就会吸引更多的优秀回答者为平台带来优秀回答，平台上优秀的回答的绝对值会增长。同时为信息的获取者对某一个问题提供更多一种的选择和视角。这个是好事。</p><p>站在更高的角度来看，人类的发展历史，就是不断的减少信息存储和流通成本的历史。信息的附加成本就是在不断的下降。</p><p>所以面对更多、相对更稀薄的优质信息还是更浓的更少的优质信息,你怎么选择？</p><p>之后我还会谈谈我对如何筛选信息的理解，敬请大家期待。</p><p>利益相关：知乎员工</p>]]>
    </content>
    <id>https://xilidou.com/2019/08/13/zhihu/</id>
    <link href="https://xilidou.com/2019/08/13/zhihu/"/>
    <published>2019-08-13T22:57:41.000Z</published>
    <summary>
      <![CDATA[<p>恭喜知乎 F 轮融资成功，今天不谈技术，谈谈别的。</p>
<p><img src="/images/lks.jpg" alt="刘看山"></p>]]>
    </summary>
    <title>从需求第三定律说起--为什么知乎的回答质量下降了</title>
    <updated>2026-08-08T02:04:49.447Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="权限" scheme="https://xilidou.com/tags/%E6%9D%83%E9%99%90/"/>
    <content>
      <![CDATA[<p><img data-src="/images/alex-pudov-588871-unsplash%202.jpg?x-oss-process=image/resize,p_40" alt="keepout"></p><p>我们的业务系统使用了一段时间后，用户的角色类型越来越多，这时候不同类型的用户可以使用不同功能，看见不同数据的需求就变得越来越迫切。<br>如何设计一个可扩展,且易于接入的权限系统.就显得相当重要了。结合之前我实现的的权限系统，今天就来和大家探讨一下我对权限系统的理解。</p><p>这篇文章会从权限系统业务设计，技术架构，关键代码几个方面，详细的阐述权限系统的实现。</p><span id="more"></span><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>权限系统是一个系统的基础功能，但是作为创业公司，秉承着快比完美更重要原则，老系统的权限系统都是硬编码在代码或者写在到配置文件中的。随着业务的发展，如此简陋的权限系统就显得捉襟见肘了。开发一套新的，强大的权限系统就提上了日程。</p><p>这里有两个重点：</p><ul><li>业务系统已经运行一段时间积累了可观的代码和接口了，新的权限系统权在设计之初的一个要求就是，尽量减少权限系统对原有业务代码的入侵。（为了达成这个目的，我们会大量的使用 spring、springboot、jpa  以及 hibernate  的高级特性）</li><li>系统要易于使用，可以由业务方自行进行配置。</li></ul><h2 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h2><p>权限系统需要支持功能权限和数据权限。</p><h3 id="功能权限"><a href="#功能权限" class="headerlink" title="功能权限"></a>功能权限</h3><p>所谓功能权限，就是指，拥有某种角色的用户，只能看到某些功能，并使用它。实现功能权限就简化为：</p><ul><li>页面元素如何根据不同用户进行渲染</li><li>API 的访问权限如何根据不同的用户进行管理</li></ul><h3 id="数据权限"><a href="#数据权限" class="headerlink" title="数据权限"></a>数据权限</h3><p>所谓数据权限是指，数据是隔离的，用户能看到的数据，是经过控制的，用户只能看到拥有权限的某些数据。</p><p>比如，某个地区的 leader 可以查看并操作这个地区的所有员工负责的订单数据，但是员工就只能操作和查看自己负责的的订单数据。</p><p>对于数据权限，我们需要考虑的问题就抽象为，</p><ol><li>数据的归属问题：数据产生以后归属于谁？</li><li>确定了数据的归属，根据某些配置，就能确定谁可以查看归属于谁的数据。</li></ol><h2 id="业务设计"><a href="#业务设计" class="headerlink" title="业务设计"></a>业务设计</h2><p>经过上面的分析，我们可以抽象出以下几个实体：</p><h3 id="功能权限-1"><a href="#功能权限-1" class="headerlink" title="功能权限"></a>功能权限</h3><ul><li>用户</li><li>角色</li><li>功能</li><li>页面元素</li><li>API 信息</li></ul><p>我们知道，对于一某个功能来说，它是由若干的前端元素和后端 API 组成的。</p><p>比如“合同审核” 这个功能就包括了，“查看按钮”、“审核按钮” 等前端元素。</p><p>涉及的 api 就可能包含了 <code>contract</code> 的 <code>get</code> 和 <code>patch</code> 两个 Restful 风格的接口。</p><p>抽象出来就是：在权限系统中若干前端元素和后端 API 组成了一个功能。</p><p>具体的关系，就是如下图：</p><p><img data-src="/images/permissin-er.png" alt="permission-er"></p><h3 id="数据权限-1"><a href="#数据权限-1" class="headerlink" title="数据权限"></a>数据权限</h3><p>具体每个系统的数据权限的实现有所不同，我们这里实现的数据权限是依赖于公司的组织架构实现的，所有涉及到的实体如下：</p><ul><li>用户</li><li>数据权限关系</li><li>部门</li><li>数据拥有者</li><li>具体数据（订单，合同）</li></ul><p>这里需要说明一下，要接入数据权限，首先需要梳理数据的归属问题，数据归属于谁？或者准确的来说，数据属于哪个数据拥有者，这个数据拥有者属于哪个部门。通过这个关联关系我们就可以明确，这个数据属于哪个部门。</p><p>对于数据的使用用户，来说，就需要查询，这个用户可以查看某个模块的某个部门的数据。</p><p>这里需要说明的是，不同的系统的数据权限需要具体分析，我们系统的数据权限是建立在公司的组织架构上的。</p><p>本质就是：</p><ul><li>数据归属于某个数据拥有者</li><li>用户能够看到该数据拥有者的数据</li></ul><p>具体的关系图如下：</p><p><img data-src="/images/datepermission.png" alt="date-permission"></p><p>注意，实际上用户和数据拥有者都是同一个实体 User 表示，只是为了表述方便进行了区分。</p><h2 id="实现的技术难点"><a href="#实现的技术难点" class="headerlink" title="实现的技术难点"></a>实现的技术难点</h2><h3 id="Mysql-中树的储存"><a href="#Mysql-中树的储存" class="headerlink" title="Mysql 中树的储存"></a>Mysql 中树的储存</h3><p>可以看出来，我们的功能和组织架构都是典型的树形结构。</p><p>我们最常见的场景如下</p><ul><li>查询某个功能，及其所有子功能。</li><li>查询某个部门，及其所有子部门的所属员工。</li></ul><p>抽象以后就是查询树的某个节点，和他的所有子节点。</p><p>为了便于查询，我们可以增加两个冗余字段，一个是 <code>parent_id</code> ,还有一个是 <code>path</code>。</p><ul><li>parent_id 很好理解，就是父节点的 id；</li><li>path 指的是，这个节点，路径上的 id 的。使用’.’进行分隔的一个字符串。 比如</li></ul><figure class="highlight shell"><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">    A</span><br><span class="line">   / \</span><br><span class="line">  B   C</span><br><span class="line"> /\   /\</span><br><span class="line">D  E F  G</span><br><span class="line">        /\</span><br><span class="line">       H  I</span><br></pre></td></tr></table></figure><p>对于 D 的 path 就是 <code>(A.id).(B.id).</code> 这要的好处的就是通过 <code>sql</code> 的 <code>like</code> 的语句就能快速的查询出某个节点的子节点。</p><p>比如要获取节点 C 的所有子节点:</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 * from user where path like (A.id).(C.id).%</span><br></pre></td></tr></table></figure><p>一次查询可以获取所有子节点，是一种查询友好的设计。如果需要我们可以为 <code>path</code> 字段增加索引，根据索引的左值定律，这样的 like 查询是可以走索引的。提升查询效率。</p><h3 id="快速的自动的获取-API-信息"><a href="#快速的自动的获取-API-信息" class="headerlink" title="快速的自动的获取 API 信息"></a>快速的自动的获取 API 信息</h3><p>我们知道 <code>Spirng mvc</code> 在启动的时候会扫描被 <code>@RequestMapping</code> 注解标记的方法，并把数据放在 <code>RequestMappingHandlerMapping</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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Componet</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ApiScanSerivce</span></span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autoired</span></span><br><span class="line">    <span class="keyword">private</span> RequestMappingHandlerMapping requestMapping;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@PostConstruct</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">update</span><span class="params">()</span></span>&#123;</span><br><span class="line"></span><br><span class="line">        Map&lt;RequestMappingInfo,HandlerMethed&gt; handlerMethods = requestMapping.getHandlerMethods();</span><br><span class="line">        <span class="keyword">for</span>(Map.Entry RequestMappinInfo,HandlerMethod) entry: handlerMethods.entrySet()&#123;</span><br><span class="line">            <span class="comment">// 处理 API 上传的相关逻辑</span></span><br><span class="line">            updateApiInfo();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>获取项目的所有 http 接口。这样我们就可以遍历处理项目的接口数据。</p><h3 id="描述一个-API"><a href="#描述一个-API" class="headerlink" title="描述一个 API"></a>描述一个 API</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="class"><span class="keyword">class</span> <span class="title">ApiInfo</span></span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="keyword">private</span> String uri; <span class="comment">// api 的 uri</span></span><br><span class="line">    <span class="keyword">private</span> String method; <span class="comment">//请求的 method：eg： get、 post、 patch。</span></span><br><span class="line">    <span class="keyword">private</span> String project; <span class="comment">// 这组 api 属于哪一个 web 工程。</span></span><br><span class="line">    <span class="keyword">private</span> String signature; <span class="comment">//方法的签名</span></span><br><span class="line">    <span class="keyword">private</span> Intger status; <span class="comment">// api 状态</span></span><br><span class="line">    <span class="keyword">private</span> Intger whiteList; <span class="comment">// 是否是白名单 api 如果是就不需过滤</span></span><br><span class="line"></span><br><span class="line">&#125;</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">signature = className + <span class="string">&quot;#&quot;</span> + methodName +<span class="string">&quot;(&quot;</span> + parameterTypeList+<span class="string">&quot;)&quot;</span></span><br></pre></td></tr></table></figure><h3 id="用户的权限数据"><a href="#用户的权限数据" class="headerlink" title="用户的权限数据"></a>用户的权限数据</h3><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@ToString</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserPermisson</span></span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//用户可以看到的前端元素的列表</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;Long&gt; pageElementIdList;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//用户可以使用的 API 列表</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;String&gt; apiSignatureList;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//用户不同模块的数据权限 的 map。map 的 key 是模块名称，value 是这个能够看到数据属于那些用户的列表</span></span><br><span class="line">    <span class="keyword">private</span> Map&lt;String,List&lt;Long&gt;&gt; dataAccessMap；</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="利用-Spring-特性实现功能权限"><a href="#利用-Spring-特性实现功能权限" class="headerlink" title="利用 Spring 特性实现功能权限"></a>利用 Spring 特性实现功能权限</h3><p>对于如何使用 Spring 实现方法拦截，很自然的就像到了使用拦截器来实现。考虑到我们这个权限的组件是一个通用组件，所以就可以写一个抽象类，暴露出<code>getUid(HttpServletRequest requset)</code> 用户获取使用系统的 <code>userId</code>,以及 <code>onPermission(String msg)</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><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">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">PermissonAbstractInterceptor</span> <span class="keyword">extends</span> <span class="title">HandlerInterceptorAdapter</span></span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> abstarct <span class="keyword">long</span> <span class="title">getUid</span><span class="params">(HttpServletRequest requset)</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">abstract</span> <span class="title">onPermession</span><span class="params">(String str)</span> <span class="keyword">throws</span> Exception</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">preHandler</span><span class="params">(HttpServletRequest request,HttoServletResponse respponse,Object handler)</span> <span class="keyword">throws</span> Excption</span>&#123;</span><br><span class="line">        <span class="comment">// 获取用户的 uid</span></span><br><span class="line">        <span class="keyword">long</span> uid = getUid(request);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 根据用户 获取用户相关的 权限对象</span></span><br><span class="line">        UserPermisson userPermission = getUserPermissonByUid(uid);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span>(inandler <span class="keyword">instanceof</span> HanderMethod)&#123;</span><br><span class="line">            <span class="comment">//获取请求方的签名</span></span><br><span class="line">            String methodSignerture = getMethodSignerture(handler);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span>(!userPermisson.getApiSignatureList().contains(methodSignerture))&#123;</span><br><span class="line"></span><br><span class="line">                onPermession(<span class="string">&quot;该用户没有权限&quot;</span>);</span><br><span class="line"></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></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>以上的代码只是提供一个思路。不是真实的代码实现。</p><p>所以接入方就只需要继承这个抽象方法，并实现对应的方法，如果你使用的是 Springboot 的，只需要把实现的拦截器注册到拦截器里面就可以使用了：</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="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MyWebAppConfigurer</span> <span class="keyword">extends</span> <span class="title">WebMvcConfigurerAdapter</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addInterceptors</span><span class="params">(InterceptorRegistry registry)</span> </span>&#123;</span><br><span class="line">        registry.addInterceptor(permissionInterceptor);</span><br><span class="line">        <span class="keyword">super</span>.addInterceptors(registry);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="利用-Hibrenate-特性实现数据权限"><a href="#利用-Hibrenate-特性实现数据权限" class="headerlink" title="利用 Hibrenate 特性实现数据权限"></a>利用 Hibrenate 特性实现数据权限</h3><p>通过上面的代码可以看出来，功能权限的实现，基本做到了没有侵入代码。对于数据权限的实现的原则还是尽量少的减少代码的入侵。</p><p>我们默认代码使用 Java 经典的 Controller、Service、Dao 三层架构。 主要使用的技术 Spring Aop、Jpa 的 filter，基本的实现思路如下图：</p><p><img data-src="/images/jpa-filter.png" alt="date permission"></p><p>基本的思路如下：</p><ol><li>用户登录以后，获取用户的数据权限相关信息。</li><li>把相关信息权限系统放入 ThreadLocal 中。</li><li>在 Dao 层中，从 ThreadLocal 中获取权限相关的权限数据。</li><li>在 filter 中填充权限相关数据。</li><li>从 Hibernate 上下文中取出 Session。</li><li>在 Session 上添加相关 filter。</li></ol><p>通过图片我们可以看出，我们基本不需要对 Controller、Service、Dao 进行修改，只需要按需实现对应模块的 filter。</p><p>看到这里你可能觉得”嚯~~”,还有这种操作？我们就看看代码是怎么具体实现的吧。</p><ol><li>首先需要在 Entity 上写一个 Filter,假设我们写的是订单模块。</li></ol><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="meta">@Entity</span></span><br><span class="line"><span class="meta">@Table(name = &quot;order&quot;)</span></span><br><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@ToString</span></span><br><span class="line"><span class="meta">@FilterDef(name = &quot;orderOwnerFilter&quot;, parameters = &#123;@ParamDef name= &quot;ownerIds&quot;,type = &quot;long&quot;&#125;)</span></span><br><span class="line"><span class="meta">@Filters(&#123;@Filter name= &quot;orderOwnerFiler&quot;, condition = &quot;ownder in (:ownerIds)&quot;&#125;)</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">order</span></span>&#123;</span><br><span class="line">    <span class="keyword">private</span> Long id;</span><br><span class="line">    <span class="keyword">private</span> Long ownerId;</span><br><span class="line">    <span class="comment">//其他参数省略</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="2"><li>写个注解</li></ol><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">@Retention(RetentinPolicy.RUNTIME)</span></span><br><span class="line"><span class="meta">@Taget(ElementType.METHOD)</span></span><br><span class="line"><span class="keyword">public</span> <span class="meta">@interface</span> OrderFilter&#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ol start="3"><li>编写一个切面用于处理 Session、datePermission、和 Filter</li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Aspect</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OrderFilterAdvice</span></span>&#123;</span><br><span class="line">    <span class="meta">@PersistenceContext</span></span><br><span class="line">    <span class="keyword">private</span> EntityManager entityManager;</span><br><span class="line">    <span class="meta">@Around(&quot;annotation(OrderFilter)&quot;)</span></span><br><span class="line">    <span class="function">pblict Object <span class="title">doProcess</span> <span class="params">(ProceedingJoinPoint joinPonit)</span> <span class="keyword">throws</span> ThrowableP</span>&#123;</span><br><span class="line">        <span class="keyword">try</span>&#123;</span><br><span class="line">            <span class="comment">//从上下文里面获取 owerId，这个 Id 在 web 中就已经存好了</span></span><br><span class="line">            List&lt;Long&gt; ownerIds = getListFromThreadLocal();</span><br><span class="line">            <span class="comment">//获取查询中的 session</span></span><br><span class="line">            Session session = entityManager.unwrap(Session.class);</span><br><span class="line">            <span class="comment">// 在 session 中加入 filter</span></span><br><span class="line">            Filter filter = unwrap.enableFilter(<span class="string">&quot;orderOwnerFilter&quot;</span>);</span><br><span class="line">            <span class="comment">// filter 中加入数据</span></span><br><span class="line">            filter.setParameterList(<span class="string">&quot;ownerIds&quot;</span>，ownerIds)</span><br><span class="line">            <span class="comment">//执行 被拦截的方法</span></span><br><span class="line">            <span class="keyword">return</span> join.proceed();</span><br><span class="line">        &#125;<span class="keyword">catch</span>(Throwable e)&#123;</span><br><span class="line">            log.error();</span><br><span class="line">        &#125;<span class="keyword">finally</span>&#123;</span><br><span class="line">            <span class="comment">// 最后 disable filter</span></span><br><span class="line">           entityManager.unwrap(Session.class).disbaleFilter(<span class="string">&quot;orderOwnerFilter&quot;</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><pre><code>这个拦截器，拦截被打了 `@OrderFilter` 的方法。</code></pre><h3 id="易于接入"><a href="#易于接入" class="headerlink" title="易于接入"></a>易于接入</h3><p>为了方便接入项目，我们可以将涉及到的整套代码封装为一个 <code>springboot-starter</code> 这样使用者只需要引入对应的 starter 就能够接入权限系统。  </p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>权限系统随着业务的发展，是从可以没有逐渐变成为非常重要的模块。往往需要接入权限系统的时候，系统已经成熟的运行了一段时间了。大量的接口，负责的业务，为权限系统的接入提高了难度。同时权限系统又是看似通用，但是定制的点又不少的系统。</p><p>设计套权限系统的初衷就是，不需要大量修改代码，业务方就可方便简单的接入。<br>具体实现代码的时候，我们充分利用了面向切面的编程思想。同时大量的使用了 <code>Spring</code>、<code>Hibrenate</code>框架的高级特性，保证的代码的灵活，以及横向扩展的能力。</p><p>看完文章如果你发现有疑问，或者更好的实现方法，欢迎留言与我讨论。</p>]]>
    </content>
    <id>https://xilidou.com/2019/05/11/permisson/</id>
    <link href="https://xilidou.com/2019/05/11/permisson/"/>
    <published>2019-05-11T21:23:41.000Z</published>
    <summary>
      <![CDATA[<p><img src="/images/alex-pudov-588871-unsplash%202.jpg?x-oss-process=image/resize,p_40" alt="keepout"></p>
<p>我们的业务系统使用了一段时间后，用户的角色类型越来越多，这时候不同类型的用户可以使用不同功能，看见不同数据的需求就变得越来越迫切。<br>如何设计一个可扩展,且易于接入的权限系统.就显得相当重要了。结合之前我实现的的权限系统，今天就来和大家探讨一下我对权限系统的理解。</p>
<p>这篇文章会从权限系统业务设计，技术架构，关键代码几个方面，详细的阐述权限系统的实现。</p>]]>
    </summary>
    <title>如何利用 Spring Hibernate 高级特性设计实现一个权限系统</title>
    <updated>2026-08-08T02:04:49.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="工具" scheme="https://xilidou.com/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="tools" scheme="https://xilidou.com/tags/tools/"/>
    <category term="nodejs" scheme="https://xilidou.com/tags/nodejs/"/>
    <content>
      <![CDATA[<p>居然有人忘记吃饭？？？</p><p>为了解决这个问题，我写了一个微信机器人到点就提醒他吃饭。</p><span id="more"></span><p><a href="https://github.com/diaozxin007/remindEat">Github 地址</a></p><p>使用方法</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git clone https://github.com/diaozxin007/remindEat</span><br></pre></td></tr></table></figure><p>修改 config&#x2F;default.json 里面的 ‘toName’ 为要提醒人的备注名称。</p><figure class="highlight shell"><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">cd remindEat</span><br><span class="line">npm install</span><br></pre></td></tr></table></figure><p><code>wechaty</code> 使用了无头浏览器，安装的过程中会到 google 下载 chromium。如果遇到下载不成功的错误。可以尝试</p><figure class="highlight shell"><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">export PUPPETEER_DOWNLOAD_HOST=https://storage.googleapis.com.cnpmjs.org</span><br><span class="line">npm install</span><br></pre></td></tr></table></figure><p>编译完成后：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">node remindEat.js</span><br></pre></td></tr></table></figure><p>如果在 <code>ubuntu</code> 上启动报错缺少包，可以参考 <a href="https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md">puppeteer&#x2F;docs&#x2F;troubleshooting.md</a></p><p>到时候对方应该不会忘记吃饭了。</p><p>实现原理：</p><p>这个机器人主要使用两个库：</p><ul><li><a href="https://www.npmjs.com/package/wechaty">wechaty</a> 一个 node 实现的微信机器人。</li><li><a href="https://www.npmjs.com/package/node-schedule">node-schedule</a> 一个定时任务触发器。</li></ul><p>其实核心的原理，就在 wechaty 登录以后，注册了一个定时任务。这个定时任务，用于在饭点的时候，注册另外一个 schedule ，同时这个 schedule 是为了实现每分钟一次的提示。</p><p>当对方按照指定的话术服务短信的时候，我们只需要调用每分钟提醒一次的 schedule cancel() 方法。</p><p>希望每一个人都能按时吃饭，谢谢大家。</p>]]>
    </content>
    <id>https://xilidou.com/2019/05/07/wx-bot/</id>
    <link href="https://xilidou.com/2019/05/07/wx-bot/"/>
    <published>2019-05-07T23:56:33.000Z</published>
    <summary>
      <![CDATA[<p>居然有人忘记吃饭？？？</p>
<p>为了解决这个问题，我写了一个微信机器人到点就提醒他吃饭。</p>]]>
    </summary>
    <title>居然有人能忘记吃饭？写个微信机器人提醒他</title>
    <updated>2026-08-08T02:04:49.447Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="随笔" scheme="https://xilidou.com/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="总结" scheme="https://xilidou.com/tags/%E6%80%BB%E7%BB%93/"/>
    <content>
      <![CDATA[<p><img data-src="/images/2019-04-25-022243.jpg" alt="winter"></p><p>2018年结束了，这一年成长是的一年。</p><span id="more"></span><h1 id="目标回顾："><a href="#目标回顾：" class="headerlink" title="目标回顾："></a>目标回顾：</h1><p>2017年底给自己定了几个目标：</p><ul><li><p>买房，希望新的一年在北京站稳脚跟。(1&#x2F;1)</p></li><li><p>晋级，向T6进发。(入职新公司，给了资深 title，1&#x2F;1)</p></li><li><p>学习，新的一年着重应该聚焦两个相关点吧，一个是自己的老本行，更加深入的研究分布式系统。还有就是重启AI相关的学习。（确实研究了不少分布式的知识，AI 还是没有开始 2&#x2F;1）</p></li><li><p>博客，每个月应该会有两篇文章。保证一年24篇文章。（博客一共更新18篇文章 18&#x2F;24）</p></li><li><p>读书，每个月应该完成一本书（4&#x2F;12）。</p></li></ul><p>总体来说对于目标的完成程度给自己今年目标的完成打个 70 分吧。主要的欠缺还是读书的本数和 AI 的学习。</p><h1 id="工作"><a href="#工作" class="headerlink" title="工作"></a>工作</h1><p>离开了老东家，入职了知乎。从原来的招聘业务，切换到了商业变现业务。对业务的积累归零，重新开始，对我来说也是不小的挑战。从 CPM，CPC 开始学习广告知识。了解了广告，创意，素材，排期，订单，合同，刊例，库存等等的概念。</p><p>说到工作，就不得不谈谈。年底的互联网寒冬，公司迎来了“优化”。同事，早上还在愉快的写代码，中午谈话，下午回收账号，连交接的邮件都来不及发出来，一天之内再也和公司没有任何关系，真是无情而残酷。震撼与庆幸之余，不得不拷问自己，如何能够时刻保持自己的竞争力？我想只能是做一个持续学习者，终生学习者。保有随时具有失去工作的危机感,才能在这种每天都在快速变化的环境中存活。</p><h1 id="学习"><a href="#学习" class="headerlink" title="学习"></a>学习</h1><p>今年，持续的输出了很多文章，虽然没有达到年前的目标 24 篇文章但是，输出的 18 篇，文章质量我还是比较满意的。</p><ul><li><p>深入的从源码级别了解了 Redis 的设计和实现，阅读了《Redis设计与实现》，并结合 Reids 的源码，了解了 Redis 的 底层数据结构，了解了 Redis 是如何使用合理的数据结构，平衡时间复杂度和空间复杂度。同时，还学习了 Redis 如何使用 Reactor 模型，基于 epoll 实现了 NIO ，提高 IO 的利用率。这一系列关于 Redis 的学习，从数据结构和 IO 两方面提升了自己的水平。</p></li><li><p>通过一年学习总结，摸索了一套如何有效阅读源码的思路：借助资料（图书，博客）-&gt; 源码走读思考 -&gt; debug 调试 -&gt; 基于思想简化细节，造轮子。基于这一套方法论，学习了 Spring，Hystrix（部分），dubbo（部分） 的源码，产出了“徒手撸框架”系列文章。</p></li><li><p>其实下半年还花时间，进行了一些方法论的学习。关于方法论是否有效会在下文进行阐述。</p></li></ul><h1 id="生活"><a href="#生活" class="headerlink" title="生活"></a>生活</h1><p>今年生活上最大的事情就是在北京买了房子，选房时候的纠结和艰险不表，终于可以有自己的家了。至于买车？啥时候摇上号再说吧。生活进入正轨之后，更多的还是平淡，日常和琐碎。</p><p>通过年底的装修，突然发现，现金流的重要性。月光肯定是不行的，手上有现金，才能面对大额的支出。</p><p>装修是一项及其繁琐和持久的工程，需要考虑的问题方方面面，所以尝试把公司推进项目的方法论，引入到装修中，按照工作中推进项目的流程要推进装修这件事情。<a href="https://www.yuque.com/docs/share/c3f13698-6325-4efb-aae0-59f0a2dac6c7">项目文档</a>，还真有不错的体验。其实还是认识到了方法论的重要性，按照一套既有成熟的标准来推进某些事情的时候，虽然不能保证做的都正确，但是还是可以做到心安理得，从容不迫吧。</p><p>至于那只暹罗猫，只是又长胖了，又变黑了而已。还是那么可爱。</p><p><img data-src="/images/2019-04-25-022244.jpg" alt="cat"></p><p>感谢家人父母对我的支持，还有老婆对我加班的忍耐。</p><h1 id="旅游"><a href="#旅游" class="headerlink" title="旅游"></a>旅游</h1><p>2018 年国庆，请了五天假，开开心心去了一趟夏威夷。开上了自己心心念念的敞篷野马，浮潜遇上了可爱的野生海豚，开车穿越云层在全世界最适合观星的山顶看到了银河，去活火山国家公园，但是没有看见岩浆。阳光，沙滩，大海，美不胜收。</p><p>有机会想带上爸妈，再去一次。</p><p><img data-src="/images/2019-04-25-022245.jpg" alt="Mustang"></p><p>还去了一趟成都，虽然只是匆匆一个周末，但也吃到了“串串”，也算了一桩心愿。</p><p><img data-src="/images/2019-04-25-022246.jpg" alt="chuanchuan"></p><h1 id="投资"><a href="#投资" class="headerlink" title="投资"></a>投资</h1><p>2017年小试牛刀的成功，有了一种天选之人的蜜汁自信，当然，2018 最终亏钱了。不过教训不少，投资这种反人性的活动，只有真正亏钱了，才会领教到市场的无情，才会去敬畏他。2019年要做的就是，努力工作保证现金流持续流入、强制储蓄保证应急资金的充足、最后用积极的心态面对市场。</p><h1 id="思考和总结"><a href="#思考和总结" class="headerlink" title="思考和总结"></a>思考和总结</h1><p>2018 对于我来说，今年的主题是成长。或者对于某些事情有了新的思考。或者，对于已经有的思维有着新的认识和更新。</p><h2 id="友好的和自己相处"><a href="#友好的和自己相处" class="headerlink" title="友好的和自己相处"></a>友好的和自己相处</h2><p>我们生活在一个贩卖焦虑的时代，如何友好的和自己相处，不被焦虑困扰，是今年思考最多的一个问题。今年下半年的自己，一直处在一个焦虑的状态。当一件事情处于自己无法掌控情况下的时候，就会处于一种相当焦虑的状态。总是担心最坏的结果发生在自己身上。如何与自己友好的相处？接受事情的不完美，接受不确定的世界，让自己相信事情总会有解决的办法，勇敢面对自己，勇敢面对这个世界。2019年重要的一项目标，就是如何的自恰，如何友好的和自己相处。</p><h2 id="方法论的学习"><a href="#方法论的学习" class="headerlink" title="方法论的学习"></a>方法论的学习</h2><p>一直以来都不太看得上方法论，觉得方法论是笨的人才需要学习的，方法论是按部就班，不懂变通的代名词。今年对这个问题的理解有了根本的转变，实际上方法论就是前人的经验总结，虽然看上去比较呆板，但是他确实有效。实际上按照一定的、通用的方法论推进某个事情的时候，至少保证事情的结果，达到预期的60%。剩下的就需要自己对于该事情的经验和积累了。所以现在想来,对于普通人来说：</p><blockquote><p>通用方法论 + 行业经验 &#x3D; （80% ~ 90%） 预期效果</p></blockquote><p>如果要达到 100 % 那就需要拼上天赋了。所以新的一年，我还会着重训练自己的阅读，写作的方法论。提升自己的通用能力，在寒冬中为自己储备更多的竞争力。</p><h2 id="复杂-VS-简单"><a href="#复杂-VS-简单" class="headerlink" title="复杂 VS 简单"></a>复杂 VS 简单</h2><p>解决复杂问题的其中一种思路就是，把复杂的问题，通过抽象以后简单看待，用最简单的规律去总结复杂的事情。事情处理完以后，及时复盘，形成沉淀，记录下来，变成某件事情的方法论。</p><p>但是面对简单问题的时候，总需要用多个角度，充分的思考，得出不一样的看法，保证对这个简单事情，全面的认识。不遗漏任何一个可能出现问题的点。</p><h2 id="无限的边界-VS-确定的边界"><a href="#无限的边界-VS-确定的边界" class="headerlink" title="无限的边界 VS 确定的边界"></a>无限的边界 VS 确定的边界</h2><p>对自己的要求不要设置边界，不要对知识自我设立边界。如今的社会，是一个分工高度明确的社会。在工作中需要的技能越来越单一。所谓“边界的无限”实际就是时刻需要突破舒适区，去尝试了解不属于自己负责的系统。</p><ul><li><p>了解上下游运行逻辑：</p><p>这里所谓的上下游，需要从两个角度去理解，一个角度是实际参与系统中，数据流向的上下游。比如，作为广告的投放后端，需要了解广告投放引擎，算法，数据的基本原理。第二，作为技术开发的角色，需要去了解产品，测试，运营运行的基本逻辑。只有了解了上下游的运行逻辑，理解你的同事手中的工作的运行逻辑。才做到，<strong>合理响应上游提出的要求、和合理的向下游提出要求</strong>。</p></li><li><p>了解整个系统运作的逻辑：</p><p>就是要求自己从整个系统的角度着眼，实现自己手上的系统。在实际开发中我们经常遇到一个问题，就是如果整个系统灵活多变，意味的大量的抽象和更多的开发成本，后期可维护性增加，修改起来比较迅速。如果一个系统比较死板，那开发的成本就会大量减少，但是扩展起来就是灾难。所以从整个系统运行的逻辑的高度去看这个问题，平衡灵活和成本，才能保证开发效率和后期可变更的一个平衡。</p></li></ul><p>对自己的要求是不设边界，但是与人合作的时候，却需要与对方明确事情的边界，尤其在项目开始前，就明确边界。在明确的边界内做到最好，这个才是保证与人合作能够顺利进行的基石。</p><h2 id="知识付费"><a href="#知识付费" class="headerlink" title="知识付费"></a>知识付费</h2><p>不知道从什么时候开始，所谓知识付费这个事情就火了，作为一个新知青年，2018年的的确为知识付出了不少费，但是任然处于买的多，学的少的社会主义初级阶段。反思以后发现，优秀的知识付费产品，或者说干货为主的知识付费产品，并不能减少学习需要投入的精力成本。觉得付费的，经过编排的知识，学起来就能容易一点，并不是一个正确的理解。或者保守一点说，付费的知识产品，在减少精力成本上，贡献有限，只是减少资料的收集和整理这个过程。所以：</p><blockquote><p>知识付费 <strong>不等于</strong> 买了就会<br>知识付费 <strong>不等于</strong> 简单好学<br>知识付费 <strong>不等于</strong> 都能学会</p></blockquote><p>所以今年知识付费，给我带来的困扰就是不聚焦，摊子铺的大但是效果并不好。学习还是只能脚踏实地，付费的知识，也只是一个学习路上的拐杖，学习之路上真正走路的还是你自己。</p><h2 id="对-feed-流的警惕"><a href="#对-feed-流的警惕" class="headerlink" title="对 feed 流的警惕"></a>对 feed 流的警惕</h2><p>feed:</p><blockquote><p>vt. 喂养；供给；放牧；抚养（家庭等）；靠…为生</p></blockquote><p>可以说这个 feed 这个单词相当形象和传神。信息被喂到你面前，而不是你去搜索，寻觅获得。依赖了 feed 限流，就失去了对信息选择的权利。</p><p>2018年，是头条系最成功的一年，基于算法分发信息这个模式全面统治互联网的一年。下拉刷新，上滑加载更多，这两个简单的动作完全就是时间的黑洞。算法一定会根据你的点击，阅读时长，阅读的字数，不断的推荐你感兴趣的信息，不断的把你喜欢的信息喂给你。这个时候就形成了一个恐怖的“信息茧房”。wiki 的定义：</p><blockquote><p>在信息传播中，因公众自身的信息需求并非全方位的，公众只注意自己选择的东西和使自己愉悦的通讯领域，久而久之，会将自身桎梏于像蚕茧一般的“茧房”中。</p></blockquote><p>在“茧房”中自娱自乐。最终被束缚的是自己的思想。所以新的一年我依然会对 feed 流保持警惕。尽可能使用 “搜索” 而不是 “推荐”。</p><h1 id="2019年目标"><a href="#2019年目标" class="headerlink" title="2019年目标"></a>2019年目标</h1><p>高高立起的 flag：</p><ul><li>写作，保持现在写作的节奏。新的一年需要更新 20 篇文章。</li><li>读书，去年给自己的要求过于高了，2019年妥协一些 8 本书。</li><li>学习，技术上，继续学习开源组件源码。业务上，全面了解商业变现业务。</li><li>完成装修，入住新家。</li><li>友好的和自己相处。</li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>2018 主题颜色，是暗色的，经历了严酷的互联网寒冬，虽然活下来了，但是更不能放松对自己的要求。比起2017年的奋勇前进，2018年更多的是稍微放慢脚步，回头看看，仔细想想。</p><p>展望新的一年，又一次充满了希望。</p><p><img data-src="/images/2019-04-25-022247.jpg" alt="hope"></p>]]>
    </content>
    <id>https://xilidou.com/2019/01/01/2018/</id>
    <link href="https://xilidou.com/2019/01/01/2018/"/>
    <published>2019-01-01T12:07:37.000Z</published>
    <summary>
      <![CDATA[<p><img src="/images/2019-04-25-022243.jpg" alt="winter"></p>
<p>2018年结束了，这一年成长是的一年。</p>]]>
    </summary>
    <title>我的2018年总结</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="LongAdder" scheme="https://xilidou.com/tags/LongAdder/"/>
    <category term="并发" scheme="https://xilidou.com/tags/%E5%B9%B6%E5%8F%91/"/>
    <content>
      <![CDATA[<p><img data-src="/images/2019-04-25-022205.jpg" alt="IMG"></p><p>最近在看阿里的 <a href="https://github.com/alibaba/Sentinel">Sentinel</a> 的源码的时候。发现使用了一个类 LongAdder 来在并发环境中计数。这个时候就提出了疑问，JDK 中已经有 AtomicLong 了，为啥还要使用 LongAdder ？ AtomicLong 已经是基于 CAS 的无锁结构，已经有很好的并发表现了，为啥还要用 LongAdder ？于是赶快找来源码一探究竟。</p><span id="more"></span><h2 id="AtomicLong-的缺陷"><a href="#AtomicLong-的缺陷" class="headerlink" title="AtomicLong 的缺陷"></a>AtomicLong 的缺陷</h2><p>大家可以阅读我之前写的 <a href="https://xilidou.com/2018/02/01/java-cas/">JAVA 中的 CAS</a> 详细了解 AtomicLong 的实现原理。需要注意的一点是，AtomicLong 的 Add() 是依赖自旋不断的 CAS 去累加<strong>一个</strong> Long 值。如果在竞争激烈的情况下，CAS 操作不断的失败，就会有大量的线程不断的自旋尝试 CAS 会造成 CPU 的极大的消耗。</p><h2 id="LongAdder-解决方案"><a href="#LongAdder-解决方案" class="headerlink" title="LongAdder 解决方案"></a>LongAdder 解决方案</h2><p>通过阅读 LongAdder 的 Javadoc 我们了解到：</p><blockquote><p>This class is usually preferable to {@link AtomicLong} when multiple threads update a common sum that is used for purposes such as collecting statistics, not for fine-grained synchronization control.  Under low update contention, the two classes have similar characteristics. But under high contention, expected throughput of this class is significantly higher, at the expense of higher space consumption.</p></blockquote><p>大概意思就是，LongAdder 功能类似 AtomicLong ，在低并发情况下二者表现差不多，在高并发情况下 LongAdder 的表现就会好很多。</p><p>LongAdder 到底用了什么黑科技能做到高性比 AtomicLong 还要好呢呢？对于同样的一个 add() 操作，上文说到 AtomicLong 只对一个 Long 值进行 CAS 操作。而 LongAdder 是针对 Cell 数组的某个 Cell 进行 CAS 操作 ，把线程的名字的 hash 值，作为 Cell 数组的下标，然后对 Cell[i] 的 long 进行 CAS 操作。简单粗暴的分散了高并发下的竞争压力。</p><h2 id="LongAdder-的实现细节"><a href="#LongAdder-的实现细节" class="headerlink" title="LongAdder 的实现细节"></a>LongAdder 的实现细节</h2><p>虽然原理简单粗暴，但是代码写得却相当细致和精巧。</p><p>在 <code>java.util.concurrent.atomic</code> 包下面我们可以看到 LongAdder 的源码。首先看 add() 方法的源码。</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">add</span><span class="params">(<span class="keyword">long</span> x)</span> </span>&#123;</span><br><span class="line">    Cell[] as; <span class="keyword">long</span> b, v; <span class="keyword">int</span> m; Cell a;</span><br><span class="line">    <span class="keyword">if</span> ((as = cells) != <span class="keyword">null</span> || !casBase(b = base, b + x)) &#123;</span><br><span class="line">        <span class="keyword">boolean</span> uncontended = <span class="keyword">true</span>;</span><br><span class="line">        <span class="keyword">if</span> (as == <span class="keyword">null</span> || (m = as.length - <span class="number">1</span>) &lt; <span class="number">0</span> ||</span><br><span class="line">            (a = as[getProbe() &amp; m]) == <span class="keyword">null</span> ||</span><br><span class="line">            !(uncontended = a.cas(v = a.value, v + x)))</span><br><span class="line">            longAccumulate(x, <span class="keyword">null</span>, uncontended);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>看到这个 add() 方法，首先需要了解 Cell 是什么？</p><p>Cell 是 <code>java.util.concurrent.atomic</code> 下 <code>Striped64</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><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@sun</span>.misc.Contended <span class="keyword">static</span> <span class="keyword">final</span> <span class="class"><span class="keyword">class</span> <span class="title">Cell</span> </span>&#123;</span><br><span class="line">    <span class="keyword">volatile</span> <span class="keyword">long</span> value;</span><br><span class="line">    Cell(<span class="keyword">long</span> x) &#123; value = x; &#125;</span><br><span class="line">    <span class="function"><span class="keyword">final</span> <span class="keyword">boolean</span> <span class="title">cas</span><span class="params">(<span class="keyword">long</span> cmp, <span class="keyword">long</span> val)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> UNSAFE.compareAndSwapLong(<span class="keyword">this</span>, valueOffset, cmp, val);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// unsafe 机制</span></span><br><span class="line">    <span class="comment">// Unsafe mechanics</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> sun.misc.Unsafe UNSAFE;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">long</span> valueOffset;</span><br><span class="line">    <span class="keyword">static</span> &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            UNSAFE = sun.misc.Unsafe.getUnsafe();</span><br><span class="line">            Class&lt;?&gt; ak = Cell.class;</span><br><span class="line">            valueOffset = UNSAFE.objectFieldOffset</span><br><span class="line">                (ak.getDeclaredField(<span class="string">&quot;value&quot;</span>));</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> Error(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>首先 Cell 被 @sun.misc.Contended 修饰。意思是让Java编译器和JRE运行时来决定如何填充。不理解不要紧，不影响理解。</p><p><strong>其实一个 Cell 的本质就是一个 volatile 修饰的 long 值，且这个值能够进行 cas 操作。</strong></p><p>回到我们的 add() 方法。</p><p>这里涉及四个额外的方法 casBase() , getProbe() , a.cas() , longAccumulate();</p><p>我们看名字就知道 casBase() 和 a.cas() 都是对参数的 cas 操作。 </p><p>getProbe() 的作用，就是根据当前线程 hash 出一个 int 值。</p><p>longAccumlate() 的作用比较复杂，之后我们会讲解。</p><p>所以这个 add() 操作归纳以后就是：</p><ol><li>如果 cells 数组不为空，对参数进行 casBase 操作，如果 casBase 操作失败。可能是竞争激烈，进入第二步。</li><li>如果 cells 为空，直接进入 longAccumulate();</li><li>m &#x3D; cells 数组长度减一，如果数组长度小于 1，则进入 longAccumulate()</li><li>如果都没有满足以上条件，则对当前线程进行某种 hash 生成一个数组下标，对下标保存的值进行 cas 操作。如果操作失败，则说明竞争依然激烈，则进入 longAccumulate().</li></ol><p>可见，操作的核心思想还是基于 cas。但是 cas 失败后，并不是傻乎乎的自旋，而是逐渐升级。升级的 cas 都不管用了则进入 longAccumulate() 这个方法。</p><p>下面就开始揭开 longAccumulate 的神秘面纱。</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><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><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">final</span> <span class="keyword">void</span> <span class="title">longAccumulate</span><span class="params">(<span class="keyword">long</span> x, LongBinaryOperator fn,</span></span></span><br><span class="line"><span class="params"><span class="function">                          <span class="keyword">boolean</span> wasUncontended)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">int</span> h;</span><br><span class="line">    <span class="keyword">if</span> ((h = getProbe()) == <span class="number">0</span>) &#123;</span><br><span class="line">        ThreadLocalRandom.current(); <span class="comment">// force initialization</span></span><br><span class="line">        h = getProbe();</span><br><span class="line">        wasUncontended = <span class="keyword">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">boolean</span> collide = <span class="keyword">false</span>;                <span class="comment">// True if last slot nonempty</span></span><br><span class="line">    <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">        Cell[] as; Cell a; <span class="keyword">int</span> n; <span class="keyword">long</span> v;</span><br><span class="line">        <span class="comment">//如果操作的cell 为空，double check 新建 cell</span></span><br><span class="line">        <span class="keyword">if</span> ((as = cells) != <span class="keyword">null</span> &amp;&amp; (n = as.length) &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> ((a = as[(n - <span class="number">1</span>) &amp; h]) == <span class="keyword">null</span>) &#123;</span><br><span class="line">                <span class="keyword">if</span> (cellsBusy == <span class="number">0</span>) &#123;       <span class="comment">// Try to attach new Cell</span></span><br><span class="line">                    Cell r = <span class="keyword">new</span> Cell(x);   <span class="comment">// Optimistically create</span></span><br><span class="line">                    <span class="keyword">if</span> (cellsBusy == <span class="number">0</span> &amp;&amp; casCellsBusy()) &#123;</span><br><span class="line">                        <span class="keyword">boolean</span> created = <span class="keyword">false</span>;</span><br><span class="line">                        <span class="keyword">try</span> &#123;               <span class="comment">// Recheck under lock</span></span><br><span class="line">                            Cell[] rs; <span class="keyword">int</span> m, j;</span><br><span class="line">                            <span class="keyword">if</span> ((rs = cells) != <span class="keyword">null</span> &amp;&amp;</span><br><span class="line">                                (m = rs.length) &gt; <span class="number">0</span> &amp;&amp;</span><br><span class="line">                                rs[j = (m - <span class="number">1</span>) &amp; h] == <span class="keyword">null</span>) &#123;</span><br><span class="line">                                rs[j] = r;</span><br><span class="line">                                created = <span class="keyword">true</span>;</span><br><span class="line">                            &#125;</span><br><span class="line">                        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                            cellsBusy = <span class="number">0</span>;</span><br><span class="line">                        &#125;</span><br><span class="line">                        <span class="keyword">if</span> (created)</span><br><span class="line">                            <span class="keyword">break</span>;</span><br><span class="line">                        <span class="keyword">continue</span>;           <span class="comment">// Slot is now non-empty</span></span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">                collide = <span class="keyword">false</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// cas 失败 继续循环</span></span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (!wasUncontended)       <span class="comment">// CAS already known to fail</span></span><br><span class="line">                wasUncontended = <span class="keyword">true</span>;      <span class="comment">// Continue after rehash</span></span><br><span class="line"></span><br><span class="line">            <span class="comment">// 如果 cell cas 成功 break</span></span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (a.cas(v = a.value, ((fn == <span class="keyword">null</span>) ? v + x :</span><br><span class="line">                                         fn.applyAsLong(v, x))))</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 如果 cell 的长度已经大于等于 cpu 的数量，扩容意义不大，就不用标记冲突，重试</span></span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (n &gt;= NCPU || cells != as)</span><br><span class="line">                collide = <span class="keyword">false</span>;            <span class="comment">// At max size or stale</span></span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (!collide)</span><br><span class="line">                collide = <span class="keyword">true</span>;</span><br><span class="line">            <span class="comment">// 获取锁，上锁扩容，将冲突标记为否，继续执行    </span></span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (cellsBusy == <span class="number">0</span> &amp;&amp; casCellsBusy()) &#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="keyword">if</span> (cells == as) &#123;      <span class="comment">// Expand table unless stale</span></span><br><span class="line">                        Cell[] rs = <span class="keyword">new</span> Cell[n &lt;&lt; <span class="number">1</span>];</span><br><span class="line">                        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; n; ++i)</span><br><span class="line">                            rs[i] = as[i];</span><br><span class="line">                        cells = rs;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                    cellsBusy = <span class="number">0</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                collide = <span class="keyword">false</span>;</span><br><span class="line">                <span class="keyword">continue</span>;                   <span class="comment">// Retry with expanded table</span></span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">// 没法获取锁，重散列，尝试其他槽</span></span><br><span class="line">            h = advanceProbe(h);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 获取锁，初始化 cell 数组</span></span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (cellsBusy == <span class="number">0</span> &amp;&amp; cells == as &amp;&amp; casCellsBusy()) &#123;</span><br><span class="line">            <span class="keyword">boolean</span> init = <span class="keyword">false</span>;</span><br><span class="line">            <span class="keyword">try</span> &#123;                           <span class="comment">// Initialize table</span></span><br><span class="line">                <span class="keyword">if</span> (cells == as) &#123;</span><br><span class="line">                    Cell[] rs = <span class="keyword">new</span> Cell[<span class="number">2</span>];</span><br><span class="line">                    rs[h &amp; <span class="number">1</span>] = <span class="keyword">new</span> Cell(x);</span><br><span class="line">                    cells = rs;</span><br><span class="line">                    init = <span class="keyword">true</span>;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                cellsBusy = <span class="number">0</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">if</span> (init)</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 表未被初始化，可能正在初始化，回退使用 base。</span></span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (casBase(v = base, ((fn == <span class="keyword">null</span>) ? v + x :</span><br><span class="line">                                    fn.applyAsLong(v, x))))</span><br><span class="line">            <span class="keyword">break</span>;                          <span class="comment">// Fall back on using base</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>longAccumulate 看上去比较复杂。我们慢慢分析。</p><p>回忆一下，什么情况会进入到这个 longAccumulate 方法中</p><ul><li>cell[] 数组为空，</li><li>cell[i] 数据的某个下标元素为空，</li><li>casBase 失败，</li><li>a.cas 失败，</li><li>cell.length - 1 &lt; 0</li></ul><p>在 longAccumulate 中有几个标记位，我们也先理解一下</p><ul><li><code>cellsBusy</code> cells 的操作标记位，如果正在修改、新建、操作 cells 数组中的元素会,会将其 cas 为 1，否则为0。</li><li><code>wasUncontended</code> 表示 cas 是否失败，如果失败则考虑操作升级。</li><li><code>collide</code> 是否冲突，如果冲突，则考虑扩容 cells 的长度。</li></ul><p>整个 for(;;) 死循环，都是以 cas 操作成功而告终。否则则会修改上述描述的几个标记位，重新进入循环。</p><p>所以整个循环包括如下几种情况：</p><ol><li><p>cells 不为空</p><ol><li>如果 cell[i] 某个下标为空，则 new 一个 cell，并初始化值，然后退出</li><li>如果 cas 失败，继续循环</li><li>如果 cell 不为空，且 cell cas 成功，退出</li><li>如果 cell 的数量，大于等于 cpu 数量或者已经扩容了，继续重试。（扩容没意义）</li><li>设置 collide 为 true。</li><li>获取 cellsBusy 成功就对 cell 进行扩容，获取 cellBusy 失败则重新 hash 再重试。</li></ol></li><li><p>cells 为空且获取到 cellsBusy ，init cells 数组，然后赋值退出。</p></li><li><p>cellsBusy 获取失败，则进行 baseCas ，操作成功退出，不成功则重试。</p></li></ol><p>至此 longAccumulate 就分析完了。之所以这个方法那么复杂，我认为有两个原因</p><ol><li>是因为并发环境下要考虑各种操作的原子性，所以对于锁都进行了 double check。</li><li>操作都是逐步升级，以最小的代价实现功能。</li></ol><p>最后说说 LongAddr 的 sum() 方法，这个就很简单了。</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="function"><span class="keyword">public</span> <span class="keyword">long</span> <span class="title">sum</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    Cell[] as = cells; Cell a;</span><br><span class="line">    <span class="keyword">long</span> sum = base;</span><br><span class="line">    <span class="keyword">if</span> (as != <span class="keyword">null</span>) &#123;</span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; as.length; ++i) &#123;</span><br><span class="line">            <span class="keyword">if</span> ((a = as[i]) != <span class="keyword">null</span>)</span><br><span class="line">                sum += a.value;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> sum;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>就是遍历 cell 数组，累加 value 就行。LongAdder 余下的方法就比较简单，没有什么可以讨论的了。</p><h2 id="LongAdder-VS-AtomicLong"><a href="#LongAdder-VS-AtomicLong" class="headerlink" title="LongAdder VS AtomicLong"></a>LongAdder VS AtomicLong</h2><p>看上去 LongAdder 性能全面超越了 AtomicLong。为什么 jdk 1.8 中还是保留了 AtomicLong 的实现呢？</p><p>其实我们可以发现，LongAdder 使用了一个 cell 列表去承接并发的 cas，以提升性能，但是 LongAdder 在统计的时候如果有并发更新，可能导致统计的数据有误差。</p><p>如果用于自增 id 的生成，就不适合使用 LongAdder 了。这个时候使用 AtomicLong 就是一个明智的选择。</p><p>而在 Sentinel 中 LongAdder 承担的只是统计任务，且允许误差。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>LongAdder 使用了一个比较简单的原理，解决了 AtomicLong 类，在极高竞争下的性能问题。但是 LongAdder 的具体实现却非常精巧和细致，分散竞争，逐步升级竞争的解决方案，相当漂亮，值得我们细细品味。</p>]]>
    </content>
    <id>https://xilidou.com/2018/11/27/LongAdder/</id>
    <link href="https://xilidou.com/2018/11/27/LongAdder/"/>
    <published>2018-11-27T20:42:20.000Z</published>
    <summary>
      <![CDATA[<p><img src="/images/2019-04-25-022205.jpg" alt="IMG"></p>
<p>最近在看阿里的 <a href="https://github.com/alibaba/Sentinel">Sentinel</a> 的源码的时候。发现使用了一个类 LongAdder 来在并发环境中计数。这个时候就提出了疑问，JDK 中已经有 AtomicLong 了，为啥还要使用 LongAdder ？ AtomicLong 已经是基于 CAS 的无锁结构，已经有很好的并发表现了，为啥还要用 LongAdder ？于是赶快找来源码一探究竟。</p>]]>
    </summary>
    <title>从 LongAdder 中窥见并发组件的设计思路</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="rpc" scheme="https://xilidou.com/tags/rpc/"/>
    <content>
      <![CDATA[<p><img data-src="/images/2019-04-25-022219.jpg" alt="title"></p><p>微服务已经是每个互联网开发者必须掌握的一项技术。而 RPC 框架，是构成微服务最重要的组成部分之一。趁最近有时间。又看了看 dubbo 的源码。dubbo 为了做到灵活和解耦，使用了大量的设计模式和 SPI机制，要看懂 dubbo 的代码也不太容易。</p><p>按照《徒手撸框架》系列文章的套路，我还是会极简的实现一个 RPC 框架。帮助大家理解 RPC 框架的原理。</p><p>广义的来讲一个完整的 RPC 包含了很多组件，包括服务发现，服务治理，远程调用，调用链分析，网关等等。我将会慢慢的实现这些功能，这篇文章主要先讲解的是 RPC 的基石，<strong>远程调用</strong> 的实现。</p><p>相信，读完这篇文章你也一定可以自己实现一个可以提供 RPC 调用的框架。</p><span id="more"></span><h2 id="1-RPC-的调用过程"><a href="#1-RPC-的调用过程" class="headerlink" title="1. RPC 的调用过程"></a>1. RPC 的调用过程</h2><p>通过下图我们来了解一下 RPC 的调用过程，从宏观上来看看到底一次 RPC 调用经过些什么过程。</p><p>当一次调用开始：</p><p><img data-src="/images/2019-04-25-22220.jpg" alt="img"></p><ol><li>client 会调用本地动态代理 proxy</li><li>这个代理会将调用通过协议转序列化字节流</li><li>通过 netty 网络框架，将字节流发送到服务端</li><li>服务端在受到这个字节流后，会根据协议，反序列化为原始的调用，利用反射原理调用服务方提供的方法</li><li>如果请求有返回值，又需要把结果根据协议序列化后，再通过 netty 返回给调用方</li></ol><h2 id="2-框架概览和技术选型"><a href="#2-框架概览和技术选型" class="headerlink" title="2. 框架概览和技术选型"></a>2. 框架概览和技术选型</h2><p>看一看框架的组件:</p><p><img data-src="/images/2019-04-25-022221.jpg" alt="ig"></p><p><code>clinet</code>就是调用方。<code>servive</code>是服务的提供者。<code>protocol</code>包定义了通信协议。<code>common</code>包含了通用的一些逻辑组件。</p><p>技术选型项目使用 <code>maven</code> 作为包管理工具，<code>json</code> 作为序列化协议，使用<code>spring boot</code>管理对象的生命周期，<code>netty</code> 作为 <code>nio</code> 的网路组件。所以要阅读这篇文章，你需要对<code>spring boot</code>和<code>netty</code>有基本的了解。</p><p>下面就看看每个组件的具体实现：</p><h2 id="3-protocol"><a href="#3-protocol" class="headerlink" title="3. protocol"></a>3. protocol</h2><p>其实作为 RPC 的协议，只需要考虑一个问题，就是怎么把一次本地方法的调用，变成能够被网络传输的字节流。</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><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="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RpcRequest</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 调用编号</span></span><br><span class="line">    <span class="keyword">private</span> String requestId;</span><br><span class="line">    <span class="comment">// 类名</span></span><br><span class="line">    <span class="keyword">private</span> String className;</span><br><span class="line">    <span class="comment">// 方法名</span></span><br><span class="line">    <span class="keyword">private</span> String methodName;</span><br><span class="line">    <span class="comment">// 请求参数的数据类型</span></span><br><span class="line">    <span class="keyword">private</span> Class&lt;?&gt;[] parameterTypes;</span><br><span class="line">    <span class="comment">// 请求的参数</span></span><br><span class="line">    <span class="keyword">private</span> Object[] parameters;</span><br><span class="line">&#125;</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><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="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RpcResponse</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 调用编号</span></span><br><span class="line">    <span class="keyword">private</span> String requestId;</span><br><span class="line">    <span class="comment">// 抛出的异常</span></span><br><span class="line">    <span class="keyword">private</span> Throwable throwable;</span><br><span class="line">    <span class="comment">// 返回结果</span></span><br><span class="line">    <span class="keyword">private</span> Object result;</span><br><span class="line"></span><br><span class="line">&#125;</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><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">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">Serialization</span> </span>&#123;</span><br><span class="line">    &lt;T&gt; <span class="keyword">byte</span>[] serialize(T obj);</span><br><span class="line">    &lt;T&gt; <span class="function">T <span class="title">deSerialize</span><span class="params">(<span class="keyword">byte</span>[] data,Class&lt;T&gt; clz)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可选用的序列化的协议很多，比如：</p><ul><li>jdk 的序列化方法。（不推荐，不利于之后的跨语言调用）</li><li>json 可读性强，但是序列化速度慢，体积大。</li><li>protobuf，kyro，Hessian 等都是优秀的序列化框架，也可按需选择。</li></ul><p>为了简单和便于调试，我们就选择 json 作为序列化协议，使用<code>jackson</code>作为 json 解析框架。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> Zhengxin</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">JsonSerialization</span> <span class="keyword">implements</span> <span class="title">Serialization</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> ObjectMapper objectMapper;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">JsonSerialization</span><span class="params">()</span></span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.objectMapper = <span class="keyword">new</span> ObjectMapper();</span><br><span class="line">    &#125;</span><br><span class="line"></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> &lt;T&gt; <span class="keyword">byte</span>[] serialize(T obj) &#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> objectMapper.writeValueAsBytes(obj);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (JsonProcessingException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">null</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> &lt;T&gt; <span class="function">T <span class="title">deSerialize</span><span class="params">(<span class="keyword">byte</span>[] data, Class&lt;T&gt; clz)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> objectMapper.readValue(data,clz);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IOException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因为 netty 支持自定义 coder 。所以只需要实现 <code>ByteToMessageDecoder</code> 和 <code>MessageToByteEncoder</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><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="class"><span class="keyword">class</span> <span class="title">RpcDecoder</span> <span class="keyword">extends</span> <span class="title">ByteToMessageDecoder</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Class&lt;?&gt; clz;</span><br><span class="line">    <span class="keyword">private</span> Serialization serialization;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">RpcDecoder</span><span class="params">(Class&lt;?&gt; clz,Serialization serialization)</span></span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.clz = clz;</span><br><span class="line">        <span class="keyword">this</span>.serialization = serialization;</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="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">decode</span><span class="params">(ChannelHandlerContext ctx, ByteBuf in, List&lt;Object&gt; out)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(in.readableBytes() &lt; <span class="number">4</span>)&#123;</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">        in.markReaderIndex();</span><br><span class="line">        <span class="keyword">int</span> dataLength = in.readInt();</span><br><span class="line">        <span class="keyword">if</span> (in.readableBytes() &lt; dataLength) &#123;</span><br><span class="line">            in.resetReaderIndex();</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">byte</span>[] data = <span class="keyword">new</span> <span class="keyword">byte</span>[dataLength];</span><br><span class="line">        in.readBytes(data);</span><br><span class="line"></span><br><span class="line">        Object obj = serialization.deSerialize(data, clz);</span><br><span class="line">        out.add(obj);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RpcEncoder</span> <span class="keyword">extends</span> <span class="title">MessageToByteEncoder</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Class&lt;?&gt; clz;</span><br><span class="line">    <span class="keyword">private</span> Serialization serialization;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">RpcEncoder</span><span class="params">(Class&lt;?&gt; clz, Serialization serialization)</span></span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.clz = clz;</span><br><span class="line">        <span class="keyword">this</span>.serialization = serialization;</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="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">encode</span><span class="params">(ChannelHandlerContext ctx, Object msg, ByteBuf out)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(clz != <span class="keyword">null</span>)&#123;</span><br><span class="line">            <span class="keyword">byte</span>[] bytes = serialization.serialize(msg);</span><br><span class="line">            out.writeInt(bytes.length);</span><br><span class="line">            out.writeBytes(bytes);</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>至此，protocol 就实现了，我们就可以把方法的调用和结果的响应转换为一串可以在网络中传输的 byte[] 数组了。</p><h2 id="4-server"><a href="#4-server" class="headerlink" title="4. server"></a>4. server</h2><p>server 是负责处理客户端请求的组件。在互联网高并发的环境下，使用 Nio 非阻塞的方式可以相对轻松的应付高并发的场景。netty 是一个优秀的 Nio 处理框架。Server 就基于 netty 进行开发。关键代码如下：</p><ol><li>netty 是基于 Reacotr 模型的。所以需要初始化两组线程 boss 和 worker 。boss 负责分发请求，worker 负责执行相应的 handler：</li></ol><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="meta">@Bean</span></span><br><span class="line">   <span class="function"><span class="keyword">public</span> ServerBootstrap <span class="title">serverBootstrap</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line"></span><br><span class="line">       ServerBootstrap serverBootstrap = <span class="keyword">new</span> ServerBootstrap();</span><br><span class="line"></span><br><span class="line">       serverBootstrap.group(bossGroup(), workerGroup())</span><br><span class="line">               .channel(NioServerSocketChannel.class)</span><br><span class="line">               .handler(<span class="keyword">new</span> LoggingHandler(LogLevel.DEBUG))</span><br><span class="line">               .childHandler(serverInitializer);</span><br><span class="line"></span><br><span class="line">       Map&lt;ChannelOption&lt;?&gt;, Object&gt; tcpChannelOptions = tcpChannelOptions();</span><br><span class="line">       Set&lt;ChannelOption&lt;?&gt;&gt; keySet = tcpChannelOptions.keySet();</span><br><span class="line">       <span class="keyword">for</span> (<span class="meta">@SuppressWarnings(&quot;rawtypes&quot;)</span> ChannelOption option : keySet) &#123;</span><br><span class="line">           serverBootstrap.option(option, tcpChannelOptions.get(option));</span><br><span class="line">       &#125;</span><br><span class="line"></span><br><span class="line">       <span class="keyword">return</span> serverBootstrap;</span><br><span class="line">   &#125;</span><br></pre></td></tr></table></figure><ol><li>netty 的操作是基于 pipeline 的。所以我们需要把在 protocol 实现的几个 coder 注册到 netty 的 pipeline 中。</li></ol><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><br><span class="line">ChannelPipeline pipeline = ch.pipeline();</span><br><span class="line"><span class="comment">// 处理 tcp 请求中粘包的 coder，具体作用可以自行 google</span></span><br><span class="line">pipeline.addLast(<span class="keyword">new</span> LengthFieldBasedFrameDecoder(<span class="number">65535</span>,<span class="number">0</span>,<span class="number">4</span>));</span><br><span class="line"></span><br><span class="line"><span class="comment">// protocol 中实现的 序列化和反序列化 coder</span></span><br><span class="line">pipeline.addLast(<span class="keyword">new</span> RpcEncoder(RpcResponse.class,<span class="keyword">new</span> JsonSerialization()));</span><br><span class="line">pipeline.addLast(<span class="keyword">new</span> RpcDecoder(RpcRequest.class,<span class="keyword">new</span> JsonSerialization()));</span><br><span class="line"></span><br><span class="line"><span class="comment">// 具体处理请求的 handler 下文具体解释</span></span><br><span class="line">pipeline.addLast(serverHandler);</span><br><span class="line"></span><br></pre></td></tr></table></figure><ol><li>实现具体的 ServerHandler 用于处理真正的调用。</li></ol><p><code>ServerHandler</code> 继承 <code>SimpleChannelInboundHandler&lt;RpcRequest&gt;</code>。简单来说这个 <code>InboundHandler</code> 会在数据被接受时或者对于的 Channel 的状态发生变化的时候被调用。当这个 handler 读取数据的时候方法 <code>channelRead0()</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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">channelRead0</span><span class="params">(ChannelHandlerContext ctx, RpcRequest msg)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    RpcResponse rpcResponse = <span class="keyword">new</span> RpcResponse();</span><br><span class="line">    rpcResponse.setRequestId(msg.getRequestId());</span><br><span class="line">    <span class="keyword">try</span>&#123;</span><br><span class="line">        <span class="comment">// 收到请求后开始处理请求</span></span><br><span class="line">        Object handler = handler(msg);</span><br><span class="line">        rpcResponse.setResult(handler);</span><br><span class="line">    &#125;<span class="keyword">catch</span> (Throwable throwable)&#123;</span><br><span class="line">        <span class="comment">// 如果抛出异常也将异常存入 response 中</span></span><br><span class="line">        rpcResponse.setThrowable(throwable);</span><br><span class="line">        throwable.printStackTrace();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 操作完以后写入 netty 的上下文中。netty 自己处理返回值。</span></span><br><span class="line">    ctx.writeAndFlush(rpcResponse);</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>handler(msg) 实际上使用的是 cglib 的 Fastclass 实现的，其实根本原理，还是反射。学好 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><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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> Object <span class="title">handler</span><span class="params">(RpcRequest request)</span> <span class="keyword">throws</span> Throwable </span>&#123;</span><br><span class="line">    Class&lt;?&gt; clz = Class.forName(request.getClassName());</span><br><span class="line">    Object serviceBean = applicationContext.getBean(clz);</span><br><span class="line"></span><br><span class="line">    Class&lt;?&gt; serviceClass = serviceBean.getClass();</span><br><span class="line">    String methodName = request.getMethodName();</span><br><span class="line"></span><br><span class="line">    Class&lt;?&gt;[] parameterTypes = request.getParameterTypes();</span><br><span class="line">    Object[] parameters = request.getParameters();</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 根本思路还是获取类名和方法名，利用反射实现调用</span></span><br><span class="line">    FastClass fastClass = FastClass.create(serviceClass);</span><br><span class="line">    FastMethod fastMethod = fastClass.getMethod(methodName,parameterTypes);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 实际调用发生的地方</span></span><br><span class="line">    <span class="keyword">return</span> fastMethod.invoke(serviceBean,parameters);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>总体上来看，server 的实现不是很困难。核心的知识点是 netty 的 channel 的使用和 cglib 的反射机制。</p><h2 id="5-client"><a href="#5-client" class="headerlink" title="5. client"></a>5. client</h2><h3 id="future"><a href="#future" class="headerlink" title="future"></a>future</h3><p>其实，对于我来说，client 的实现难度，远远大于 server 的实现。netty 是一个异步框架，所有的返回都是基于 Future 和 Callback 的机制。</p><p>所以在阅读以下文字前强烈推荐，我之前写的一篇文章 <a href="https://www.xilidou.com/2017/10/24/Futuer%E7%A0%94%E7%A9%B6/">Future 研究</a>。利用经典的 wite 和 notify 机制，实现异步的获取请求结果。</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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> zhengxin</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DefaultFuture</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> RpcResponse rpcResponse;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">volatile</span> <span class="keyword">boolean</span> isSucceed = <span class="keyword">false</span>;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Object object = <span class="keyword">new</span> Object();</span><br><span class="line">    <span class="function"><span class="keyword">public</span> RpcResponse <span class="title">getResponse</span><span class="params">(<span class="keyword">int</span> timeout)</span></span>&#123;</span><br><span class="line">        <span class="keyword">synchronized</span> (object)&#123;</span><br><span class="line">            <span class="keyword">while</span> (!isSucceed)&#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="comment">//wait</span></span><br><span class="line">                        object.wait(timeout);</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">            <span class="keyword">return</span> rpcResponse;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setResponse</span><span class="params">(RpcResponse response)</span></span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(isSucceed)&#123;</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">synchronized</span> (object) &#123;</span><br><span class="line">            <span class="keyword">this</span>.rpcResponse = response;</span><br><span class="line">            <span class="keyword">this</span>.isSucceed = <span class="keyword">true</span>;</span><br><span class="line">            <span class="comment">//notiy</span></span><br><span class="line">            object.notify();</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><br></pre></td></tr></table></figure><h2 id="复用资源"><a href="#复用资源" class="headerlink" title="复用资源"></a>复用资源</h2><p>为了能够提升 client 的吞吐量，可提供的思路有以下几种：</p><ol><li><p>使用对象池：建立多个 client 以后保存在对象池中。但是代码的复杂度和维护 client 的成本会很高。</p></li><li><p>尽可能的复用 netty 中的 channel。<br>之前你可能注意到，为什么要在 RpcRequest 和 RpcResponse 中增加一个 ID。因为 netty 中的 channel 是会被多个线程使用的。当一个结果异步的返回后，你并不知道是哪个线程返回的。这个时候就可以考虑利用一个 Map，建立一个 ID 和 Future 映射。这样请求的线程只要使用对应的 ID 就能获取，相应的返回结果。</p></li></ol><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> Zhengxin</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ClientHandler</span> <span class="keyword">extends</span> <span class="title">ChannelDuplexHandler</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 使用 map 维护 id 和 Future 的映射关系，在多线程环境下需要使用线程安全的容器</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Map&lt;String, DefaultFuture&gt; futureMap = <span class="keyword">new</span> ConcurrentHashMap&lt;&gt;();</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">write</span><span class="params">(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(msg <span class="keyword">instanceof</span> RpcRequest)&#123;</span><br><span class="line">            RpcRequest request = (RpcRequest) msg;</span><br><span class="line">            <span class="comment">// 写数据的时候，增加映射</span></span><br><span class="line">            futureMap.putIfAbsent(request.getRequestId(),<span class="keyword">new</span> DefaultFuture());</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">super</span>.write(ctx, msg, promise);</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">channelRead</span><span class="params">(ChannelHandlerContext ctx, Object msg)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(msg <span class="keyword">instanceof</span> RpcResponse)&#123;</span><br><span class="line">            RpcResponse response = (RpcResponse) msg;</span><br><span class="line">            <span class="comment">// 获取数据的时候 将结果放入 future 中</span></span><br><span class="line">            DefaultFuture defaultFuture = futureMap.get(response.getRequestId());</span><br><span class="line">            defaultFuture.setResponse(response);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">super</span>.channelRead(ctx, msg);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> RpcResponse <span class="title">getRpcResponse</span><span class="params">(String requestId)</span></span>&#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 从 future 中获取真正的结果。</span></span><br><span class="line">            DefaultFuture defaultFuture = futureMap.get(requestId);</span><br><span class="line">            <span class="keyword">return</span> defaultFuture.getResponse(<span class="number">10</span>);</span><br><span class="line">        &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="comment">// 完成后从 map 中移除。</span></span><br><span class="line">            futureMap.remove(requestId);</span><br><span class="line">        &#125;</span><br><span class="line"></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>这里没有继承 server 中的 <code>InboundHandler</code> 而使用了 <code>ChannelDuplexHandler</code>。顾名思义就是在写入和读取数据的时候，都会触发相应的方法。写入的时候在 Map 中保存 ID 和 Future。读到数据的时候从 Map 中取出 Future 并将结果放入  Future 中。获取结果的时候需要对应的 ID。</p><p>使用 <code>Transporters</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Transporters</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> RpcResponse <span class="title">send</span><span class="params">(RpcRequest request)</span></span>&#123;</span><br><span class="line">        NettyClient nettyClient = <span class="keyword">new</span> NettyClient(<span class="string">&quot;127.0.0.1&quot;</span>, <span class="number">8080</span>);</span><br><span class="line">        nettyClient.connect(nettyClient.getInetSocketAddress());</span><br><span class="line">        RpcResponse send = nettyClient.send(request);</span><br><span class="line">        <span class="keyword">return</span> send;</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>动态代理技术最广为人知的应用，应该就是 Spring 的 Aop，面向切面的编程实现，动态的在原有方法Before 或者 After 添加代码。而 RPC 框架中动态代理的作用就是彻底替换原有方法，直接调用远程方法。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ProxyFactory</span> </span>&#123;</span><br><span class="line">    <span class="meta">@SuppressWarnings(&quot;unchecked&quot;)</span></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">T <span class="title">create</span><span class="params">(Class&lt;T&gt; interfaceClass)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> (T) Proxy.newProxyInstance(</span><br><span class="line">                interfaceClass.getClassLoader(),</span><br><span class="line">                <span class="keyword">new</span> Class&lt;?&gt;[]&#123;interfaceClass&#125;,</span><br><span class="line">                <span class="keyword">new</span> RpcInvoker&lt;T&gt;(interfaceClass)</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>当 proxyFactory 生成的类被调用的时候，就会执行 RpcInvoker 方法。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RpcInvoker</span>&lt;<span class="title">T</span>&gt; <span class="keyword">implements</span> <span class="title">InvocationHandler</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> Class&lt;T&gt; clz;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">RpcInvoker</span><span class="params">(Class&lt;T&gt; clz)</span></span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.clz = clz;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Object <span class="title">invoke</span><span class="params">(Object proxy, Method method, Object[] args)</span> <span class="keyword">throws</span> Throwable </span>&#123;</span><br><span class="line">        RpcRequest request = <span class="keyword">new</span> RpcRequest();</span><br><span class="line"></span><br><span class="line">        String requestId = UUID.randomUUID().toString();</span><br><span class="line"></span><br><span class="line">        String className = method.getDeclaringClass().getName();</span><br><span class="line">        String methodName = method.getName();</span><br><span class="line">        Class&lt;?&gt;[] parameterTypes = method.getParameterTypes();</span><br><span class="line"></span><br><span class="line">        request.setRequestId(requestId);</span><br><span class="line">        request.setClassName(className);</span><br><span class="line">        request.setMethodName(methodName);</span><br><span class="line">        request.setParameterTypes(parameterTypes);</span><br><span class="line">        request.setParameters(args);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> Transporters.send(request).getResult();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>看到这个 invoke 方法，主要三个作用，</p><ol><li>生成 RequestId。</li><li>拼装 RpcRequest。</li><li>调用 Transports 发送请求，获取结果。</li></ol><p>至此，整个调用链完整了。我们终于完成了一次 RPC 调用。</p><h2 id="与-Spring-集成"><a href="#与-Spring-集成" class="headerlink" title="与 Spring 集成"></a>与 Spring 集成</h2><p>为了使我们的 client 能够易于使用我们需要考虑，定义一个自定义注解 <code>@RpcInterface</code> 当我们的项目接入 Spring 以后，Spring 扫描到这个注解之后，自动的通过我们的 ProxyFactory 创建代理对象，并存放在 spring 的 applicationContext 中。这样我们就可以通过 <code>@Autowired</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Target(&#123;ElementType.TYPE&#125;)</span></span><br><span class="line"><span class="meta">@Retention(RetentionPolicy.RUNTIME)</span></span><br><span class="line"><span class="keyword">public</span> <span class="meta">@interface</span> RpcInterface &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RpcConfig</span> <span class="keyword">implements</span> <span class="title">ApplicationContextAware</span>,<span class="title">InitializingBean</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> ApplicationContext applicationContext;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setApplicationContext</span><span class="params">(ApplicationContext applicationContext)</span> <span class="keyword">throws</span> BeansException </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.applicationContext = applicationContext;</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">afterPropertiesSet</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        Reflections reflections = <span class="keyword">new</span> Reflections(<span class="string">&quot;com.xilidou&quot;</span>);</span><br><span class="line">        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();</span><br><span class="line">        <span class="comment">// 获取 @RpcInterfac 标注的接口</span></span><br><span class="line">        Set&lt;Class&lt;?&gt;&gt; typesAnnotatedWith = reflections.getTypesAnnotatedWith(RpcInterface.class);</span><br><span class="line">        <span class="keyword">for</span> (Class&lt;?&gt; aClass : typesAnnotatedWith) &#123;</span><br><span class="line">            <span class="comment">// 创建代理对象，并注册到 spring 上下文。</span></span><br><span class="line">            beanFactory.registerSingleton(aClass.getSimpleName(),ProxyFactory.create(aClass));</span><br><span class="line">        &#125;</span><br><span class="line">        log.info(<span class="string">&quot;afterPropertiesSet is &#123;&#125;&quot;</span>,typesAnnotatedWith);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>终于我们最简单的 RPC 框架就开发完了。下面可以测试一下。</p><h2 id="6-Demo"><a href="#6-Demo" class="headerlink" title="6. Demo"></a>6. Demo</h2><h3 id="api"><a href="#api" class="headerlink" title="api"></a>api</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@RpcInterface</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">IHelloService</span> </span>&#123;</span><br><span class="line">    <span class="function">String <span class="title">sayHi</span><span class="params">(String name)</span></span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="server"><a href="#server" class="headerlink" title="server"></a>server</h3><p>IHelloSerivce 的实现：</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="meta">@Service</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestServiceImpl</span> <span class="keyword">implements</span> <span class="title">IHelloService</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">sayHi</span><span class="params">(String name)</span> </span>&#123;</span><br><span class="line">        log.info(name);</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;Hello &quot;</span> + name;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</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><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"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Application</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        ConfigurableApplicationContext context = SpringApplication.run(Application.class);</span><br><span class="line">        TcpService tcpService = context.getBean(TcpService.class);</span><br><span class="line">        tcpService.start();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line">````</span><br><span class="line"></span><br><span class="line">### client</span><br><span class="line"></span><br><span class="line">```java</span><br><span class="line"><span class="meta">@SpringBootApplication()</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ClientApplication</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        ConfigurableApplicationContext context = SpringApplication.run(ClientApplication.class);</span><br><span class="line">        IHelloService helloService = context.getBean(IHelloService.class);</span><br><span class="line">        System.out.println(helloService.sayHi(<span class="string">&quot;doudou&quot;</span>));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行以后输出的结果：</p><blockquote><p>Hello doudou</p></blockquote><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>终于我们实现了一个最简版的 RPC 远程调用的模块。只是包含最最基础的远程调用功能。</p><p>如果你对这个项目感兴趣，欢迎你与我联系，为这个框架贡献代码。</p><p>老规矩 Github 地址：<a href="https://github.com/diaozxin007/DouRpc">DouPpc</a></p><p>徒手撸框架系列文章地址：</p><p><a href="https://www.xilidou.com/2018/01/08/spring-ioc/">徒手撸框架–实现IoC</a><br><a href="https://www.xilidou.com/2018/01/13/spring-aop/">徒手撸框架–实现Aop</a><br><a href="https://www.xilidou.com/2018/01/22/merge-request/">徒手撸框架–高并发环境下的请求合并</a></p>]]>
    </content>
    <id>https://xilidou.com/2018/09/26/dourpc-remoting/</id>
    <link href="https://xilidou.com/2018/09/26/dourpc-remoting/"/>
    <published>2018-09-26T18:18:21.000Z</published>
    <summary>
      <![CDATA[<p><img src="/images/2019-04-25-022219.jpg" alt="title"></p>
<p>微服务已经是每个互联网开发者必须掌握的一项技术。而 RPC 框架，是构成微服务最重要的组成部分之一。趁最近有时间。又看了看 dubbo 的源码。dubbo 为了做到灵活和解耦，使用了大量的设计模式和 SPI机制，要看懂 dubbo 的代码也不太容易。</p>
<p>按照《徒手撸框架》系列文章的套路，我还是会极简的实现一个 RPC 框架。帮助大家理解 RPC 框架的原理。</p>
<p>广义的来讲一个完整的 RPC 包含了很多组件，包括服务发现，服务治理，远程调用，调用链分析，网关等等。我将会慢慢的实现这些功能，这篇文章主要先讲解的是 RPC 的基石，<strong>远程调用</strong> 的实现。</p>
<p>相信，读完这篇文章你也一定可以自己实现一个可以提供 RPC 调用的框架。</p>]]>
    </summary>
    <title>徒手撸框架--实现 RPC 远程调用</title>
    <updated>2026-08-08T02:04:49.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="工具" scheme="https://xilidou.com/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="tools" scheme="https://xilidou.com/tags/tools/"/>
    <category term="写作" scheme="https://xilidou.com/tags/%E5%86%99%E4%BD%9C/"/>
    <content>
      <![CDATA[<p><img data-src="/images/2019-04-25-022227.jpg" alt="img"></p><p>写作是技术输出的重要手段。自己也写了一年多的文章，累计也超过五万多字。今天就想谈谈自己对于写作的一些看法以及写作时使用到的工具。工欲善其事必先利其器。</p><span id="more"></span><h2 id="输入"><a href="#输入" class="headerlink" title="输入"></a>输入</h2><p>能做到持续的输出文字，首先需要自己有所积累的同时不断的输入新的内容。要构建自己的知识系统，首先要考虑的是自己知识系统的输入是什么？</p><p>我想我的知识输入主要来自于三个方面：</p><ol><li>泛读书籍</li></ol><p>当我拿到一本书的时候，我需要的是快速的建立印象。略读了解书的结构，知道书的每个章节大致覆盖的内容，在脑子为这本书建立索引。这个时候的读书笔记，或者读书心得就好像一份落地的索引。为将来需要的时候提供查询的依据。</p><ol><li>研究技术</li></ol><p>这个时候的阅读，就比较有目的性了。对于某个领域的专业知识，依托第一步产生的索引。可以在众多资料中快速定位。成体系，成系统的学习，然后整理消化。</p><ol><li>工作中的总结</li></ol><p>学习的目的就是使用。在实际使用知识的时候，必然会有各种各样的挑战，这个时候就需要逐步的调试，重复的验证，考验之前的知识体系。每一次解决某个问题，就为我们知识体系打上一个补丁。整项工作完成后需要回顾总结，归档。</p><p>总结一下，四个步骤:<br>第一步,摊大饼，建索引。第二步，抓住某个点，体系学习。第三步，实际应用，发现知识盲区，及时打补丁。第四步，总结归档。</p><h2 id="加工"><a href="#加工" class="headerlink" title="加工"></a>加工</h2><p>了解了写作的素材的来源，就需要时合适的工具，加工知识。</p><ol><li>对于电子书，我使用 <a href="http://marginnote.webflow.io/">MarginNote</a> 这个软件来阅读。MarginNote 是一款，集文档管理，标注，思维导图，大纲等功能于一体的学习软件。可以说功能相当强大。</li></ol><p><img data-src="/images/2019-04-25-22232.jpg" alt="img"></p><p>通这个软件，可以迅速的建立索引，实现把书读薄的目的。 同时 MarginNote 还有更多其他用法，大家可以到他的官网了解。强烈推荐购买。</p><ol><li>笔记本和纸</li></ol><p>对于实体书，实体的笔记也是得力的助手。对于手写的笔记比较自由，但是思路还是一样的，迅速记录知识要点，同时可以附上自己的思考。</p><p><img data-src="/images/2019-04-25-022235.jpg" alt="img"></p><ol><li>至于如何有效的阅读一本书，推荐大家阅读 <a href="https://book.douban.com/subject/1013208/">《如何阅读一本书》</a>。</li></ol><h2 id="写作"><a href="#写作" class="headerlink" title="写作"></a>写作</h2><p>写作是检测自己是否真正掌握知识的一种手段。如果能够把一个知识真正的讲明白才是，你才真正的掌握这项知识。</p><h3 id="markdown"><a href="#markdown" class="headerlink" title="markdown"></a>markdown</h3><p>写作的核心是使用使用 <a href="https://www.appinn.com/markdown/">markdown</a> 这种无格式标记语言。</p><p>为什么使用 markdown ？</p><p>主要是 markdown 是一种 「易读易写」 的纯文本标记语法。语法是由限个（常用不超过20个）符合组成，并没有太大的学习成本。</p><p>纯文本的好处就是，不依赖与特定的工具就能编写阅读。与其相反的就是 M$ 的 Office 系列软件。比如 Docx 文件就必须在大型的 Office 条件中才能使用，同时使用 M$ word 的时候，时刻要担心格式和排版的问题。</p><p>而对于 markdown 用户来说，在写作的时候，就只需要关注内容。等需要排版的时候，再交由专业的工具来完成。</p><p>这里推荐几个我用过，比较好用的 markdown 编辑器：</p><ul><li><a href="https://zh.mweb.im/">MWeb</a>：是一个在 Mac 环境下的优秀的 markdown 文件编辑器。</li></ul><p><img data-src="/images/2019-04-25-022239.jpg" alt="MWeb"></p><p>使用门槛比较低，同时提供很多高级功能。</p><p><img data-src="/images/2019-04-25-022240.jpg" alt="img"></p><p>功能也比较强大，支持文档导出 PDF，HTML，同时有比较友好的图片解决方案。</p><p>缺点：不支持版本控制工具，不能正确识别 hexo 的 yml 配置文件。不过如果不是程序员用户 MWeb 可以说没有缺点。</p><ul><li><a href="https://code.visualstudio.com/">Visual Studio code</a></li></ul><p><img data-src="/images/2019-04-25-022241.jpg" alt="img"></p><p>对于程序员来说 Vs code 简直就是完美的 markdown 解决方案。Vs code 默认就极好的支持了 markdown 语法。</p><p><img data-src="/images/2019-04-25-22242.jpg" alt="img"></p><p>优点：</p><ul><li>无缝集成 Github</li><li>通过安装插件各种模板语言</li><li>可以直接操作终端</li><li>支持 markdown 预览</li><li>无缝集成 hexo，</li><li>一站式解决写作，排版，发布，备份等工作。</li></ul><p>缺点：</p><ul><li>对于非技术人员门槛过高。</li></ul><h2 id="输出"><a href="#输出" class="headerlink" title="输出"></a>输出</h2><p>完成了写作之后，就需要考虑如何呈现给读者。</p><h3 id="图床"><a href="#图床" class="headerlink" title="图床"></a>图床</h3><ol><li>七牛云，目前对备案，域名要求越来越高，如果搞定了备案，好用。</li><li>阿里云 OSS，我的服务器托管在aliyun，顺手买了一个 OSS，目前来看功能强大，价格也实惠，推荐。</li><li>如果以上还是门口比较高，推荐一个神器 <a href="https://toolinbox.net/iPic/">iPic</a>。只需要把图片拖拽到他的图标上，一键上传，生成 Markdown 的链接。免费版直接使用微博的图床，支持 https，唯一的缺点就是哪天微博不高兴了取消了api，就不能用了吧。</li></ol><h2 id="图片压缩"><a href="#图片压缩" class="headerlink" title="图片压缩"></a>图片压缩</h2><p>一般我们直接截图的文件尺寸都很大，影响页面加载速度，可以使用 <a href="https://tinypng.com/">TinyPng</a> 在不损失图片质量的情况下，尽可能的压缩图片文件大小。</p><h2 id="排版"><a href="#排版" class="headerlink" title="排版"></a>排版</h2><p>由于我自己使用 hexo 作为静态博客的管理工具，hexo 直接支持 markdown 格式。所以直接使用 hexo 编译 markdown 就能获得很好的效果。</p><p>对于<a href="https://juejin.im/timeline">掘金</a>、<a href="http://www.jianshu.com/">简书</a>、<a href="https://www.zhihu.com/">知乎</a>等直接支持 markdown 内容平台，那就再好不过了。直接把源文件粘贴进去–完美。</p><p>对于微信公众号和头条号来说，推荐两个排版工具给大家：</p><ol><li><p><a href="https://markdown-here.com/">Markdown Here</a> : 是一个浏览器插件。可以解决大部分富文本编辑器的排版问题。功能及其强大，但是对于一个不会写 css 的后端程序员来说，预设的主题较少，自己定制又不会。比较尴尬。</p></li><li><p>颜家大少提供的 <a href="http://md.aclickall.com/">Md2All</a> 只要把 Markdown 源文件复制到页面中，点击 “复制” 然后粘贴到微信公众编辑页面。直接搞到格式和图片可以说相当靠谱和。大家看到我的微信公众号里面的文章都是用这个工具排版。</p></li></ol><h2 id="备份"><a href="#备份" class="headerlink" title="备份"></a>备份</h2><p>直接使用 github 管理文章，文章写完以后 push 到远程分支。同时定期打包 zip 放到坚果云。</p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>这篇文章包含了我这几年写作的心得,还有写作过程中使用的一些工具。希望能对你有所帮助。如有更好的工具，也欢迎你留言告诉我。</p>]]>
    </content>
    <id>https://xilidou.com/2018/08/17/write-tools/</id>
    <link href="https://xilidou.com/2018/08/17/write-tools/"/>
    <published>2018-08-17T14:31:00.000Z</published>
    <summary>
      <![CDATA[<p><img src="/images/2019-04-25-022227.jpg" alt="img"></p>
<p>写作是技术输出的重要手段。自己也写了一年多的文章，累计也超过五万多字。今天就想谈谈自己对于写作的一些看法以及写作时使用到的工具。工欲善其事必先利其器。</p>]]>
    </summary>
    <title>我的写作工具链</title>
    <updated>2026-08-08T02:04:49.447Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="docx" scheme="https://xilidou.com/tags/docx/"/>
    <category term="pdf" scheme="https://xilidou.com/tags/pdf/"/>
    <category term="office" scheme="https://xilidou.com/tags/office/"/>
    <content>
      <![CDATA[<p><img data-src="/images/2019-04-25-022249.jpg" alt="img"></p><p>最近做了一个比较有意思的需求，实现的比较有意思。</p><h2 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h2><ol><li>用户上传一个 docx 文件，文档中有占位符若干，识别为文档模板。</li><li>用户在前端可以将标签拖拽到模板上，替代占位符。</li><li>后端根据标签，获取标签内容，生成 pdf 文档并打上水印。</li></ol><h2 id="需求实现的难点"><a href="#需求实现的难点" class="headerlink" title="需求实现的难点"></a>需求实现的难点</h2><ol><li>模板文件来自业务方，财务，执行等角色，不可能使用类似 （freemark、velocity、Thymeleaf） 技术常用的模板标记语言。</li><li>文档在上传后需要解析，生成 html 供前端拖拽标签，同时渲染的最终文档是 pdf 。由于生成的 pdf 是正式文件，必须要求格式严格保证。</li><li>前端如果直接使用富文本编辑器，目前开源没有比较满意的实现，同时自主开发富文本需要极高技术含量。所以不考虑富文本编辑器的可能。</li></ol><span id="more"></span><h2 id="技术调研和技术选型（Java-技术栈）"><a href="#技术调研和技术选型（Java-技术栈）" class="headerlink" title="技术调研和技术选型（Java 技术栈）"></a>技术调研和技术选型（Java 技术栈）</h2><h3 id="1-对-docx-文档格式的转换"><a href="#1-对-docx-文档格式的转换" class="headerlink" title="1. 对 docx 文档格式的转换"></a>1. 对 docx 文档格式的转换</h3><p>一顿google以后发现了 StackOverflow 上的这个回答：<a href="https://stackoverflow.com/questions/43363624/converting-docx-into-pdf-in-java">Converting docx into pdf in java</a> 使用如下的 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">Apache POI 3.15</span><br><span class="line">org.apache.poi.xwpf.converter.core-1.0.6.jar</span><br><span class="line">org.apache.poi.xwpf.converter.pdf-1.0.6.jar</span><br><span class="line">fr.opensagres.xdocreport.itext.extension-2.0.0.jar</span><br><span class="line">itext-2.1.7.jar</span><br><span class="line">ooxml-schemas-1.3.jar</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>实际上写了一个 Demo 测试以后发现，这套组合以及年久失修，对于复杂的 docx 文档都不能友好支持，代码不严谨，不时有 Nullpoint 的异常抛出，还有莫名的jar包冲突的错误，最致命的一个问题是，不能严格保证格式。复杂的序号会出现各种问题。 pass。</p><p> 第二种思路，使用 <a href="https://www.libreoffice.org/">LibreOffice</a>, LibreOffice 提供了一套 api 可以提供给 java 程序调用。<br>所以使用 <a href="https://github.com/sbraconnier/jodconverter">jodconverter</a> 来调用 LibreOffice。之前网上搜到的教程早就已经过时。jodconverter 早就推出了 4.2 版本。最靠谱的文档还是直接看官方提供的<a href="https://github.com/sbraconnier/jodconverter/wiki">wiki</a>。</p><h3 id="2-渲染模板"><a href="#2-渲染模板" class="headerlink" title="2. 渲染模板"></a>2. 渲染模板</h3><p>第一种思路，将 docx 装换为 html 的纯文本格式，再使用 Java 现有的模板引擎（freemark，velocity）渲染内容。但是 docx 文件装换为 html 还是会有极大的格式损失。 pass。</p><p>第二种思路。直接操作 docx 文档在 docx 文档中直接将占位符替换为内容。这样保证了格式不会损失，但是没有现成的模板引擎可以支持 docx 的渲染。需要自己实现。</p><h3 id="3-水印"><a href="#3-水印" class="headerlink" title="3. 水印"></a>3. 水印</h3><p>这个相对比较简单，直接使用 <a href="https://itextpdf.com/">itextpdf</a> 免费版就能解决问题。需要注意中文的问题字体，下文会逐步讲解。</p><h2 id="关键技术实现"><a href="#关键技术实现" class="headerlink" title="关键技术实现"></a>关键技术实现</h2><h3 id="jodconverter-libreoffice-的使用"><a href="#jodconverter-libreoffice-的使用" class="headerlink" title="jodconverter + libreoffice 的使用"></a>jodconverter + libreoffice 的使用</h3><p><code>jodconverter</code> 已经提供了一套完整的<code>spring-boot</code>解决方案,只需要在 <code>pom.xml</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><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="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.jodconverter<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>jodconverter-local<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>4.2.0<span class="tag">&lt;/<span class="name">version</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">dependenc</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="tag">&lt;<span class="name">groupId</span>&gt;</span>org.jodconverter<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>jodconverter-spring-boot-starter<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>4.2.0<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><span class="line"></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><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><br><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ApplicationConfig</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> OfficeManager officeManager;</span><br><span class="line">    <span class="meta">@Bean</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> DocumentConverter <span class="title">documentConverter</span><span class="params">()</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> LocalConverter.builder()</span><br><span class="line">                .officeManager(officeManager)</span><br><span class="line">                .build();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>在配置文件 <code>application.properties</code> 中添加：</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></pre></td><td class="code"><pre><span class="line"><span class="comment"># libreoffice 安装目录</span></span><br><span class="line">jodconverter.local.office-home=/Applications/LibreOffice.app/Contents</span><br><span class="line"><span class="comment"># 开启jodconverter</span></span><br><span class="line">jodconverter.local.enabled=<span class="literal">true</span></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><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> DocumentConverter documentConverter;</span><br><span class="line"><span class="keyword">private</span> <span class="keyword">byte</span>[] docxToPDF(InputStream inputStream) &#123;</span><br><span class="line">    <span class="keyword">try</span> (ByteArrayOutputStream byteArrayOutputStream = <span class="keyword">new</span> ByteArrayOutputStream()) &#123;</span><br><span class="line">        documentConverter</span><br><span class="line">                .convert(inputStream)</span><br><span class="line">                .as(DefaultDocumentFormatRegistry.DOCX)</span><br><span class="line">                .to(byteArrayOutputStream)</span><br><span class="line">                .as(DefaultDocumentFormatRegistry.PDF)</span><br><span class="line">                .execute();</span><br><span class="line">        <span class="keyword">return</span> byteArrayOutputStream.toByteArray();</span><br><span class="line">    &#125; <span class="keyword">catch</span> (OfficeException | IOException e) &#123;</span><br><span class="line">        log.error(<span class="string">&quot;convert pdf error&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>就将 docx 转换为 pdf。注意流需要关闭，防止内存泄漏。</p><h3 id="模板的渲染"><a href="#模板的渲染" class="headerlink" title="模板的渲染"></a>模板的渲染</h3><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><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><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">OfficeService</span></span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//占位符 &#123;&#125;</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Pattern SymbolPattern = Pattern.compile(<span class="string">&quot;\\&#123;(.+?)\\&#125;&quot;</span>, Pattern.CASE_INSENSITIVE);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">byte</span>[] replaceSymbol(InputStream inputStream,Map&lt;String,String&gt; symbolMap) <span class="keyword">throws</span> IOException &#123;</span><br><span class="line">        XWPFDocument doc = <span class="keyword">new</span> XWPFDocument(inputStream)</span><br><span class="line">        replaceSymbolInPara(doc,symbolMap);</span><br><span class="line">        replaceInTable(doc,symbolMap)</span><br><span class="line">        <span class="keyword">try</span>(ByteArrayOutputStream os = <span class="keyword">new</span> ByteArrayOutputStream()) &#123;</span><br><span class="line">            doc.write(os);</span><br><span class="line">            <span class="keyword">return</span> os.toByteArray();</span><br><span class="line">        &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">            inputStream.close();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">int</span> <span class="title">replaceSymbolInPara</span><span class="params">(XWPFDocument doc,Map&lt;String,String&gt; symbolMap)</span></span>&#123;</span><br><span class="line">        XWPFParagraph para;</span><br><span class="line">        Iterator&lt;XWPFParagraph&gt; iterator = doc.getParagraphsIterator();</span><br><span class="line">        <span class="keyword">while</span>(iterator.hasNext())&#123;</span><br><span class="line">            para = iterator.next();</span><br><span class="line">            replaceInPara(para,symbolMap);</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="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">replaceInPara</span><span class="params">(XWPFParagraph para,Map&lt;String,String&gt; symbolMap)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        List&lt;XWPFRun&gt; runs;</span><br><span class="line">        <span class="keyword">if</span> (symbolMatcher(para.getParagraphText()).find()) &#123;</span><br><span class="line">            String text = para.getParagraphText();</span><br><span class="line">            Matcher matcher3 = SymbolPattern.matcher(text);</span><br><span class="line">            <span class="keyword">while</span> (matcher3.find()) &#123;</span><br><span class="line">                String group = matcher3.group(<span class="number">1</span>);</span><br><span class="line">                String symbol = symbolMap.get(group);</span><br><span class="line">                <span class="keyword">if</span> (StringUtils.isBlank(symbol)) &#123;</span><br><span class="line">                    symbol = <span class="string">&quot; &quot;</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                text = matcher3.replaceFirst(symbol);</span><br><span class="line">                matcher3 = SymbolPattern.matcher(text);</span><br><span class="line">            &#125;</span><br><span class="line">            runs = para.getRuns();</span><br><span class="line">            String fontFamily = runs.get(<span class="number">0</span>).getFontFamily();</span><br><span class="line">            <span class="keyword">int</span> fontSize = runs.get(<span class="number">0</span>).getFontSize();</span><br><span class="line">            XWPFRun xwpfRun = para.insertNewRun(<span class="number">0</span>);</span><br><span class="line">            xwpfRun.setFontFamily(fontFamily);</span><br><span class="line">            xwpfRun.setText(text);</span><br><span class="line">            <span class="keyword">if</span>(fontSize &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                xwpfRun.setFontSize(fontSize);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">int</span> max = runs.size();</span><br><span class="line">            <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">1</span>; i &lt; max; i++) &#123;</span><br><span class="line">                para.removeRun(<span class="number">1</span>);</span><br><span class="line">            &#125;</span><br><span class="line"></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="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">replaceInTable</span><span class="params">(XWPFDocument doc,Map&lt;String,String&gt; symbolMap)</span> </span>&#123;</span><br><span class="line">        Iterator&lt;XWPFTable&gt; iterator = doc.getTablesIterator();</span><br><span class="line">        XWPFTable table;</span><br><span class="line">        List&lt;XWPFTableRow&gt; rows;</span><br><span class="line">        List&lt;XWPFTableCell&gt; cells;</span><br><span class="line">        List&lt;XWPFParagraph&gt; paras;</span><br><span class="line">        <span class="keyword">while</span> (iterator.hasNext()) &#123;</span><br><span class="line">            table = iterator.next();</span><br><span class="line">            rows = table.getRows();</span><br><span class="line">            <span class="keyword">for</span> (XWPFTableRow row : rows) &#123;</span><br><span class="line">                cells = row.getTableCells();</span><br><span class="line">                <span class="keyword">for</span> (XWPFTableCell cell : cells) &#123;</span><br><span class="line">                    paras = cell.getParagraphs();</span><br><span class="line">                    <span class="keyword">for</span> (XWPFParagraph para : paras) &#123;</span><br><span class="line">                        replaceInPara(para,symbolMap);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</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="function"><span class="keyword">private</span> Matcher <span class="title">symbolMatcher</span><span class="params">(String str)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> SymbolPattern.matcher(str);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><em>这里需要特别注意</em>：</p><ol><li>在解析的文档中，<code>para.getParagraphText()</code>指的是获取段落，<code>para.getRuns()</code>应该指的是获取词。但是问题来了，获取到的 runs 的划分是一个谜。目前我也没有找到规律，很有可能我们的占位符被划分到了多个<code>run</code>中，我们并不是简单的针对 <code>run</code> 做正则表达的替换，而要先把所有的 <code>runs</code> 组合起来再进行正则替换。</li><li>在调用<code>para.insertNewRun()</code>的时候 <code>run</code> 并不会保持字体样式和字体大小需要手动获取并设置。<br>由于以上两个蜜汁实现，所以就写了一坨蜜汁代码才能保证正则替换和格式正确。</li></ol><p>test 方法：</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">replaceSymbol</span><span class="params">()</span> <span class="keyword">throws</span> IOException </span>&#123;</span><br><span class="line">    File file = <span class="keyword">new</span> File(<span class="string">&quot;symbol.docx&quot;</span>);</span><br><span class="line">    InputStream inputStream = <span class="keyword">new</span> FileInputStream(file);</span><br><span class="line"></span><br><span class="line">    File outputFile = <span class="keyword">new</span> File(<span class="string">&quot;out.docx&quot;</span>);</span><br><span class="line">    FileOutputStream outputStream = <span class="keyword">new</span> FileOutputStream(outputFile);</span><br><span class="line">    Map&lt;String,String&gt; map = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line">    map.put(<span class="string">&quot;tableName&quot;</span>,<span class="string">&quot;水果价目表&quot;</span>);</span><br><span class="line">    map.put(<span class="string">&quot;name&quot;</span>,<span class="string">&quot;苹果&quot;</span>);</span><br><span class="line">    map.put(<span class="string">&quot;price&quot;</span>,<span class="string">&quot;1.5/斤&quot;</span>);</span><br><span class="line">    <span class="keyword">byte</span>[] bytes = office.replaceSymbol(inputStream, map, );</span><br><span class="line"></span><br><span class="line">    outputStream.write(bytes);</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><code>replaceSymbol()</code> 方法接受两个参数，一个是输入的docx文件数据流，另一个是占位符和内容的map。</p><p>这个方法使用前：</p><p><img data-src="/images/2019-04-25-22250.jpg" alt="before"></p><p>使用后：<br><img data-src="/images/2019-04-25-022250.jpg" alt="after"></p><h3 id="增加水印"><a href="#增加水印" class="headerlink" title="增加水印"></a>增加水印</h3><p><code>pom.xml</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><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- https://mvnrepository.com/artifact/com.itextpdf/itextpdf --&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>com.itextpdf<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>itextpdf<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.5.13<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>增加水印的代码：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">byte</span>[] addWatermark(InputStream inputStream,String watermark) <span class="keyword">throws</span> IOException, DocumentException &#123;</span><br><span class="line"></span><br><span class="line">    PdfReader reader = <span class="keyword">new</span> PdfReader(inputStream);</span><br><span class="line">    <span class="keyword">try</span>(ByteArrayOutputStream os = <span class="keyword">new</span> ByteArrayOutputStream()) &#123;</span><br><span class="line">        PdfStamper stamper = <span class="keyword">new</span> PdfStamper(reader, os);</span><br><span class="line">        <span class="keyword">int</span> total = reader.getNumberOfPages() + <span class="number">1</span>;</span><br><span class="line">        PdfContentByte content;</span><br><span class="line">        <span class="comment">// 设置字体</span></span><br><span class="line">        BaseFont baseFont = BaseFont.createFont(<span class="string">&quot;simsun.ttf&quot;</span>, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);</span><br><span class="line">        <span class="comment">// 循环对每页插入水印</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">1</span>; i &lt; total; i++) &#123;</span><br><span class="line">            <span class="comment">// 水印的起始</span></span><br><span class="line">            content = stamper.getUnderContent(i);</span><br><span class="line">            <span class="comment">// 开始</span></span><br><span class="line">            content.beginText();</span><br><span class="line">            <span class="comment">// 设置颜色</span></span><br><span class="line">            content.setColorFill(<span class="keyword">new</span> BaseColor(<span class="number">244</span>, <span class="number">244</span>, <span class="number">244</span>));</span><br><span class="line">            <span class="comment">// 设置字体及字号</span></span><br><span class="line">            content.setFontAndSize(baseFont, <span class="number">50</span>);</span><br><span class="line">            <span class="comment">// 设置起始位置</span></span><br><span class="line">            content.setTextMatrix(<span class="number">400</span>, <span class="number">780</span>);</span><br><span class="line">            <span class="keyword">for</span> (<span class="keyword">int</span> x = <span class="number">0</span>; x &lt; <span class="number">5</span>; x++) &#123;</span><br><span class="line">                <span class="keyword">for</span> (<span class="keyword">int</span> y = <span class="number">0</span>; y &lt; <span class="number">5</span>; y++) &#123;</span><br><span class="line">                    content.showTextAlignedKerned(Element.ALIGN_CENTER,</span><br><span class="line">                            watermark,</span><br><span class="line">                            (<span class="number">100f</span> + x * <span class="number">350</span>),</span><br><span class="line">                            (<span class="number">40.0f</span> + y * <span class="number">150</span>),</span><br><span class="line">                            <span class="number">30</span>);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            content.endText();</span><br><span class="line">        &#125;</span><br><span class="line">        stamper.close();</span><br><span class="line">        <span class="keyword">return</span> os.toByteArray();</span><br><span class="line">    &#125;<span class="keyword">finally</span> &#123;</span><br><span class="line">        reader.close();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="字体"><a href="#字体" class="headerlink" title="字体"></a>字体</h3><ol><li>使用文档的时候，字体也同样重要，如果你使用了 libreOffice 没有的字体，比如宋体。需要把字体文件 <code>xxx.ttf</code></li></ol><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">cp xxx.ttc /usr/share/fonts</span><br><span class="line">fc-cache -fv</span><br></pre></td></tr></table></figure><ol><li><code>itextpdf</code> 不支持汉字，需要提供额外的字体：</li></ol><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">//字体路径</span></span><br><span class="line">String fontPath = <span class="string">&quot;simsun.ttf&quot;</span></span><br><span class="line"><span class="comment">//设置字体</span></span><br><span class="line">BaseFont baseFont = BaseFont.createFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>整个需求挺有意思，但是在查询的时候发现中文文档的质量实在堪忧，要么极度过时，要么就是大家互相抄袭。<br>查询一个项目的技术文档，最好的路径应该如下:</p><p>项目官网 Getting Started &#x3D;&#x3D; github demo &gt; StackOverflow &gt;&gt; <del>CSDN</del> &gt;&gt; <del>百度知道</del></p><p>欢迎关注我的微信公众号<br><img data-src="/images/2019-04-25-022251.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2018/08/15/java-doc-pdf/</id>
    <link href="https://xilidou.com/2018/08/15/java-doc-pdf/"/>
    <published>2018-08-15T21:00:38.000Z</published>
    <summary>
      <![CDATA[<p><img src="/images/2019-04-25-022249.jpg" alt="img"></p>
<p>最近做了一个比较有意思的需求，实现的比较有意思。</p>
<h2 id="需求"><a href="#需求" class="headerlink" title="需求"></a>需求</h2><ol>
<li>用户上传一个 docx 文件，文档中有占位符若干，识别为文档模板。</li>
<li>用户在前端可以将标签拖拽到模板上，替代占位符。</li>
<li>后端根据标签，获取标签内容，生成 pdf 文档并打上水印。</li>
</ol>
<h2 id="需求实现的难点"><a href="#需求实现的难点" class="headerlink" title="需求实现的难点"></a>需求实现的难点</h2><ol>
<li>模板文件来自业务方，财务，执行等角色，不可能使用类似 （freemark、velocity、Thymeleaf） 技术常用的模板标记语言。</li>
<li>文档在上传后需要解析，生成 html 供前端拖拽标签，同时渲染的最终文档是 pdf 。由于生成的 pdf 是正式文件，必须要求格式严格保证。</li>
<li>前端如果直接使用富文本编辑器，目前开源没有比较满意的实现，同时自主开发富文本需要极高技术含量。所以不考虑富文本编辑器的可能。</li>
</ol>]]>
    </summary>
    <title>Java 渲染 docx 文件，并生成 pdf 加水印</title>
    <updated>2026-08-08T02:04:49.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="工具" scheme="https://xilidou.com/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="alfred" scheme="https://xilidou.com/tags/alfred/"/>
    <category term="效率" scheme="https://xilidou.com/tags/%E6%95%88%E7%8E%87/"/>
    <category term="tools" scheme="https://xilidou.com/tags/tools/"/>
    <content>
      <![CDATA[<p>最近换工作以后，结结实实的写了几个月的业务。需求完结以后，就找找自己喜欢的东西写写，换个口味。</p><p>撸码最难的就是给变量取名字了。所以就写一个变量生成器吧。</p><span id="more"></span><h2 id="演示如下"><a href="#演示如下" class="headerlink" title="演示如下"></a>演示如下</h2><p><embed src="https://imgcache.qq.com/tencentvideo_v1/playerv3/TPout.swf?max_age=86400&v=20161117&vid=p1343hsm107&auto=0" allowFullScreen="true" quality="high" width="480" height="400" align="middle" allowScriptAccess="always" type="application/x-shockwave-flash"></embed></p><h2 id="实现思路"><a href="#实现思路" class="headerlink" title="实现思路"></a>实现思路</h2><p>使用了 Mac 上最出名的效率工具 <code>Alfred</code>。利用 <code>Alfred</code> 调用本地的 <code>python</code> 脚本，利用 http 模块，请求远程的 API 接口。</p><p>远程 API 获取查询的字符后，首先使用<code>结巴分词</code>，对查询的句子进行分词，然后调用有道词典的 API 翻译，拼接以后返回。</p><p>最终，一个回车就能把结果输入到我们的 IDE 里面减少很多操作，妈妈再也不会担心我取不出变量名啦。</p><h2 id="API-的实现"><a href="#API-的实现" class="headerlink" title="API 的实现"></a>API 的实现</h2><p>既然说换个口味，那 API 我肯定不会使用 ‘Spring mvc’ 啦。</p><p>主要采用的是 ‘vertx’ 这个基于’netty’ 的全异步的 java 库。有兴趣的同学可以参考 <a href="http://vartx.io/">http://vartx.io</a> 。</p><p>使用 Spring boot 管理对象的生命周期。</p><p>使用 “结巴分词” 对查询的语句进行分词。</p><p>使用 guava cache 来对查询结果进行缓存。为啥要缓存？主要是有道的翻译API是收费的，查完把结果缓存起来能节约一点算一点。</p><p>至于为什么使用本地缓存而不是 Redis？因为阿里云的 Redis 一个月要25块钱啊。自己搭一个？我的vps 一共只有 1G 内存啊。</p><p>说到底，架构设计需要考虑实际情况，一味上高大上的技术也不可取。适合的才是最好的。</p><h3 id="vertx-web"><a href="#vertx-web" class="headerlink" title="vertx-web"></a>vertx-web</h3><p>写过 <code>netty</code> 的同学就知道，<code>netty</code> 的业务逻辑是写在一个个的 <code>handler</code>中的。</p><p>同样 <code>vertx</code> 也类似于 <code>netty</code> 也是使用 <code>handler</code> 来处理请求。</p><p>vertx 通过 Router 这个类，将请求路由到不同的 Handler 中。所以我们直接看代码：</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">StaticServer</span> <span class="keyword">extends</span> <span class="title">AbstractVerticle</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Autowired</span></span><br><span class="line">    <span class="keyword">private</span> VariableHandler variableHandler;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">start</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        Router router = Router.router(vertx);</span><br><span class="line">        router.route().handler(BodyHandler.create());</span><br><span class="line">        router.post(<span class="string">&quot;/api/hump&quot;</span>).handler(routingContext -&gt;variableHandler.get(routingContext));</span><br><span class="line">        vertx.createHttpServer().requestHandler(router::accept).listen(<span class="number">8080</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们把 <code>VariableHandler</code> 绑定到了 ’&#x2F;api&#x2F;hump‘ 这个 uri 的 post 方法上了。服务器启动以后会监听 ’8080‘ 端口。 vertx-web的运行是不需要类似 tomcat 这样的容器的。</p><h2 id="RestTemplate"><a href="#RestTemplate" class="headerlink" title="RestTemplate"></a>RestTemplate</h2><p>我们一般是用 <code>Httpclient</code> 在代码中调用 http 接口。但是我觉得 HTTPClient 封装的不是很好。我们可以直接使用 <code>Spring boot web</code> 提供的 RestTemplate （真香）。直接看代码：</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="function"><span class="keyword">private</span> ApiResponse <span class="title">requestYoudao</span><span class="params">(String param)</span></span>&#123;</span><br><span class="line">    <span class="keyword">long</span> timeMillis = System.currentTimeMillis();</span><br><span class="line">    String salt = String.valueOf(timeMillis);</span><br><span class="line">    String sign = Md5Utils.md5(appKey + param + salt + secretKey);</span><br><span class="line">    MultiValueMap&lt;String,String&gt; bodyMap = <span class="keyword">new</span> LinkedMultiValueMap&lt;&gt;();</span><br><span class="line">    bodyMap.add(<span class="string">&quot;q&quot;</span>,param);</span><br><span class="line">    bodyMap.add(<span class="string">&quot;from&quot;</span>,<span class="string">&quot;auto&quot;</span>);</span><br><span class="line">    bodyMap.add(<span class="string">&quot;to&quot;</span>,<span class="string">&quot;auto&quot;</span>);</span><br><span class="line">    bodyMap.add(<span class="string">&quot;appKey&quot;</span>,appKey);</span><br><span class="line">    bodyMap.add(<span class="string">&quot;salt&quot;</span>,salt);</span><br><span class="line">    bodyMap.add(<span class="string">&quot;sign&quot;</span>,sign);</span><br><span class="line">    MultiValueMap&lt;String,String&gt; headersMap = <span class="keyword">new</span> LinkedMultiValueMap&lt;&gt;();</span><br><span class="line">    HttpEntity&lt;MultiValueMap&lt;String, String&gt;&gt; requestEntity  = <span class="keyword">new</span> HttpEntity&lt;&gt;(bodyMap, headersMap);</span><br><span class="line">    <span class="keyword">return</span> restTemplate.postForObject(url, requestEntity,ApiResponse.class);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Guava"><a href="#Guava" class="headerlink" title="Guava"></a>Guava</h2><p>Guava 是 google 提供的一个java 基础库类，如果会使用 Guava 的话，会成倍的提升你的开发效率。在本项目中主要使用 Guava 提供的本地缓存和字符串操作：</p><p>Guava cache 的使用很简单直接看代码：</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">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> Cache&lt;String,ApiResponse&gt; cache;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> ApiResponse <span class="title">cachedResponse</span><span class="params">(String param)</span></span>&#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> cache.get(param, () -&gt; requestYoudao(param));</span><br><span class="line">    &#125;<span class="keyword">catch</span> (Exception e)&#123;</span><br><span class="line">log.error(<span class="string">&quot;error&quot;</span>,e);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Guava 对提供了很多给力的字符串的操作。尤其是对字符串下划线，大小写，驼峰形式，提供的强有力的支持。这样使得我们的 API 提供各种风格的变量形式。我们直接看代：</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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">switch</span> (status)&#123;</span><br><span class="line">    <span class="keyword">case</span> Constants.LOWER_CAMEL:</span><br><span class="line">        <span class="keyword">return</span> CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL,underline);</span><br><span class="line">    <span class="keyword">case</span> Constants.LOWER_HYPHEN:</span><br><span class="line">        <span class="keyword">return</span> CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_HYPHEN,underline);</span><br><span class="line">    <span class="keyword">case</span> Constants.LOWER_UNDERSCORE:</span><br><span class="line">        <span class="keyword">return</span> CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_UNDERSCORE,underline);</span><br><span class="line">    <span class="keyword">case</span> Constants.UPPER_CAMEL:</span><br><span class="line">        <span class="keyword">return</span> CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL,underline);</span><br><span class="line">    <span class="keyword">case</span> Constants.UPPER_UNDERSCORE:</span><br><span class="line">        <span class="keyword">return</span> CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE,underline);</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        <span class="keyword">return</span>  CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL,underline);</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>以上就是 API 接口的实现。</p><h2 id="python-脚本"><a href="#python-脚本" class="headerlink" title="python 脚本"></a>python 脚本</h2><p>本地的python 脚本就极其简单了：</p><figure class="highlight python"><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><br><span class="line"><span class="comment"># -*- coding:utf-8 -*-</span></span><br><span class="line"><span class="keyword">import</span> httplib,urllib,json</span><br><span class="line"></span><br><span class="line">url = <span class="string">&#x27;xilidou.com&#x27;</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">query</span>(<span class="params">q,status=<span class="number">0</span></span>):</span></span><br><span class="line">    response = get(q,status)</span><br><span class="line">    dates = json.loads(response.read())</span><br><span class="line">    items = <span class="built_in">list</span>()</span><br><span class="line">    <span class="keyword">for</span> date <span class="keyword">in</span> dates:</span><br><span class="line">        item = &#123;&#125;</span><br><span class="line">        item[<span class="string">&#x27;title&#x27;</span>] = date.encode(<span class="string">&#x27;utf-8&#x27;</span>)</span><br><span class="line">        item[<span class="string">&#x27;arg&#x27;</span>] = date.encode(<span class="string">&#x27;utf-8&#x27;</span>)</span><br><span class="line">        item[<span class="string">&#x27;subtitle&#x27;</span>] = <span class="string">&#x27;回车复制&#x27;</span></span><br><span class="line">        item[<span class="string">&#x27;icon&#x27;</span>] = getIcon()</span><br><span class="line">        items.append(item)</span><br><span class="line">    jsonBean = &#123;&#125;</span><br><span class="line">    jsonBean[<span class="string">&#x27;items&#x27;</span>] = items</span><br><span class="line">    json_str = json.dumps(jsonBean)</span><br><span class="line">    <span class="keyword">if</span> json_str:</span><br><span class="line">        <span class="built_in">print</span> json_str</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">str</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">get</span>(<span class="params">q,status=<span class="number">0</span></span>):</span></span><br><span class="line">    parameters= <span class="built_in">dict</span>()</span><br><span class="line">    parameters[<span class="string">&#x27;q&#x27;</span>] = q</span><br><span class="line">    parameters[<span class="string">&#x27;status&#x27;</span>] = status</span><br><span class="line"></span><br><span class="line">    parameters = urllib.urlencode(parameters)</span><br><span class="line">    headers = &#123;<span class="string">&quot;Content-type&quot;</span>: <span class="string">&quot;application/x-www-form-urlencoded&quot;</span>&#125;</span><br><span class="line"></span><br><span class="line">    conn = httplib.HTTPSConnection(url)</span><br><span class="line">    conn.request(<span class="string">&#x27;POST&#x27;</span>,<span class="string">&#x27;/api/hump&#x27;</span>,parameters,headers)</span><br><span class="line">    response = conn.getresponse()</span><br><span class="line">    <span class="keyword">return</span> response</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">getIcon</span>():</span></span><br><span class="line">    icon = &#123;&#125;</span><br><span class="line">    icon[<span class="string">&#x27;path&#x27;</span>] = <span class="string">&#x27;icon.png&#x27;</span></span><br><span class="line">    <span class="keyword">return</span> icon</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span> __name__ == <span class="string">&#x27;__main__&#x27;</span>:</span><br><span class="line">    query(<span class="string">&#x27;中文&#x27;</span>)</span><br><span class="line"></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>干两件事情：</p><ul><li>从 Alfred 中获取用户输入的待查询字符串。</li><li>调用远程的 API 接口获取返回后格式化然后打印结果。</li></ul><h2 id="Alfred"><a href="#Alfred" class="headerlink" title="Alfred"></a>Alfred</h2><p>大家可以直接下载 github 代码。在 python 文件夹里面找到 <code>hump.alfredworkflow</code> 双击。就安装到你的 Mac 上了。</p><p>前提是你的 Mac 安装了 aflred 且付费成为高级用户。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>老规矩 github 地址：<a href="https://github.com/diaozxin007/HumpApi">https://github.com/diaozxin007/HumpApi</a></p><p>workflow 下载地址：<a href="/images/hump.alfredworkflow">下载</a></p><p>我之前还开发了一个利用 alfred 直接查询有道词典的 workflow。效果如下图：</p><p><img data-src="/images/2019-04-25-022217.jpg" alt="youdao"></p><p>下载地址如下：<a href="https://www.xilidou.com/2017/10/24/%E6%9C%89%E9%81%93-Alfred-Workflow-%E5%A8%81%E5%8A%9B%E5%8A%A0%E5%BC%BA%E7%89%88/">https://www.xilidou.com/2017/10/24/%E6%9C%89%E9%81%93-Alfred-Workflow-%E5%A8%81%E5%8A%9B%E5%8A%A0%E5%BC%BA%E7%89%88/</a></p><p>欢迎关注我的微信公众号：<br><img data-src="/images/2019-04-25-022218.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2018/07/09/hump-api/</id>
    <link href="https://xilidou.com/2018/07/09/hump-api/"/>
    <published>2018-07-09T11:26:04.000Z</published>
    <summary>
      <![CDATA[<p>最近换工作以后，结结实实的写了几个月的业务。需求完结以后，就找找自己喜欢的东西写写，换个口味。</p>
<p>撸码最难的就是给变量取名字了。所以就写一个变量生成器吧。</p>]]>
    </summary>
    <title>撸码的福音--变量名生成器的实现</title>
    <updated>2026-08-08T02:04:49.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="分布式" scheme="https://xilidou.com/categories/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    <category term="分布式" scheme="https://xilidou.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    <category term="raft" scheme="https://xilidou.com/tags/raft/"/>
    <content>
      <![CDATA[<p>好久没有更新博客了，最近研究了Raft 协议，谈谈自己对 Raft 协议的理解。希望这篇文章能够帮助大家理解 <a href="https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf">Raft 论文</a>。</p><span id="more"></span><h2 id="Raft-是什么"><a href="#Raft-是什么" class="headerlink" title="Raft 是什么"></a>Raft 是什么</h2><p>Raft 是一种分布式系统的一致性算法。</p><p>在分布式系统中，我们需要让一组机器作为一个整体向外界提供服务。由于在实际的条件下，我们认为每台机器都是不100%可靠的，随时都可能发生宕机。每台机器之间的通信也不是可靠的，可能发生通信的阻塞、丢失、重试。所以需要某些算法来保证在大多数机器都正常的情况下向外提供可靠的服务。</p><p>在 Raft提出之前，Paxos 已经被提出，但是 Paxos 相当复杂。Raft 的目标就是提出一种易于理解的分布式一致性算法。</p><p>在了解 Raft 之前需要了解一下什么状态机:</p><p>论文指出，Raft 是一种用来管理日志复制的一致性算法。所以我们就要先了解一下。什么是日志复制状态机。我们思考一个问题。如果你要与你的小伙伴分享一个很复杂的操作及计算。一般来说你有两种做法：<br>第一种:你自己负责计算，经过一段时间的计算，算出结果后，直接把计算结果告诉你的小伙伴。<br>第二种:你把每一个操作的步骤都告诉你的伙伴，告诉他怎么做，由你的伙伴自己计算出结果。</p><p>第二种方式，就是复制状态机的工作原理。复制状态机是通过复制日志来实现的。每一台服务器保存着一份日志，日志中包含一系列的命令，状态机会按顺序执行这些命令。因为每一台计算机的状态机都是确定的，所以每个状态机的状态都是相同的，执行的命令是相同的，最后的执行结果也就是一样的了。</p><p>在实际中这种有很多类似的应用比如 mysql 的主从同步就是通过 binlog 进行同步。</p><p>在现实生活中，如何有效的组织多人进行协助，最自然的想法就是选举一个领导，交由领导极大的权威，就能极大的提升整个团队工作效率。</p><p>下面就谈谈我对 Raft 算法的理解。</p><h2 id="基本安全保证"><a href="#基本安全保证" class="headerlink" title="基本安全保证"></a>基本安全保证</h2><p>为了保证过程正确性，Raft需要保证以下的性质时刻为真：</p><ul><li><p>选举安全原则：<br>同一届任期内至多只能有一个领导人。</p></li><li><p>领导人只加原则：<br>领导人的日志只能增加，不能重写或者删除。</p></li><li><p>日志匹配原则：<br>如果两个日志具有相同的任期和索引，则这两段日志在[0,索引]之间的日志完全相同。</p></li><li><p>领导人完全原则：<br>如果一条日志被提交，那么后续的任意任期的领导人都会有这条日志。</p></li><li><p>状态机安全原则：<br>如果一个服务器已经将给定索引位置的日志条目应用到状态机中，则所有其他服务器不会在该索引位置应用不同的条目。</p></li></ul><h2 id="选取领导者"><a href="#选取领导者" class="headerlink" title="选取领导者"></a>选取领导者</h2><p>所以 Raft 算法成立的最重要的前提之一就是选举。</p><ul><li>Raft 由多个节点组成。</li><li>强领导者， 整个 Raft 在同一时间，只有一个领导者，日志有领导者负责分发和同步。</li><li>领导选举， 领导是由民主选举产生的，集群中多数节点投票通过就能成为主。</li></ul><p>对于在集群中的节点。在任意时间中，都有可能处于以下三种状态之一：</p><ul><li>跟随者</li><li>候选人</li><li>领导人</li></ul><p>每个领导人都有一个任期限制。每一届任期的开始阶段，都是选举。如果选举出了领导者就由该领导人负责领导集群。如果没有选举出领导，就会进入下一次选举。直到选举出领导者为止。</p><p>角色之间的转换：</p><p><img data-src="/images/raft_role.png" alt="role"></p><p>领导者会周期性的向每台机器发送心跳，确保自己的领导地位。</p><p>跟随者在长时间没有收到领导人的心跳，就会发起投票成为候选人，同时任期 + 1，如果获得超过半数的支持，就升任为领导。</p><p>如果候选人，在发起投票的时候，发现集群里面有领导人的时候，就会重新成为追随者。</p><p>如果候选人，发起投票后，一定时间里面没有收到超过半数的反馈，就会再次发起投票。</p><p>如果领导者发现在集群中发现存在下一任期的领导者，就会变为追随者。</p><h2 id="日志同步"><a href="#日志同步" class="headerlink" title="日志同步"></a>日志同步</h2><p>在选举出领导人以后，就开始处理客户端的日志。</p><p>领导者在收到客户端的请求，每个请求包含一个操作的命令。领导者会将命令记录到自己的日志中，并向自己的追随者发起同步的请求，要求自己的追随者复制这个命令。</p><p>一旦这个命令被大多数的追随者保存了。领导者就认为这个状态已经处于提交（commited）的状态。同时告知客户端，命令已经被提交。如果这个时候，追随者发生了崩溃或者延时。领导者会一直尝试重试，直到追随者接受命令，并存储到自己的日志中。这个过程一直持续到所有的追随者最终存储了所有的日志条目。</p><p>作为 Raft 的节点需要保证如下性质。</p><ul><li>如果在不同日志中的两个条目有着相同的索引和任期号，则它们所存储的命令是相同的。</li><li>如果在不同日志中的两个条目有着相同的索引和任期号，则它们之间的所有条目都是完全一样的。</li></ul><p>有了如上性质的保证。如果在某些情况下，发生了追随者的日志与领导者不同步的情况。（包括的情况，就可能是丢失日志，或者保存了领导者没有的日志，或者两兼有），在 Raft 算法中，领导人通过强制追随者们复制它的日志来处理日志的不一致。这就意味着，在追随者上的冲突日志会被领导者的日志覆盖。</p><p>为了使得追随者的日志同自己的一致，领导人需要找到追随者同它的日志一致的地方，然后删除追随者在该位置之后的条目，然后将自己在该位置之后的条目发送给追随者。</p><h2 id="安全分析"><a href="#安全分析" class="headerlink" title="安全分析"></a>安全分析</h2><p>需要分析在各种情况下，每个角色发生宕机，数据的安全性。</p><h3 id="选举限制"><a href="#选举限制" class="headerlink" title="选举限制"></a>选举限制</h3><p>Raft 保证自己的日志，永远由领导者向追随者流动。也就是说领导者永远不会删改自己的日志，只能向上增加日志。为了达成这个限制，Raft 使用投票的方式来阻止没有包含全部日志条目的服务器赢得选举。</p><p>当一个候选人发起投票的时候，需要告诉大家，自己最新的日志。其他节点在投票的时候，要保证自己的日志不能比候选人的新，否则就拒绝投票。通过这个限制就保证了获取多数票的领导者的日志，至少比大多数人要新。</p><p>任期越大，日志越长，越容易成为领导者。</p><h3 id="提交之前任期的日志条目"><a href="#提交之前任期的日志条目" class="headerlink" title="提交之前任期的日志条目"></a>提交之前任期的日志条目</h3><p><img data-src="/images/raft_error.png" alt="erro"></p><p><del>这个在论文中比较难以理解。我看到这一节的时候也是读了好几遍才理解论文的意思。实际上作者表达的意思是图 （d）是正确的，而（e）是错误的。</del></p><p><del>因为 2 号日志没有commited，但是由于一系列操作，造成了 2 号日志没有提交，但是高任期的leader 却认为 2 号日志被提交了。</del></p><p>与知乎网友讨论发现这个地方还是理解有误，这个图后来作者换了一个更容易理解的图：</p><p><img data-src="/images/raft2.jpg" alt="error2"></p><p>应该是说，如果高term的leader，可以操作低任期的 log 的话，会造成 d 和 e 情况错误。且 d 造成了 2 号日志的丢失。所以加上限制以后，就不会出现这种问题了。</p><p>为了解决这个问题。Raft 限制，只有当前任期的 leader 可以决定一条日志是否 commited，而不能由高任期的 leader 通过计算某条日志（例子中的 2号日志）超过半数节点持有，就确定日志被commited。</p><p>换句话说，就是 Raft 限制每个leader 只能确定自己任期内的日志是否commited。而不能由高任期的 leader确定。</p><h2 id="追随者和候选人崩溃"><a href="#追随者和候选人崩溃" class="headerlink" title="追随者和候选人崩溃"></a>追随者和候选人崩溃</h2><p>由于 Raft 是一个强领导的，少数服从多数的系统。上面花了了很多的篇幅讨论 leader 奔溃后 Raft 协议是如何保证准确性和安全性的。如果追随者或者候选人挂了，就比较简单了。</p><p>如果候选人崩溃，一段时间以后，某个节点会出发超时，重新发起选举，一切就回复正常了。</p><p>如果一个追随者崩溃，会被 leader 感知。 leader 会一直重试，直到追随者恢复，并同步所有日志。</p><h2 id="系统的扩容"><a href="#系统的扩容" class="headerlink" title="系统的扩容"></a>系统的扩容</h2><p>分布式系统一大优势就是能够快速扩容。</p><p>Raft 为了保证扩容的安全性，采用了两段two-phase）方法。</p><p>在C<sub>old</sub> 和 C<sub>new</sub> 之间存在一个中间态， C<sub>old,new</sub> 的状态。防止刚开始扩容的时候，新的一组机器数量大于老集群数量，就有可能在新机器中自发投票选举出一个 leader，造成集群中有两个leader形成脑裂。</p><ul><li>日志条目被复制给集群中新、老配置的所有服务器。</li><li>新、老配置的服务器都能成为领导人。</li><li>需要分别在两种配置上获得大多数的支持才能达成一致（针对选举和提交）</li></ul><p>需要解决三个问题：</p><ul><li>为了不拖慢整个集群相应速度，可以不给新加入的节点投票权。知道日志追齐以后再开放投票权力</li><li>如果扩容以后，老的 leader 属于被踢出的节点，老 leader 不会立即下线，而是继续工作，直到 C<sub>new</sub> 被提交。这个时候 leader 自己只负责管理集群而自己不追加日志。</li><li>将要被被删除的节点，不会收到领导的心跳，就会不停的认为自己超时，会不断的成为候选人，并不断的发起投票。造成集群的 leader 不断的退位，然后再次产生 leader。造成集群的响应能力降低。为了避免这个问题，当服务器确认当前领导人存在时，服务器会忽略请求投票。每个服务器在开始一次选举之前，至少等待一个最小选举超时时间。</li></ul><h2 id="日志的压缩"><a href="#日志的压缩" class="headerlink" title="日志的压缩"></a>日志的压缩</h2><p>日志的压缩比较容易理解，随着集群的使用，日志的数量越来越大，就会降低集群的性能，同时占用大量的存储空间。所以需要定期对日志进行压缩。快照是最简单的压缩方法。在快照系统中，整个系统的状态都以快照的形式写入到稳定的持久化存储中，然后到那个时间点之前的日志全部丢弃。</p><h2 id="客户端交互"><a href="#客户端交互" class="headerlink" title="客户端交互"></a>客户端交互</h2><p>整个 Raft 协议中，客户端只与 leader 进行交互。</p><p>客户端与集群通信的时候，首先随便与集群中的任意一个节点交互，询问 leader 是谁。</p><p>是客户端对于每一条指令都赋予一个唯一的序列号。然后，状态机跟踪每条指令最新的序列号和相应的响应。如果接收到一条指令，它的序列号已经被执行了，那么就立即返回结果，而不重新执行指令。这样保证交互的命令是幂等的。如果一条命令被重复提交，并不会造成状态机的错误。</p><p>对于读取的命令来说，如领导人已经被废黜，而自己不知道。就容易造成客户端读取到脏数据。最新的数据由别的 leader 维护了。为了避免这个问题：</p><ul><li>领导人必须拥有最新的数据，这一点是必然的。Raft 天然保证这个特性。</li><li>领导人在访问数据之前需要发送一次心跳，保证自己的领导地位。</li></ul><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><ul><li><a href="https://raft.github.io/">Raft 首页</a></li><li><a href="https://github.com/maemual/raft-zh_cn">Raft 中文翻译</a></li><li><a href="https://github.com/wenweihu86/raft-java.git">Raft java 实现</a></li></ul>]]>
    </content>
    <id>https://xilidou.com/2018/06/04/raft/</id>
    <link href="https://xilidou.com/2018/06/04/raft/"/>
    <published>2018-06-04T21:40:31.000Z</published>
    <summary>
      <![CDATA[<p>好久没有更新博客了，最近研究了Raft 协议，谈谈自己对 Raft 协议的理解。希望这篇文章能够帮助大家理解 <a href="https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf">Raft 论文</a>。</p>]]>
    </summary>
    <title>Raft 协议学习笔记</title>
    <updated>2026-08-08T02:04:49.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="rpc" scheme="https://xilidou.com/tags/rpc/"/>
    <category term="dubbo" scheme="https://xilidou.com/tags/dubbo/"/>
    <content>
      <![CDATA[<p>今天开始将开启 dubbo 的源码研究。</p><p>dubbo 是什么？</p><p>dubbo 是阿里巴巴开发的一个基于 java 的开源的 RPC 框架。所谓 RPC 指的的是 Remote Procedure Call Protocol 远程过程调用协议。</p><span id="more"></span><h2 id="阅读代码前的准备"><a href="#阅读代码前的准备" class="headerlink" title="阅读代码前的准备"></a>阅读代码前的准备</h2><ol><li>下载代码：</li></ol><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">git clone https://github.com/apache/incubator-dubbo.git</span><br></pre></td></tr></table></figure><ol><li>IDE 支持</li></ol><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mvn idea:idea</span><br></pre></td></tr></table></figure><p>然后就可以自由的玩耍了。</p><h2 id="架构"><a href="#架构" class="headerlink" title="架构"></a>架构</h2><p>我们看代码包的结构：</p><p><img data-src="/images/dubbo.jpeg" alt="dubbo code"></p><ul><li>dubbo-common 公共逻辑模块：包括 Util 类和通用模型。</li><li>dubbo-remoting 远程通讯模块：相当于 Dubbo 协议的实现，如果 RPC 用 RMI协议则不需要使用此包。</li><li>dubbo-rpc 远程调用模块：抽象各种协议，以及动态代理，只包含一对一的调用，不关心集群的管理。</li><li>dubbo-cluster 集群模块：将多个服务提供方伪装为一个提供方，包括：负载均衡, 容错，路由等，集群的地址列表可以是静态配置的，也可以是由注册中心下发。</li><li>dubbo-registry 注册中心模块：基于注册中心下发地址的集群方式，以及对各种注册中心的抽象。</li><li>dubbo-monitor 监控模块：统计服务调用次数，调用时间的，调用链跟踪的服务。</li><li>dubbo-config 配置模块：是 Dubbo 对外的 API，用户通过 Config 使用D ubbo，隐藏 Dubbo 所有细节。</li><li>dubbo-container 容器模块：是一个 Standlone 的容器，以简单的 Main 加载 Spring 启动，因为服务通常不需要 Tomcat&#x2F;JBoss 等 Web 容器的特性，没必要用 Web 容器去加载服务。</li></ul><h2 id="依赖关系"><a href="#依赖关系" class="headerlink" title="依赖关系"></a>依赖关系</h2><p>这张图是从 dubbo 的官网上下载下来的：</p><p><img data-src="/images/dubbo-architecture.png" alt="dubbo-architecture"></p><p>顺着序号我们来看看 dubbo 的各个模块是怎么工作的。</p><p>名词解释：</p><ul><li>Container 服务容器，可以类比 tomcat 或者 jetty</li><li>Provider 服务的提供方</li><li>Consumer 服务的消费方，或者称为调用方</li><li>Registry 注册中心，用于提供服务发现，注册等功能</li><li>Monitor 监控方，用于监控整个集群的工作状态</li></ul><p>所以按照序号我们看看 dubbo 各个模块都干什么了？</p><ol start="0"><li>container 是 dubbo 运行的容器，容器启动以后会初始化服务的提供方（Provider）。</li><li>Provider 在启动成功以后，会向注册中心（Registry）告知，某ip，某端口，提供某服务。</li><li>Comsumer 启动以后会向注册中心订阅自己关心的服务的状态。</li><li>服务中心会向 Comsumer 发送通知，告知它关心的服务的动向。</li><li>Comsumer 获取了服务提供方（Provider）的相关信息后，就会远程调用服务方提供的方法。完成远程调用。</li><li>Comsumer 和 Provider 会定时的上报自己运行的情况。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>以上就是对 dubbo 的代码结构和运行步骤的简单介绍。dubbo 的源码学习也就算打开了一个序幕。</p><p>下一篇文章就会从 dubbo-container 这个包开始逐步的介绍 dubbo 的源码实现。敬请期待。</p>]]>
    </content>
    <id>https://xilidou.com/2018/04/01/dubbo-start/</id>
    <link href="https://xilidou.com/2018/04/01/dubbo-start/"/>
    <published>2018-04-01T16:51:26.000Z</published>
    <summary>
      <![CDATA[<p>今天开始将开启 dubbo 的源码研究。</p>
<p>dubbo 是什么？</p>
<p>dubbo 是阿里巴巴开发的一个基于 java 的开源的 RPC 框架。所谓 RPC 指的的是 Remote Procedure Call Protocol 远程过程调用协议。</p>]]>
    </summary>
    <title>dubbo 源码学习（一）开篇</title>
    <updated>2026-08-08T02:04:49.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Redis" scheme="https://xilidou.com/categories/Redis/"/>
    <category term="redis" scheme="https://xilidou.com/tags/redis/"/>
    <content>
      <![CDATA[<p>之前写了一系列文章，已经很深入的探讨了 Redis 的数据结构，数据库的实现，key的过期策略以及 Redis 是怎么处理事件的。所以距离 Redis 的单机实现只差最后一步了，就是 Redis 是怎么处理 client 发来的命令并返回结果的，所以我们就仔细讨论一下 Redis 是怎么执行命令的。</p><p>阅读这篇文章你将会了解到：</p><ul><li>Redis 是怎么执行远程客户端发来的命令的</li></ul><span id="more"></span><h1 id="Redis-client（客户端）"><a href="#Redis-client（客户端）" class="headerlink" title="Redis client（客户端）"></a>Redis client（客户端）</h1><p>Redis 是单线程应用，它是如何与多个客户端简历网络链接并处理命令的？<br>由于 Redis 是基于 I&#x2F;O 多路复用技术，为了能够处理多个客户端的请求，Redis 在本地为每一个链接到 Redis 服务器的客户端创建了一个 redisClient 的数据结构，这个数据结构包含了每个客户端各自的状态和执行的命令。 Redis 服务器使用一个链表来维护多个 redisClient 数据结构。</p><p>在服务器端用一个链表来管理所有的 redisClient。</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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">redisServer</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">    <span class="built_in">list</span> *clients;              <span class="comment">/* List of active clients */</span></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所以我就看看 redisClient 包含的数据结构和重要参数：</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><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></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">redisClient</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 客户端状态标志</span></span><br><span class="line">    <span class="keyword">int</span> flags;              <span class="comment">/* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 套接字描述符</span></span><br><span class="line">    <span class="keyword">int</span> fd;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 当前正在使用的数据库</span></span><br><span class="line">    redisDb *db;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 当前正在使用的数据库的 id （号码）</span></span><br><span class="line">    <span class="keyword">int</span> dictid;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 客户端的名字</span></span><br><span class="line">    robj *name;             <span class="comment">/* As set by CLIENT SETNAME */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 查询缓冲区</span></span><br><span class="line">    sds querybuf;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 查询缓冲区长度峰值</span></span><br><span class="line">    <span class="keyword">size_t</span> querybuf_peak;   <span class="comment">/* Recent (100ms or more) peak of querybuf size */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 参数数量</span></span><br><span class="line">    <span class="keyword">int</span> argc;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 参数对象数组</span></span><br><span class="line">    robj **argv;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 记录被客户端执行的命令</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">redisCommand</span> *<span class="title">cmd</span>, *<span class="title">lastcmd</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 请求的类型：内联命令还是多条命令</span></span><br><span class="line">    <span class="keyword">int</span> reqtype;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 剩余未读取的命令内容数量</span></span><br><span class="line">    <span class="keyword">int</span> multibulklen;       <span class="comment">/* number of multi bulk arguments left to read */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 命令内容的长度</span></span><br><span class="line">    <span class="keyword">long</span> bulklen;           <span class="comment">/* length of bulk argument in multi bulk request */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 回复链表</span></span><br><span class="line">    <span class="built_in">list</span> *reply;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 回复链表中对象的总大小</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">long</span> reply_bytes; <span class="comment">/* Tot bytes of objects in reply list */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 已发送字节，处理 short write 用</span></span><br><span class="line">    <span class="keyword">int</span> sentlen;            <span class="comment">/* Amount of bytes already sent in the current</span></span><br><span class="line"><span class="comment">                               buffer or object being sent. */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 回复偏移量</span></span><br><span class="line">    <span class="keyword">int</span> bufpos;</span><br><span class="line">    <span class="comment">// 回复缓冲区</span></span><br><span class="line">    <span class="keyword">char</span> buf[REDIS_REPLY_CHUNK_BYTES];</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这里需要特别的注意，redisClient 并非指远程的客户端，而是一个 Redis 服务本地的数据结构，我们可以理解这个 redisClient 是远程客户端的一个映射或者代理。</p><h2 id="flags"><a href="#flags" class="headerlink" title="flags"></a>flags</h2><p>flags 表示了目前客户端的角色，以及目前所处的状态。他比较特殊可以单独表示一个状态或者多个状态。</p><h2 id="querybuf"><a href="#querybuf" class="headerlink" title="querybuf"></a>querybuf</h2><p>querybuf 是一个 sds 动态字符串类型，所谓 buf 说明是它只是一个缓冲区，用于存储没有被解析的命令。 </p><h2 id="argc-argv"><a href="#argc-argv" class="headerlink" title="argc &amp; argv"></a>argc &amp; argv</h2><p>上文的 querybuf 是一个没有处理过的命令，当 Redis 将 querybuf 命令解析以后，会将得出的参数个数和以及参数分别保存在 argc 和 argv 中。argv 是一个 redisObject 的数组。</p><h2 id="cmd"><a href="#cmd" class="headerlink" title="cmd"></a>cmd</h2><p> Redis 使用一个字典保存了所有的 redisCommand。key 是 redisCommand 的名字，值就是一个 redisCommand 结构，这个结构保存了命令的实现函数，命令的标志，命令应该给定的参数个数，命令的执行次数和总消耗时长等统计信息，cmd 是一个 redisCommand。</p><p>当 Redis 解析出 argv 和 argc 后，会根据数组 argv[0]，到字典中查询出对应的 redisCommand。上文的例子中 Redis 就会去字典去查找 <code>SET</code> 这个命令对应的 redisCommand。redis 会执行 redisCommand 中命令的实现函数。</p><h2 id="buf-bufpos-reply"><a href="#buf-bufpos-reply" class="headerlink" title="buf &amp; bufpos &amp; reply"></a>buf &amp; bufpos &amp; reply</h2><p>buf 是一个长度为 REDIS_REPLY_CHUNK_BYTES 的数组。Redis 执行相应的操作以后，就会将需要返回的返回的数据存储到 buf 中，bufpos 用于记录 buf 中已用的字节数数量，当需要恢复的数据大于 REDIS_REPLY_CHUNK_BYTES 时，redis 就会是用 reply 这个链表来保存数据。</p><h2 id="其他参数"><a href="#其他参数" class="headerlink" title="其他参数"></a>其他参数</h2><p>其他参数大家看注释就能明白，就是字面的意思。省略的参数基本上涉及 Redis 集群管理的参数，在之后的文章中会继续讲解。</p><h2 id="客户端的链接和断开"><a href="#客户端的链接和断开" class="headerlink" title="客户端的链接和断开"></a>客户端的链接和断开</h2><p>上文说过 redisServer 是用一个链表来维护所有的 redisClient 状态，每当有一个客户端发起链接以后，就会在 Redis 中生成一个对应的 redisClient 数据结构，增加到<code>clients</code>这个链表之后。</p><p>一个客户端很可能被多种原因断开。</p><p>总体分为几种类型：</p><ul><li>客户端主动退出或者被 kill。</li><li>timeout 超时。</li><li>Redis 为了自我保护，会断开发的数据超过限制大小的客户端。</li><li>Redis 为了自我保护，会断需要返回的数据超过限制大小的客户端。</li></ul><h2 id="调用总结"><a href="#调用总结" class="headerlink" title="调用总结"></a>调用总结</h2><p>当客户端和服务器端的嵌套字变得可读的时候，服务器将会调用命令请求处理器来执行以下操作：</p><ol><li>读取嵌套字中的数据，写入 querybuf。</li><li>解析 querybuf 中的命令，记录到 argc 和 argv 中。</li><li>根据 argv[0] 查找对应的 recommand。</li><li>执行 recommand 对应的实现函数。</li><li>执行以后将结果存入 buf &amp; bufpos &amp; reply 中，返回给调用方。</li></ol><h1 id="Redis-Server-服务端"><a href="#Redis-Server-服务端" class="headerlink" title="Redis Server (服务端)"></a>Redis Server (服务端)</h1><p>上文是从 redisClient 的角度来观察命令的执行，文章接下来的部分将会从 Redis 的代码层面，微观的观察 Redis 是怎么实现命令的执行的。</p><h2 id="redisServer-的启动"><a href="#redisServer-的启动" class="headerlink" title="redisServer 的启动"></a>redisServer 的启动</h2><p>在了解redisServer 的工作机制的工作机制之前，需要了解 redisServer 的启动做了什么：</p><p>可以继续观察 Redis 的 main() 函数。</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="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span> **argv)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 创建并初始化服务器数据结构</span></span><br><span class="line">    initServer();</span><br><span class="line"></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们只关注 <code>initServer()</code> 这个函数，他负责初始化服务器的数据结构。继续跟踪代码:</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><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><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">initServer</span><span class="params">()</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">//创建eventLoop</span></span><br><span class="line">    server.el = aeCreateEventLoop(server.maxclients+REDIS_EVENTLOOP_FDSET_INCR);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/* Create an event handler for accepting new connections in TCP and Unix</span></span><br><span class="line"><span class="comment">     * domain sockets. */</span></span><br><span class="line">    <span class="comment">// 为 TCP 连接关联连接应答（accept）处理器</span></span><br><span class="line">    <span class="comment">// 用于接受并应答客户端的 connect() 调用</span></span><br><span class="line">    <span class="keyword">for</span> (j = <span class="number">0</span>; j &lt; server.ipfd_count; j++) &#123;</span><br><span class="line">        <span class="keyword">if</span> (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,</span><br><span class="line">            acceptTcpHandler,<span class="literal">NULL</span>) == AE_ERR)</span><br><span class="line">            &#123;</span><br><span class="line">                redisPanic(</span><br><span class="line">                    <span class="string">&quot;Unrecoverable error creating server.ipfd file event.&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="keyword">if</span> (server.sofd &gt; <span class="number">0</span> &amp;&amp; aeCreateFileEvent(server.el,server.sofd,AE_READABLE,</span><br><span class="line">        acceptUnixHandler,<span class="literal">NULL</span>) == AE_ERR) redisPanic(<span class="string">&quot;Unrecoverable error creating server.sofd file event.&quot;</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">//...</span></span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>篇幅限制，我们省略了很多与本编文章无关的代码，保留了核心逻辑代码。</p><p>在上一篇文章中 <a href="https://www.xilidou.com/2018/03/22/redis-event/">《Redis 中的事件驱动模型》</a> 我们讲解过，redis 使用不同的事件处理器，处理不同的事件。</p><p>在这段代码里面：</p><ul><li>初始化了事件处理器的 eventLoop</li><li>向 eventLoop 中注册了两个事件处理器 <code>acceptTcpHandler</code> 和 <code>acceptUnixHandler</code>，分别处理远程的链接和本地链接。</li></ul><h2 id="redisClient-的创建"><a href="#redisClient-的创建" class="headerlink" title="redisClient 的创建"></a>redisClient 的创建</h2><p>当有一个远程客户端连接到 Redis 的服务器，会触发 <code>acceptTcpHandler</code> 事件处理器.</p><p><code>acceptTcpHandler</code> 事件处理器，会创建一个链接。然后继续调用 <code>acceptCommonHandler</code>。</p><p><code>acceptCommonHandler</code> 事件处理器的作用是：</p><ul><li>调用 <code>createClient()</code> 方法创建 redisClient</li><li>检查已经创建的 redisClient 是否超过 server 允许的数量的上限</li><li>如果超过上限就拒绝远程连接</li><li>否则创建 redisClient 创建成功</li><li>并更新连接的统计次数，更新 redisClinet 的 flags 字段</li></ul><p>这个时候 Redis 在服务端创建了 redisClient 数据结构，这个时候远程的客户端就在 redisServer 中创建了一个代理。远程的客户端就与 Redis 服务器建立了联系，就可以向服务器发送命令了。</p><h2 id="处理命令"><a href="#处理命令" class="headerlink" title="处理命令"></a>处理命令</h2><p>在 <code>createClient()</code> 行数中：</p><figure class="highlight c"><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">// 绑定读事件到事件 loop （开始接收命令请求）</span></span><br><span class="line"><span class="keyword">if</span> (aeCreateFileEvent(server.el,fd,AE_READABLE,readQueryFromClient, c) == AE_ERR)</span><br></pre></td></tr></table></figure><p>向 eventLoop 中注册了 <code>readQueryFromClient</code>。  <code>readQueryFromClient</code> 的作用就是从client中读取客户端的查询缓冲区内容。</p><p>然后调用函数 <code>processInputBuffer</code> 来处理客户端的请求。在 <code>processInputBuffer</code> 中有几个核心函数：</p><ul><li><code>processInlineBuffer</code> 和 <code>processMultibulkBuffer</code> 解析 querybuf 中的命令，记录到 argc 和 argv 中。</li><li><code>processCommand</code> 根据 argv[0] 查找对应的 recommen,执行 recommend 对应的执行函数。在执行之前还会验证命令的正确性。将结果存入 buf &amp; bufpos &amp; reply 中</li></ul><h2 id="返回数据"><a href="#返回数据" class="headerlink" title="返回数据"></a>返回数据</h2><p>万事具备了，执行完了命令就需要把数据返回给远程的调用方。调用链如下</p><p>processCommand -&gt; addReply -&gt; prepareClientToWrite</p><p>在 <code>prepareClientToWrite</code> 中我们有见到了熟悉的代码：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">aeCreateFileEvent(server.el, c-&gt;fd, AE_WRITABLE,sendReplyToClient, c) == AE_ERR) <span class="keyword">return</span> REDIS_ERR;</span><br></pre></td></tr></table></figure><p>向 eventloop 绑定了 <code>sendReplyToClient</code> 事件处理器。</p><p>在 <code>sendReplyToClient</code> 中观察代码发现，如果 bufpos 大于 0，将会把 buf 发送给远程的客户端，如果链表 reply 的长度大于0，就会将遍历链表 reply，发送给远程的客户端，这里需要注意的是，为了避免 reply 数据量过大，就会过度的占用资源引起 Redis 相应慢。为了解决这个问题，当写入的总数量大于 REDIS_MAX_WRITE_PER_EVENT 时，Redis 将会临时中断写入，记录操作的进度，将处理时间让给其他操作，剩余的内容等下次继续。这样的套路我们一路走来看过太多了。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><ol><li>远程客户端连接到 redis 后，redis服务端会为远程客户端创建一个 redisClient 作为代理。</li><li>redis 会读取嵌套字中的数据，写入 querybuf 中。</li><li>解析 querybuf 中的命令，记录到 argc 和 argv 中。</li><li>根据 argv[0] 查找对应的 recommand。</li><li>执行 recommend 对应的执行函数。</li><li>执行以后将结果存入 buf &amp; bufpos &amp; reply 中。</li><li>返回给调用方。返回数据的时候，会控制写入数据量的大小，如果过大会分成若干次。保证 redis 的相应时间。</li></ol><p>Redis 作为单线程应用，一直贯彻的思想就是，每个步骤的执行都有一个上限（包括执行时间的上限或者文件尺寸的上限）一旦达到上限，就会记录下当前的执行进度，下次再执行。保证了 Redis 能够及时响应不发生阻塞。</p><p>大家还可以阅读我的 Redis 相关的文章：</p><p><a href="https://www.xilidou.com/2018/03/12/redis-data/?fromblog=redis-server">Redis 的基础数据结构（一） 可变字符串、链表、字典</a></p><p><a href="https://www.xilidou.com/2018/03/13/redis-data2/?blogfrom=redis-server">Redis 的基础数据结构（二） 整数集合、跳跃表、压缩列表</a></p><p><a href="https://www.xilidou.com/2018/03/15/redis-object/">Redis 的基础数据结构（三）对象 </a></p><p><a href="https://www.xilidou.com/2018/03/20/redis-server/">Redis 数据库、键过期的实现</a></p><p><a href="https://www.xilidou.com/2018/03/22/redis-event/">Redis 中的事件驱动模型</a></p><p>欢迎关注我的微信公众号：<br><img data-src="/images/2019-04-25-22205.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2018/03/30/redis-recommend/</id>
    <link href="https://xilidou.com/2018/03/30/redis-recommend/"/>
    <published>2018-03-30T12:45:46.000Z</published>
    <summary>
      <![CDATA[<p>之前写了一系列文章，已经很深入的探讨了 Redis 的数据结构，数据库的实现，key的过期策略以及 Redis 是怎么处理事件的。所以距离 Redis 的单机实现只差最后一步了，就是 Redis 是怎么处理 client 发来的命令并返回结果的，所以我们就仔细讨论一下 Redis 是怎么执行命令的。</p>
<p>阅读这篇文章你将会了解到：</p>
<ul>
<li>Redis 是怎么执行远程客户端发来的命令的</li>
</ul>]]>
    </summary>
    <title>Redis 命令的执行过程</title>
    <updated>2026-08-08T02:04:49.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Redis" scheme="https://xilidou.com/categories/Redis/"/>
    <category term="redis" scheme="https://xilidou.com/tags/redis/"/>
    <category term="event" scheme="https://xilidou.com/tags/event/"/>
    <content>
      <![CDATA[<p>Redis 是一个事件驱动的内存数据库，服务器需要处理两种类型的事件。</p><ul><li>文件事件</li><li>时间事件</li></ul><p>下面就会介绍这两种事件的实现原理。</p><span id="more"></span><h1 id="文件事件"><a href="#文件事件" class="headerlink" title="文件事件"></a>文件事件</h1><p>Redis 服务器通过 socket 实现与客户端（或其他redis服务器）的交互,文件事件就是服务器对 socket 操作的抽象。 Redis 服务器，通过监听这些 socket 产生的文件事件并处理这些事件，实现对客户端调用的响应。</p><h2 id="Reactor"><a href="#Reactor" class="headerlink" title="Reactor"></a>Reactor</h2><p>Redis 基于 Reactor 模式开发了自己的事件处理器。</p><p>这里就先展开讲一讲 Reactor 模式。看下图：</p><p><img data-src="/images/Reactor.jpg" alt="reactor"></p><p>“I&#x2F;O 多路复用模块”会监听多个 FD ，当这些FD产生，accept，read，write 或 close 的文件事件。会向“文件事件分发器（dispatcher）”传送事件。</p><p>文件事件分发器（dispatcher）在收到事件之后，会根据事件的类型将事件分发给对应的 handler。</p><p>我们顺着图，从上到下的逐一讲解 Redis 是怎么实现这个 Reactor 模型的。</p><h2 id="I-O-多路复用模块"><a href="#I-O-多路复用模块" class="headerlink" title="I&#x2F;O 多路复用模块"></a>I&#x2F;O 多路复用模块</h2><p>Redis 的 I&#x2F;O 多路复用模块，其实是封装了操作系统提供的 select，epoll，avport 和 kqueue 这些基础函数。向上层提供了一个统一的接口，屏蔽了底层实现的细节。</p><p>一般而言 Redis 都是部署到 Linux 系统上，所以我们就看看使用 Redis 是怎么利用 linux 提供的 epoll 实现I&#x2F;O 多路复用。</p><p>首先看看 epoll 提供的三个方法：</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><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"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * 创建一个epoll的句柄，size用来告诉内核这个监听的数目一共有多大</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">epoll_create</span><span class="params">(<span class="keyword">int</span> size)</span>；</span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function"><span class="comment">/*</span></span></span><br><span class="line"><span class="comment"><span class="function"> * 可以理解为，增删改 fd 需要监听的事件</span></span></span><br><span class="line"><span class="comment"><span class="function"> * epfd 是 epoll_create() 创建的句柄。</span></span></span><br><span class="line"><span class="comment"><span class="function"> * op 表示 增删改</span></span></span><br><span class="line"><span class="comment"><span class="function"> * epoll_event 表示需要监听的事件，Redis 只用到了可读，可写，错误，挂断 四个状态</span></span></span><br><span class="line"><span class="comment"><span class="function"> */</span></span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">epoll_ctl</span><span class="params">(<span class="keyword">int</span> epfd, <span class="keyword">int</span> op, <span class="keyword">int</span> fd, struct epoll_event *event)</span>；</span></span><br><span class="line"><span class="function"></span></span><br><span class="line"><span class="function"><span class="comment">/*</span></span></span><br><span class="line"><span class="comment"><span class="function"> * 可以理解为查询符合条件的事件</span></span></span><br><span class="line"><span class="comment"><span class="function"> * epfd 是 epoll_create() 创建的句柄。</span></span></span><br><span class="line"><span class="comment"><span class="function"> * epoll_event 用来存放从内核得到事件的集合</span></span></span><br><span class="line"><span class="comment"><span class="function"> * maxevents 获取的最大事件数</span></span></span><br><span class="line"><span class="comment"><span class="function"> * timeout 等待超时时间</span></span></span><br><span class="line"><span class="comment"><span class="function"> */</span></span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">epoll_wait</span><span class="params">(<span class="keyword">int</span> epfd, struct epoll_event * events, <span class="keyword">int</span> maxevents, <span class="keyword">int</span> timeout)</span></span>;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>再看 Redis 对文件事件，封装epoll向上提供的接口：</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><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></pre></td><td class="code"><pre><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="comment"> */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">aeApiState</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// epoll_event 实例描述符</span></span><br><span class="line">    <span class="keyword">int</span> epfd;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 事件槽</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">epoll_event</span> *<span class="title">events</span>;</span></span><br><span class="line"></span><br><span class="line">&#125; aeApiState;</span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * 创建一个新的 epoll </span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">int</span>  <span class="title">aeApiCreate</span><span class="params">(aeEventLoop *eventLoop)</span></span></span><br><span class="line"><span class="function"><span class="comment">/*</span></span></span><br><span class="line"><span class="comment"><span class="function"> * 调整事件槽的大小</span></span></span><br><span class="line"><span class="comment"><span class="function"> */</span></span></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">int</span>  <span class="title">aeApiResize</span><span class="params">(aeEventLoop *eventLoop, <span class="keyword">int</span> setsize)</span></span></span><br><span class="line"><span class="function"><span class="comment">/*</span></span></span><br><span class="line"><span class="comment"><span class="function"> * 释放 epoll 实例和事件槽</span></span></span><br><span class="line"><span class="comment"><span class="function"> */</span></span></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">aeApiFree</span><span class="params">(aeEventLoop *eventLoop)</span></span></span><br><span class="line"><span class="function"><span class="comment">/*</span></span></span><br><span class="line"><span class="comment"><span class="function"> * 关联给定事件到 fd</span></span></span><br><span class="line"><span class="comment"><span class="function"> */</span></span></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">int</span>  <span class="title">aeApiAddEvent</span><span class="params">(aeEventLoop *eventLoop, <span class="keyword">int</span> fd, <span class="keyword">int</span> mask)</span></span></span><br><span class="line"><span class="function"><span class="comment">/*</span></span></span><br><span class="line"><span class="comment"><span class="function"> * 从 fd 中删除给定事件</span></span></span><br><span class="line"><span class="comment"><span class="function"> */</span></span></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">aeApiDelEvent</span><span class="params">(aeEventLoop *eventLoop, <span class="keyword">int</span> fd, <span class="keyword">int</span> mask)</span></span></span><br><span class="line"><span class="function"><span class="comment">/*</span></span></span><br><span class="line"><span class="comment"><span class="function"> * 获取可执行事件</span></span></span><br><span class="line"><span class="comment"><span class="function"> */</span></span></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">int</span>  <span class="title">aeApiPoll</span><span class="params">(aeEventLoop *eventLoop, struct timeval *tvp)</span></span></span><br><span class="line"><span class="function"></span></span><br></pre></td></tr></table></figure><p>所以看看这个ae_peoll.c 如何对 epoll 进行封装的： </p><ul><li><code>aeApiCreate()</code> 是对 <code>epoll.epoll_create()</code> 的封装。</li><li><code>aeApiAddEvent()</code>和<code>aeApiDelEvent()</code> 是对 <code>epoll.epoll_ctl()</code>的封装。</li><li><code>aeApiPoll()</code> 是对 <code>epoll_wait()</code>的封装。</li></ul><p>这样 Redis 的利用 epoll 实现的 I&#x2F;O 复用器就比较清晰了。</p><p>再往上一层次我们需要看看 ea.c 是怎么封装的？</p><p>首先需要关注的是事件处理器的数据结构：</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><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><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">aeFileEvent</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 监听事件类型掩码，</span></span><br><span class="line">    <span class="comment">// 值可以是 AE_READABLE 或 AE_WRITABLE ，</span></span><br><span class="line">    <span class="comment">// 或者 AE_READABLE | AE_WRITABLE</span></span><br><span class="line">    <span class="keyword">int</span> mask; <span class="comment">/* one of AE_(READABLE|WRITABLE) */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 读事件处理器</span></span><br><span class="line">    aeFileProc *rfileProc;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 写事件处理器</span></span><br><span class="line">    aeFileProc *wfileProc;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 多路复用库的私有数据</span></span><br><span class="line">    <span class="keyword">void</span> *clientData;</span><br><span class="line"></span><br><span class="line">&#125; aeFileEvent;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><code>mask</code> 就是可以理解为事件的类型。</p><p>除了使用 ae_peoll.c 提供的方法外,ae.c 还增加 “增删查” 的几个 API。</p><ul><li>增:<code>aeCreateFileEvent</code></li><li>删:<code>aeDeleteFileEvent</code></li><li>查: 查包括两个维度 <code>aeGetFileEvents</code> 获取某个 fd 的监听类型和<code>aeWait</code>等待某个fd 直到超时或者达到某个状态。</li></ul><h2 id="事件分发器（dispatcher）"><a href="#事件分发器（dispatcher）" class="headerlink" title="事件分发器（dispatcher）"></a>事件分发器（dispatcher）</h2><p>Redis 的事件分发器 <code>ae.c/aeProcessEvents</code> 不但处理文件事件还处理时间事件，所以这里只贴与文件分发相关的出部分代码，dispather 根据 mask 调用不同的事件处理器。</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><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="comment">//从 epoll 中获关注的事件</span></span><br><span class="line">numevents = aeApiPoll(eventLoop, tvp);</span><br><span class="line"><span class="keyword">for</span> (j = <span class="number">0</span>; j &lt; numevents; j++) &#123;</span><br><span class="line">    <span class="comment">// 从已就绪数组中获取事件</span></span><br><span class="line">    aeFileEvent *fe = &amp;eventLoop-&gt;events[eventLoop-&gt;fired[j].fd];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">int</span> mask = eventLoop-&gt;fired[j].mask;</span><br><span class="line">    <span class="keyword">int</span> fd = eventLoop-&gt;fired[j].fd;</span><br><span class="line">    <span class="keyword">int</span> rfired = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 读事件</span></span><br><span class="line">    <span class="keyword">if</span> (fe-&gt;mask &amp; mask &amp; AE_READABLE) &#123;</span><br><span class="line">        <span class="comment">// rfired 确保读/写事件只能执行其中一个</span></span><br><span class="line">        rfired = <span class="number">1</span>;</span><br><span class="line">        fe-&gt;rfileProc(eventLoop,fd,fe-&gt;clientData,mask);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 写事件</span></span><br><span class="line">    <span class="keyword">if</span> (fe-&gt;mask &amp; mask &amp; AE_WRITABLE) &#123;</span><br><span class="line">        <span class="keyword">if</span> (!rfired || fe-&gt;wfileProc != fe-&gt;rfileProc)</span><br><span class="line">            fe-&gt;wfileProc(eventLoop,fd,fe-&gt;clientData,mask);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    processed++;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到这个分发器，根据 mask 的不同将事件分别分发给了读事件和写事件。</p><h2 id="文件事件处理器的类型"><a href="#文件事件处理器的类型" class="headerlink" title="文件事件处理器的类型"></a>文件事件处理器的类型</h2><p>Redis 有大量的事件处理器类型，我们就讲解处理一个简单命令涉及到的三个处理器：</p><ul><li>acceptTcpHandler 连接应答处理器，负责处理连接相关的事件，当有client 连接到Redis的时候们就会产生 AE_READABLE 事件。引发它执行。</li><li>readQueryFromClinet 命令请求处理器，负责读取通过 sokect 发送来的命令。</li><li>sendReplyToClient 命令回复处理器，当Redis处理完命令，就会产生 AE_WRITEABLE 事件，将数据回复给 client。</li></ul><h2 id="文件事件实现总结"><a href="#文件事件实现总结" class="headerlink" title="文件事件实现总结"></a>文件事件实现总结</h2><p>我们按照开始给出的 Reactor 模型，从上到下讲解了文件事件处理器的实现，下面将会介绍时间时间的实现。</p><h1 id="时间事件"><a href="#时间事件" class="headerlink" title="时间事件"></a>时间事件</h1><p>Reids 有很多操作需要在给定的时间点进行处理，时间事件就是对这类定时任务的抽象。</p><p>先看时间事件的数据结构：</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><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></pre></td><td class="code"><pre><span class="line"><span class="comment">/* Time event structure</span></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="comment"> */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">aeTimeEvent</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 时间事件的唯一标识符</span></span><br><span class="line">    <span class="keyword">long</span> <span class="keyword">long</span> id; <span class="comment">/* time event identifier. */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 事件的到达时间</span></span><br><span class="line">    <span class="keyword">long</span> when_sec; <span class="comment">/* seconds */</span></span><br><span class="line">    <span class="keyword">long</span> when_ms; <span class="comment">/* milliseconds */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 事件处理函数</span></span><br><span class="line">    aeTimeProc *timeProc;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 事件释放函数</span></span><br><span class="line">    aeEventFinalizerProc *finalizerProc;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 多路复用库的私有数据</span></span><br><span class="line">    <span class="keyword">void</span> *clientData;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 指向下个时间事件结构，形成链表</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">aeTimeEvent</span> *<span class="title">next</span>;</span></span><br><span class="line"></span><br><span class="line">&#125; aeTimeEvent;</span><br></pre></td></tr></table></figure><p>看见 <code>next</code> 我们就知道这个 aeTimeEvent 是一个链表结构。看图：</p><p><img data-src="/images/timeEvent.jpg" alt="timeEvent"></p><p>注意这是一个按照id倒序排列的链表，并没有按照事件顺序排序。</p><h2 id="processTimeEvent"><a href="#processTimeEvent" class="headerlink" title="processTimeEvent"></a>processTimeEvent</h2><p>Redis 使用这个函数处理所有的时间事件，我们整理一下执行思路：</p><ol><li>记录最新一次执行这个函数的时间，用于处理系统时间被修改产生的问题。</li><li>遍历链表找出所有 when_sec 和 when_ms 小于现在时间的事件。</li><li>执行事件对应的处理函数。</li><li>检查事件类型，如果是周期事件则刷新该事件下一次的执行事件。</li><li>否则从列表中删除事件。</li></ol><h1 id="综合调度器（aeProcessEvents）"><a href="#综合调度器（aeProcessEvents）" class="headerlink" title="综合调度器（aeProcessEvents）"></a>综合调度器（aeProcessEvents）</h1><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><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="comment">// 1. 获取离当前时间最近的时间事件</span></span><br><span class="line">shortest = aeSearchNearestTimer(eventLoop);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 获取间隔时间</span></span><br><span class="line">timeval = shortest - nowTime;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 如果timeval 小于 0，说明已经有需要执行的时间事件了。</span></span><br><span class="line"><span class="keyword">if</span>(timeval &lt; <span class="number">0</span>)&#123;</span><br><span class="line">    timeval = <span class="number">0</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 3. 在 timeval 时间内，取出文件事件。</span></span><br><span class="line">numevents = aeApiPoll(eventLoop, timeval);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 4.根据文件事件的类型指定不同的文件处理器</span></span><br><span class="line"><span class="keyword">if</span> (AE_READABLE) &#123;</span><br><span class="line">    <span class="comment">// 读事件</span></span><br><span class="line">    rfileProc(eventLoop,fd,fe-&gt;clientData,mask);</span><br><span class="line">&#125;</span><br><span class="line">    <span class="comment">// 写事件</span></span><br><span class="line"><span class="keyword">if</span> (AE_WRITABLE) &#123;</span><br><span class="line">    wfileProc(eventLoop,fd,fe-&gt;clientData,mask);</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>以上的伪代码就是整个 Redis 事件处理器的逻辑。</p><p>我们可以再看看谁执行了这个 <code>aeProcessEvents</code>:</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><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><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">aeMain</span><span class="params">(aeEventLoop *eventLoop)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    eventLoop-&gt;stop = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">while</span> (!eventLoop-&gt;stop) &#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 如果有需要在事件处理前执行的函数，那么运行它</span></span><br><span class="line">        <span class="keyword">if</span> (eventLoop-&gt;beforesleep != <span class="literal">NULL</span>)</span><br><span class="line">            eventLoop-&gt;beforesleep(eventLoop);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 开始处理事件</span></span><br><span class="line">        aeProcessEvents(eventLoop, AE_ALL_EVENTS);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>然后我们再看看是谁调用了 <code>eaMain</code>:</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span> **argv)</span> </span>&#123;</span><br><span class="line">    <span class="comment">//一些配置和准备</span></span><br><span class="line">    ...</span><br><span class="line">    aeMain(server.el);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//结束后的回收工作</span></span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们在 Redis 的 main 方法中找个了它。</p><p>这个时候我们整理出的思路就是:</p><ul><li><p>Redis 的 main() 方法执行了一些配置和准备以后就调用 <code>eaMain()</code> 方法。</p></li><li><p><code>eaMain()</code> while(true) 的调用 <code>aeProcessEvents()</code>。</p></li></ul><p>所以我们说 Redis 是一个事件驱动的程序，期间我们发现，Redis 没有 fork 过任何线程。所以也可以说 Redis 是一个基于事件驱动的单线程应用。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>在后端的面试中 Redis 总是一个或多或少会问到的问题。</p><p>读完这篇文章你也许就能回答这几个问题：</p><ul><li>为什么 Redis 是一个单线程应用？</li><li>为什么 Redis 是一个单线程应用，却有如此高的性能？</li></ul><p>如果你用本文提供的知识点回答这两个问题，一定会在面试官心中留下一个高大的形象。</p><p>大家还可以阅读我的 Redis 相关的文章：</p><p><a href="https://www.xilidou.com/2018/03/12/redis-data/?fromblog=redis-server">Redis 的基础数据结构（一） 可变字符串、链表、字典</a></p><p><a href="https://www.xilidou.com/2018/03/13/redis-data2/?blogfrom=redis-server">Redis 的基础数据结构（二） 整数集合、跳跃表、压缩列表</a></p><p><a href="https://www.xilidou.com/2018/03/15/redis-object/">Redis 的基础数据结构（三）对象 </a></p><p><a href="https://www.xilidou.com/2018/03/20/redis-server/">Redis 数据库、键过期的实现</a></p><p>欢迎关注我的微信公众号：<br><img data-src="/images/2019-04-25-022223.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2018/03/22/redis-event/</id>
    <link href="https://xilidou.com/2018/03/22/redis-event/"/>
    <published>2018-03-22T22:48:03.000Z</published>
    <summary>
      <![CDATA[<p>Redis 是一个事件驱动的内存数据库，服务器需要处理两种类型的事件。</p>
<ul>
<li>文件事件</li>
<li>时间事件</li>
</ul>
<p>下面就会介绍这两种事件的实现原理。</p>]]>
    </summary>
    <title>Redis 中的事件驱动模型</title>
    <updated>2026-08-08T02:04:49.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Redis" scheme="https://xilidou.com/categories/Redis/"/>
    <category term="redis" scheme="https://xilidou.com/tags/redis/"/>
    <category term="db" scheme="https://xilidou.com/tags/db/"/>
    <category term="redisdb" scheme="https://xilidou.com/tags/redisdb/"/>
    <category term="redis key" scheme="https://xilidou.com/tags/redis-key/"/>
    <category term="redis expire" scheme="https://xilidou.com/tags/redis-expire/"/>
    <content>
      <![CDATA[<p>之前的文章讲解了 Redis 的数据结构，这回就可以看看作为内存数据库，Redis 是怎么存储数据的。以及键是怎么过期的。</p><p>阅读这篇文章你将会了解到：</p><ul><li>Redis 的数据库实现</li><li>Redis 键过期的策略</li></ul><span id="more"></span><h1 id="数据库的实现"><a href="#数据库的实现" class="headerlink" title="数据库的实现"></a>数据库的实现</h1><p>我们先看代码 <code>server.h/redisServer</code> </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><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="class"><span class="keyword">struct</span> <span class="title">redisServer</span>&#123;</span></span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="comment">//保存 db 的数组</span></span><br><span class="line">    redisDb *db;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//db 的数量</span></span><br><span class="line">    <span class="keyword">int</span> dbnum;</span><br><span class="line"></span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>再看redisDb的代码：</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><span class="line">10</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">redisDb</span> &#123;</span></span><br><span class="line">    dict *dict;                 <span class="comment">/* The keyspace for this DB */</span></span><br><span class="line">    dict *expires;              <span class="comment">/* Timeout of keys with a timeout set */</span></span><br><span class="line">    dict *blocking_keys;        <span class="comment">/* Keys with clients waiting for data (BLPOP)*/</span></span><br><span class="line">    dict *ready_keys;           <span class="comment">/* Blocked keys that received a PUSH */</span></span><br><span class="line">    dict *watched_keys;         <span class="comment">/* WATCHED keys for MULTI/EXEC CAS */</span></span><br><span class="line">    <span class="keyword">int</span> id;                     <span class="comment">/* Database ID */</span></span><br><span class="line">    <span class="keyword">long</span> <span class="keyword">long</span> avg_ttl;          <span class="comment">/* Average TTL, just for stats */</span></span><br><span class="line">&#125; redisDb;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>总体来说redis的 server 包含若干个（默认16个） redisDb 数据库。</p><p><img data-src="/images/redisServer.jpg" alt="db"></p><p>Redis 是一个 k-v 存储的键值对数据库。其中字典 dict 保存了数据库中的所有键值对，这个地方叫做 <code>keyspace</code> 直译过来就是“键空间”。</p><p>所以我们就可以这么认为，在 redisDb 中我们使用 dict（字典）来维护键空间。</p><ul><li><p>keyspace 的 kay 是数据库的 key，每一个key 是一个字符串对象。注意不是字符串，而是字符串对象。</p></li><li><p>keyspace 的 value 是数据库的 value，这个 value 可以是 redis 的，字符串对象，列表对象，哈希表对象，集合对象或者有序对象中的一种。</p></li></ul><h2 id="数据库读写操作"><a href="#数据库读写操作" class="headerlink" title="数据库读写操作"></a>数据库读写操作</h2><p>所以对于数据的增删改查，就是对 keyspace 这个大 map 的增删改查。</p><p>当我们执行：</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">&gt;redis SET mobile <span class="string">&quot;13800000000&quot;</span></span><br></pre></td></tr></table></figure><p>实际上就是为 keyspace 增加了一个 key 是包含字符串“mobile”的字符串对象，value 为包含字符“13800000000”的字符串对象。</p><p>看图：</p><p><img data-src="/images/dbNoExpire.jpg" alt="db"></p><p>对于删改查，没啥好说的。类似java 的 map 操作，大多数程序员应该都能理解。</p><p>需要特别注意的是，再执行对键的读写操作的时候，Redis 还要做一些额外的维护动作：</p><ul><li>维护 hit 和 miss 两个计数器。用于统计 Redis 的缓存命中率。</li><li>更新键的 LRU 时间，记录键的最后活跃时间。</li><li>如果在读取的时候发现键已经过期，Redis 先删除这个过期的键然后再执行余下操作。</li><li>如果有客户对这个键执行了 WATCH 操作，会把这个键标记为 dirty，让事务注意到这个键已经被改过。</li><li>没修改一次 dirty 会增加1。</li><li>如果服务器开启了数据库通知功能，键被修改之后，会按照配置发送通知。</li></ul><h2 id="键的过期实现"><a href="#键的过期实现" class="headerlink" title="键的过期实现"></a>键的过期实现</h2><p>Redis 作为缓存使用最主要的一个特性就是可以为键值对设置过期时间。就看看 Redis 是如果实现这一个最重要的特性的？</p><p>在 Redis 中与过期时间有关的命令 </p><ul><li>EXPIRE 设置 key 的存活时间单位秒</li><li>EXPIREAT 设置 key 的过期时间点单位秒</li><li>PEXPIRE 设置 key 的存活时间单位毫秒</li><li>PEXPIREAT 设置 key 的过期时间点单位毫秒</li></ul><p>其实这些命令，底层的命令都是由 REXPIREAT 实现的。</p><p>在 redisDb 中使用了 dict *expires，来存储过期时间的。其中 key 指向了 keyspace 中的 key（c 语言中的指针）， value 是一个 long long 类型的时间戳，标定这个 key 过期的时间点，单位是毫秒。</p><p>如果我们为上文的 mobile 增加一个过期时间。</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">&gt;redis PEXPIREAT mobile 1521469812000</span><br></pre></td></tr></table></figure><p>这个时候就会在过期的 字典中增加一个键值对。如下图：</p><p><img data-src="/images/db.jpg" alt="db"></p><p>对于过期的判断逻辑就很简单：</p><ol><li>在 字典 expires 中 key 是否存在。</li><li>如果 key 存在，value 的时间戳是否小于当前系统时间戳。</li></ol><p>接下来就需要讨论一下过期的键的删除策略。</p><p>key的删除有三种策略：</p><ol><li>定时删除，Redis定时的删除内存里面所有过期的键值对，这样能够保证内存友好，过期的key都会被删除，但是如果key的数量很多，一次删除需要CPU运算，CPU不友好。</li><li>惰性删除，只有 key 在被调用的时候才去检查键值对是否过期，但是会造成内存中存储大量的过期键值对，内存不友好，但是极大的减轻CPU 的负担。</li><li>定时部分删除，Redis定时扫描过期键，但是只删除部分，至于删除多少键，根据当前 Redis 的状态决定。</li></ol><p>这三种策略就是对时间和空间有不同的倾向。Redis为了平衡时间和空间，采用了后两种策略 惰性删除和定时部分删除。</p><p>惰性删除比较简单，不做过多介绍。主要讨论一下定时部分删除。</p><p>过期键的定时删除的策略由 expire.c&#x2F;activeExpireCycle() 函数实现，server.c&#x2F;serverCron() 定时的调用 <code>activieExpireCycle()</code> 。</p><p>activeExpireCycle 的大的操作原则是，如果过期的key比较少，则删除key的数量也比较保守，如果，过期的键多，删除key的策略就会很激进。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">unsigned</span> <span class="keyword">int</span> current_db = <span class="number">0</span>; <span class="comment">/* Last DB tested. */</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">int</span> timelimit_exit = <span class="number">0</span>;      <span class="comment">/* Time limit hit in previous call? */</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">long</span> <span class="keyword">long</span> last_fast_cycle = <span class="number">0</span>; <span class="comment">/* When last fast cycle ran. */</span></span><br></pre></td></tr></table></figure><ul><li><p>首先三个 <code>static</code> 全局参数分别记录目前遍历的 db下标，上一次删除是否是超时退出的，上一次快速操作是什么时候进行的。</p></li><li><p>计算 <code>timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;</code> 可以理解为 25% 的 cpu 时间。 </p></li><li><p>如果 db 中 expire 的大小为0 不操作</p></li><li><p>expire 占总 key 小于 1% 不操作</p></li><li><p>num &#x3D; dictSize(db-&gt;expires)；num 是 expire 使用的key的数量。</p></li><li><p>slots &#x3D; dictSlots(db-&gt;expires); slots 是 expire 字典的尺寸大小。</p></li><li><p>已使用的key（num） 大于 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 则设置为 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP。也就是说每次只检查 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个键。</p></li><li><p>随机获取带过期的 key。计算是否过期，如果过期就删除。</p></li><li><p>然后各种统计，包括删除键的次数，平均过期时间。</p></li><li><p>每遍历十六次，计算操作时间，如果超过 timelimit 结束返回。</p></li><li><p>如果删除的过期键大于 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 的 1\4 就跳出循环，结束。</p></li></ul><p>步骤比较复杂，总结一下：（这里都是以默认配置描述）</p><ol><li>redis 会用最多 25% 的 cpu 时间处理键的过期。</li><li>遍历所有的 redisDb</li><li>在每个 redisDb 中如果数据中没有过期键或者过期键比例过低就直接进入下一个 redisDb。</li><li>否则，遍历 redisDb 中的过期键，如果删除的键达到有过期时间的的key 的25% ，或者操作时间大于 cpu 时间的 25% 就结束当前循环，进入下一个redisDb。</li></ol><h1 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h1><p>这篇文章主要解释了 Redis 的数据库是怎么实现的，同时介绍了 Redis 处理过期键的逻辑。看 Redis 的代码越多越发现，实际上 Redis 一直在做的一件事情就是平衡，一直在平衡程序的空间和时间。其实平时的业务设计，就是在宏观上平衡，平衡宏观系统的时间和空间。所以，看源码是让我们从微观学习系统架构的良好途径，是架构师的成长的必经之路。</p><p>我之前的三篇关于 Redis 的基础数据结构链接地址，欢迎大家阅读。</p><p><a href="https://www.xilidou.com/2018/03/12/redis-data/?fromblog=redis-server">Redis 的基础数据结构（一） 可变字符串、链表、字典</a></p><p><a href="https://www.xilidou.com/2018/03/13/redis-data2/?blogfrom=redis-server">Redis 的基础数据结构（二） 整数集合、跳跃表、压缩列表</a></p><p><a href="https://www.xilidou.com/2018/03/15/redis-object/">Redis 的基础数据结构（三）对象 </a></p><p>欢迎关注我的微信公众号：<br><img data-src="/images/2019-04-25-022206.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2018/03/20/redis-server/</id>
    <link href="https://xilidou.com/2018/03/20/redis-server/"/>
    <published>2018-03-20T17:26:08.000Z</published>
    <summary>
      <![CDATA[<p>之前的文章讲解了 Redis 的数据结构，这回就可以看看作为内存数据库，Redis 是怎么存储数据的。以及键是怎么过期的。</p>
<p>阅读这篇文章你将会了解到：</p>
<ul>
<li>Redis 的数据库实现</li>
<li>Redis 键过期的策略</li>
</ul>]]>
    </summary>
    <title>Redis 数据库、键过期的实现</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Redis" scheme="https://xilidou.com/categories/Redis/"/>
    <category term="redis" scheme="https://xilidou.com/tags/redis/"/>
    <category term="object" scheme="https://xilidou.com/tags/object/"/>
    <category term="redisObject" scheme="https://xilidou.com/tags/redisObject/"/>
    <content>
      <![CDATA[<p>前两篇文章介绍了 Redis 的基本数据结构动态字符串，链表，字典，跳跃表，压缩链表，整数集合，但是使用过  Redis 的同学会发现，平时根本没有使用过这些数据结构。 平时使用的数据结构，包括字符串，列表，哈希，集合，还有有序集合。 其实 Redis 的实现是将底层的一种或者几种数据结构进行结合成我们使用的数据结构。</p><p>所以今天这篇文章就是要解释 Redis 是怎么实现符串，列表，哈希，集合，还有有序集合的。</p><span id="more"></span><h1 id="对象"><a href="#对象" class="headerlink" title="对象"></a>对象</h1><p>对于 Redis 来说使用了 redisObject 来对所有的对象进行了封装：</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><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"></span><br><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><br><span class="line">    <span class="comment">// 对象类型</span></span><br><span class="line">    <span class="keyword">unsigned</span> type:<span class="number">4</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 编码</span></span><br><span class="line">    <span class="keyword">unsigned</span> encoding:<span class="number">4</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 对象最后一次被访问的时间</span></span><br><span class="line">    <span class="keyword">unsigned</span> lru:REDIS_LRU_BITS; <span class="comment">/* lru time (relative to server.lruclock) */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 引用计数</span></span><br><span class="line">    <span class="keyword">int</span> refcount;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 指向实际值的指针</span></span><br><span class="line">    <span class="keyword">void</span> *ptr;</span><br><span class="line"></span><br><span class="line">&#125; robj;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>我们先关注两个参数</p><p><code>type</code> 和 <code>encoding</code> :</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><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><br><span class="line"><span class="comment">/* Object types */</span></span><br><span class="line"><span class="comment">// 对象类型</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_STRING 0</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_LIST 1</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_SET 2</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_ZSET 3</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_HASH 4</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/* Objects encoding. Some kind of objects like Strings and Hashes can be</span></span><br><span class="line"><span class="comment"> * internally represented in multiple ways. The &#x27;encoding&#x27; field of the object</span></span><br><span class="line"><span class="comment"> * is set to one of this fields for this object. */</span></span><br><span class="line"><span class="comment">// 对象编码</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_ENCODING_RAW 0     <span class="comment">/* Raw representation */</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_ENCODING_INT 1     <span class="comment">/* Encoded as integer */</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_ENCODING_HT 2      <span class="comment">/* Encoded as hash table */</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_ENCODING_ZIPMAP 3  <span class="comment">/* Encoded as zipmap */</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_ENCODING_LINKEDLIST 4 <span class="comment">/* Encoded as regular linked list */</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_ENCODING_ZIPLIST 5 <span class="comment">/* Encoded as ziplist */</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_ENCODING_INTSET 6  <span class="comment">/* E  dncoded as intset */</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_ENCODING_SKIPLIST 7  <span class="comment">/* Encoded as skiplist */</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> REDIS_ENCODING_EMBSTR 8  <span class="comment">/* Embedded sds string encoding */</span></span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>所以通过这段代码我们可以知道 Redis 支持的数据类型如下：</p><table><thead><tr><th>type</th><th>类型</th></tr></thead><tbody><tr><td>REDIS_STRING</td><td>字符串</td></tr><tr><td>REDIS_LIST</td><td>列表</td></tr><tr><td>REDIS_SET</td><td>集合</td></tr><tr><td>REDIS_ZSET</td><td>有序集合</td></tr><tr><td>REDIS_HASH</td><td>哈希表</td></tr></tbody></table><p>Redis 的 Object 通过 <code>ptr</code> 指向具体的底层数据。Redis 的底层数据:</p><table><thead><tr><th>编码</th><th>类型</th></tr></thead><tbody><tr><td>REDIS_ENCODING_RAW</td><td>SDS 实现的动态字符串对象</td></tr><tr><td>REDIS_ENCODING_INT</td><td>整数实现的动态字符串对象</td></tr><tr><td>REDIS_ENCODING_HT</td><td>字典实现的 hash 对象</td></tr><tr><td>REDIS_ENCODING_ZIPMAP</td><td>压缩map实现对对象，（3.0）版本未使用</td></tr><tr><td>REDIS_ENCODING_LINKEDLIST</td><td>双向链表实现的对象</td></tr><tr><td>REDIS_ENCODING_ZIPLIST</td><td>压缩列表实现的对象</td></tr><tr><td>REDIS_ENCODING_INTSET</td><td>整数集合实现的对象</td></tr><tr><td>REDIS_ENCODING_SKIPLIST</td><td>跳跃表实现的对象</td></tr><tr><td>REDIS_ENCODING_EMBSTR</td><td>使用 embstr 实现的动态字符串的对象</td></tr></tbody></table><p>PS：下文会解释 RAW 和 EMBSTR 的区别。</p><p>我就按照类型的顺序看看 Redis 是怎么利用底层的数据结构实现不同的对象类型的。</p><h1 id="REDIS-STRING-（字符串）"><a href="#REDIS-STRING-（字符串）" class="headerlink" title="REDIS_STRING （字符串）"></a>REDIS_STRING （字符串）</h1><p>Redis 的字符串 String，主要由 int、raw 和 emstr 底层数据实现的。 Redis 遵循以下的原则来决定使用底层数据结构的使用。</p><ul><li>如果数据是可以用 long 表示的整数，那就直接使用将ptr 的类型设置为long。将RedisObject 的 encoding 设置为 REDIS_ENCODING_INT。</li><li>如果是一个字符串，那就需要考察字符串的字节数。如果字节数小于 39 就是使用 emstr，encoding 就使用 REDIS_ENCODING_EMBSTR，底层依然是我们之前介绍的 SDS 。</li><li>如果字符串的长度超过 39 那就使用 raw，encoding 就是 REDIS_ENCODING_RAW。</li></ul><p>问题来了：</p><ol><li>为什么是 39 个字符？<br>我们所String对象是由一个 RedisObject 和 sdshdr 组成的。所以我们如下公式在<br>在64位的系统中，一个 emstr 最大占用 64bite。<br>RedisObject(16b) + sds header(8b) + emstr + “\0”(1b) &lt;&#x3D; 64<br>简单的 四则运算 emstr &lt;&#x3D; 39。</li><li>一直都是 39 么？<br>在 3.2 的版本的时候，作者对 sdshdr 做了修改，从 39 改成了 44。为什么？<br>之前我们说过一个 sdshdr 包含三个参数，<code>len</code>、<code>free</code> 还有 <code>buf</code>，在3.2之前 len 和 free 的数据类型都是 unsigned int。 这个就是为什么上面的公式 sds header 是 8个字节了。新版本的 sdshdr 变成了 sdshdr8， sdshdr16 和 sdshdr32还有 sdshdr64。优化的地方就在于如果 buf 小，使用更小位数的数据类型来描述 len 和 free 减少他们占用的内存，同时增加了一个<code>char flags</code>。emstr使用了最小的 sdshdr8。 这个时候 sds header 就变成了(len(1b) + free(1b) + flags(1b)) 3个字节， 比之前的实现少了5个字节。 所以新版本的 emstr 的最大字节变成了 44。 还是那句话 Redis 对内存真是 “斤斤计较”</li><li>SDS 是动态的为什么要区分 emstr 和 raw？<br>区别在于生产 raw 的时候，会有两步操作，分别产生 redisObject 和 sdshdr。而 emstr 一次成型，同时生成 redisObject 和 sdshdr 。就是为了高效。同时注意 emstr 是不可变的。</li><li>他们之间是什么关系？<br>如果不能用 long 表示的数据，double 也是使用 raw 或者 emstr 来保存的。<br>按照 Redis 的套路这三个底层数据在条件满足的是是会发生装换的。REDIS_ENCODING_INT 的数据如果不是整数了，那就会变成 raw 或者 emstr。emstr 发生了变化就会变成 raw。</li></ol><h1 id="REDIS-LIST-列表"><a href="#REDIS-LIST-列表" class="headerlink" title="REDIS_LIST 列表"></a>REDIS_LIST 列表</h1><p>Reids 的列表，底层是一个 ziplist 或者 linkedlist。</p><ul><li>当列表对象保存的字符串元素的长度都小于64字节。</li><li>保存的元素数量小于512个。</li></ul><p>两个条件都满足使用ziplist编码，两个条件任意一个不满足时，ziplist会变为linkedlist。</p><p>3.2 以后使用 quicklist 保存。这个数据结构之前没有讲解过。</p><p>实际上 quicklist 是 ziplist 和双向链表结合的产物。我们这样理解，每个双向链表的节点上是一个ziplist。之所以这么设计，应该是空间和时间之间的取舍或者一个折中的方案。 具体的实现我会在以后的文章里面具体分析。</p><h1 id="REDIS-SET-（集合）"><a href="#REDIS-SET-（集合）" class="headerlink" title="REDIS_SET （集合）"></a>REDIS_SET （集合）</h1><p>Redis 的集合底层是一个 intset 或者 一个字典（hashtable）。</p><p>这个比较容易理解：</p><ul><li>当集合都是整数且不超过512个的时候，就使用intset。</li><li>剩下都是用字典。</li></ul><p>使用字典的时候，字典的每一个 key 就是集合的一个元素，对应的 value 就是一个 null。</p><h1 id="REDIS-ZSET-（有序集合）"><a href="#REDIS-ZSET-（有序集合）" class="headerlink" title="REDIS_ZSET （有序集合）"></a>REDIS_ZSET （有序集合）</h1><p>Redis 的有序集合使用 ziplist 或者 skiplist 实现的。</p><ul><li>元素小于 128 个</li><li>每个元素长度 小于 64 字节。</li></ul><p>同时满足以上条件使用ziplist，否则使用skiplist。</p><p>对于 ziplist 的实现，redis 使用相邻的两个 entity 分别保存对象以及对象的排序因子。这样对于插入和查询的复杂度都是 O(n) 的。直接看图：</p><p><img data-src="/images/zset_ziplist.jpg" alt="ziplist"></p><p>元素开发工程师，排序的因子就是月薪。（好吧php是世界上最好的语言）。</p><p>对于skiplist 的实现：</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><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">zset</span>&#123;</span></span><br><span class="line"></span><br><span class="line">    zskiplist *zsl;</span><br><span class="line">    </span><br><span class="line">    dict *dict</span><br><span class="line"></span><br><span class="line">&#125;zset;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>skiplist 的有序链表的实现不只是只有一个 skiplist ，还有一个字典存储对象的key 和 排序因子的映射，这个是为了保证按照key 查询的时候时间负责度为 O(1)。同时有序性依赖 skiplist 维护。大家可以看我之前的教程。所以直接看图：</p><p><img data-src="/images/zset.jpg" alt="zset"></p><h1 id="REDIS-HASH-hash表"><a href="#REDIS-HASH-hash表" class="headerlink" title="REDIS_HASH (hash表)"></a>REDIS_HASH (hash表)</h1><p>Redis 的 hash 表 使用 ziplist 和 字典 实现的。</p><ul><li>键值对的键和值都小于 64 个字节</li><li>键值对的数量小于 512。</li></ul><p>都满足的时候使用 ziplist，否则使用字典。</p><p>ziplist 的实现类似，类似 zset 的实现。两个entity成对出现。一个存储key，另一个存储 velue。</p><p><img data-src="/images/zset_ziplist.jpg" alt="ziplist"> </p><p>还是可以使用上面使用过的图。这个时候 entity 不用排序。key 是职位名称，velue 是对应的月薪。（好吧php还是世界上最好的语言）。与zset实现的区别就是查询是 O(n) 的，插入直接往tail后面插入就行时间复杂度O(1)。</p><p>使用字典实现一个 hash表。好像没有什么可以多说的。</p><h1 id="int-refcount（引用计数器）"><a href="#int-refcount（引用计数器）" class="headerlink" title="int refcount（引用计数器）"></a>int refcount（引用计数器）</h1><p>这个参数是引用计数。Redis 自己管理内存，所以就使用了最简单的内存管理方式–引用计数。</p><ul><li>创建对象的时候计数器为1</li><li>每被一个地方引用，计数器加一</li><li>每被取消引用，计数器减一</li><li>计数器为0的时候，就说明没有地方需要这个对象了。内存就会被 Redis 回收。</li></ul><h1 id="unsigned-lru-REDIS-LRU-BITS"><a href="#unsigned-lru-REDIS-LRU-BITS" class="headerlink" title="unsigned lru:REDIS_LRU_BITS"></a>unsigned lru:REDIS_LRU_BITS</h1><p>这个参数记录了对象的最后一次活跃时间。</p><p>如果 Redis 开启了淘汰策略，且淘汰的方式是 LRU 的时候，这个参数就派上了用场。Redis 会优先回收 lru 最久的对象。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>至此 Redis 的数据结构就介绍完了。</p><p>大家可以阅读之前的文章：</p><p><a href="https://xilidou.com/2018/03/12/redis-data/?fromblog=redis-object">Redis 的基础数据结构（一） 可变字符串、链表、字典</a></p><p><a href="https://www.xilidou.com/2018/03/13/redis-data2/?blogfrom=redis-object">Redis 的基础数据结构（二） 整数集合、跳跃表、压缩列表</a></p><p>欢迎关注我的微信公众号：<br><img data-src="/images/2019-04-25-022208.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2018/03/15/redis-object/</id>
    <link href="https://xilidou.com/2018/03/15/redis-object/"/>
    <published>2018-03-15T15:25:35.000Z</published>
    <summary>
      <![CDATA[<p>前两篇文章介绍了 Redis 的基本数据结构动态字符串，链表，字典，跳跃表，压缩链表，整数集合，但是使用过  Redis 的同学会发现，平时根本没有使用过这些数据结构。 平时使用的数据结构，包括字符串，列表，哈希，集合，还有有序集合。 其实 Redis 的实现是将底层的一种或者几种数据结构进行结合成我们使用的数据结构。</p>
<p>所以今天这篇文章就是要解释 Redis 是怎么实现符串，列表，哈希，集合，还有有序集合的。</p>]]>
    </summary>
    <title>Redis 的基础数据结构（三）对象</title>
    <updated>2026-08-08T02:04:49.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Redis" scheme="https://xilidou.com/categories/Redis/"/>
    <category term="redis" scheme="https://xilidou.com/tags/redis/"/>
    <category term="iniset" scheme="https://xilidou.com/tags/iniset/"/>
    <category term="skiplist" scheme="https://xilidou.com/tags/skiplist/"/>
    <category term="ziplist" scheme="https://xilidou.com/tags/ziplist/"/>
    <content>
      <![CDATA[<p>上篇文章写了 Redis 基础数据结构的可变字符串、链表、字典。大家可以点击<a href="https://xilidou.com/2018/03/12/redis-data/">链接</a>查看。今天我们继续研究 Redis 的基础数据结构。</p><ul><li>整数集合</li><li>跳跃表</li><li>压缩列表</li></ul><span id="more"></span><h1 id="整数集合"><a href="#整数集合" class="headerlink" title="整数集合"></a>整数集合</h1><p>当一个集合只包含整数，且这个集合的元素不多的时候，Redis 就会使用整数集合 intset 。首先看 intset 的数据结构：</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><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">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">intset</span> &#123;</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 编码方式</span></span><br><span class="line">    <span class="keyword">uint32_t</span> encoding;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 集合包含的元素数量</span></span><br><span class="line">    <span class="keyword">uint32_t</span> length;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 保存元素的数组</span></span><br><span class="line">    <span class="keyword">int8_t</span> contents[];</span><br><span class="line">&#125; intset;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>其实 intset 的数据结构比较好理解。一个数据保存元素，length 保存元素的数量，也就是contents的大小，encoding 用于保存数据的编码方式。</p><p>通过代码我们可以知道，encoding 的编码类型包括了：</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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> INTSET_ENC_INT16 (sizeof(int16_t))</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> INTSET_ENC_INT32 (sizeof(int32_t))</span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> INTSET_ENC_INT64 (sizeof(int64_t))</span></span><br></pre></td></tr></table></figure><p>实际上我们可以看出来。 Redis encoding的类型，就是指数据的大小。作为一个内存数据库，采用这种设计就是为了节约内存。</p><p>既然有从小到大的三个数据结构，在插入数据的时候尽可能使用小的数据结构来节约内存，如果插入的数据大于原有的数据结构，就会触发扩容。</p><p>扩容有三个步骤：</p><ol><li>根据新元素的类型，修改整个数组的数据类型，并重新分配空间</li><li>将原有的的数据，装换为新的数据类型，重新放到应该在的位置上，且保存顺序性</li><li>再插入新元素</li></ol><p>整数集合不支持降级操作，一旦升级就不能降级了。</p><h1 id="跳跃表"><a href="#跳跃表" class="headerlink" title="跳跃表"></a>跳跃表</h1><p>跳跃表是链表的一种，是一种利用空间换时间的数据结构。跳表平均支持 O(logN)，最坏O(N)复杂度的查找。</p><p>跳表是由一个zskiplist 和 多个 zskiplistNode 组成。我们先看看他们的结构：</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><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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/* ZSETs use a specialized version of Skiplists */</span></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="comment"> */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 成员对象</span></span><br><span class="line">    robj *obj;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 分值</span></span><br><span class="line">    <span class="keyword">double</span> score;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 后退指针</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> *<span class="title">backward</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 层</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistLevel</span> &#123;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// 前进指针</span></span><br><span class="line">        <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> *<span class="title">forward</span>;</span></span><br><span class="line"></span><br><span class="line">        <span class="comment">// 跨度</span></span><br><span class="line">        <span class="keyword">unsigned</span> <span class="keyword">int</span> span;</span><br><span class="line"></span><br><span class="line">    &#125; level[];</span><br><span class="line"></span><br><span class="line">&#125; zskiplistNode;</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="comment"> */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplist</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 表头节点和表尾节点</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> *<span class="title">header</span>, *<span class="title">tail</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 表中节点的数量</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">long</span> length;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 表中层数最大的节点的层数</span></span><br><span class="line">    <span class="keyword">int</span> level;</span><br><span class="line"></span><br><span class="line">&#125; zskiplist;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>所以根据这个代码我们可以画出如下的结构图：</p><p><img data-src="/images/zskiplist.jpg" alt="zskiplist"></p><p>其实跳表就是一个利用空间换时间的数据结构，利用 level 作为链表的索引。</p><p>之前有人问过 Redis 的作者 为什么使用跳跃表，而不是 tree 来构建索引？作者的回答是：</p><ol><li>省内存。</li><li>服务于 ZRANGE 或者 ZREVRANGE 是一个典型的链表场景。时间复杂度的表现和平衡树差不多。</li><li>最重要的一点是跳跃表的实现很简单就能达到 O(logN)的级别。</li></ol><h1 id="压缩列表"><a href="#压缩列表" class="headerlink" title="压缩列表"></a>压缩列表</h1><p>压缩链表 Redis 作者的介绍是，为了尽可能节约内存设计出来的双向链表。</p><p>对于一个压缩列表代码里注释给出的数据结构如下：</p><p><img data-src="/images/ziplist.jpg" alt="ziplist"></p><p><code>zlbytes</code> 表示的是整个压缩列表使用的内存字节数</p><p><code>zltail</code> 指定了压缩列表的尾节点的偏移量</p><p><code>zllen</code> 是压缩列表 entry 的数量</p><p><code>entry</code> 就是 ziplist 的节点</p><p><code>zlend</code> 标记压缩列表的末端</p><p>这个列表中还有单个指针：</p><p><code>ZIPLIST_ENTRY_HEAD</code> 列表开始节点的头偏移量</p><p><code>ZIPLIST_ENTRY_TAIL</code> 列表结束节点的头偏移量</p><p><code>ZIPLIST_ENTRY_END</code> 列表的尾节点结束的偏移量</p><p>再看看一个 entry 的结构：</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><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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * 保存 ziplist 节点信息的结构</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">zlentry</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// prevrawlen ：前置节点的长度</span></span><br><span class="line">    <span class="comment">// prevrawlensize ：编码 prevrawlen 所需的字节大小</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">int</span> prevrawlensize, prevrawlen;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// len ：当前节点值的长度</span></span><br><span class="line">    <span class="comment">// lensize ：编码 len 所需的字节大小</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">int</span> lensize, len;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 当前节点 header 的大小</span></span><br><span class="line">    <span class="comment">// 等于 prevrawlensize + lensize</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">int</span> headersize;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 当前节点值所使用的编码类型</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">char</span> encoding;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 指向当前节点的指针</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">char</span> *p;</span><br><span class="line"></span><br><span class="line">&#125; zlentry;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>依次解释一下这几个参数。</p><p><code>prevrawlen</code> 前置节点的长度，这里多了一个 size，其实是记录了 prevrawlen 的尺寸。Redis 为了节约内存并不是直接使用默认的 int 的长度，而是逐渐升级的。<br>同理 <code>len</code> 记录的是当前节点的长度，<code>lensize</code> 记录的是 len 的长度。<br><code>headersize</code> 就是前文提到的两个 size 之和。<br><code>encoding</code> 就是这个节点的数据类型。这里注意一下 encoding 的类型只包括整数和字符串。<br><code>p</code> 节点的指针，不用过多的解释。</p><p>需要注意一点，因为每个节点都保存了前一个节点的长度，如果发生了更新或者删除节点，则这个节点之后的数据也需要修改，有一种最坏的情况就是如果每个节点都处于需要扩容的零界点，就会造成这个节点之后的节点都要修改 size 这个参数，引发连锁反应。这个时候就是 压缩链表最坏的时间复杂度 O(n^2)。不过所有节点都处于临界值，这样的概率可以说比较小。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>至此Redis的基本数据结构就介绍完了。我们可以看到 Redis 对内存的使用真是“斤斤计较”，对于内存是使用特别节约。同时 Redis 作为一个单线程应用，不用考虑并发的问题，将很多类似 size 或者 length 的参数暴露出来，将很多 O(n) 的操作降低为 O(1)。大大提升效率。下一讲，将会介绍 Redis 是怎么通过这些数据结构向外提供服务。<br>Redis 的代码真是写的太棒了，简洁高效。值得大家学习。</p><p>欢迎关注我的微信公众号：<br><img data-src="/images/2019-04-25-022203.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2018/03/13/redis-data2/</id>
    <link href="https://xilidou.com/2018/03/13/redis-data2/"/>
    <published>2018-03-13T22:14:00.000Z</published>
    <summary>
      <![CDATA[<p>上篇文章写了 Redis 基础数据结构的可变字符串、链表、字典。大家可以点击<a href="https://xilidou.com/2018/03/12/redis-data/">链接</a>查看。今天我们继续研究 Redis 的基础数据结构。</p>
<ul>
<li>整数集合</li>
<li>跳跃表</li>
<li>压缩列表</li>
</ul>]]>
    </summary>
    <title>Redis 的基础数据结构（二） 整数集合、跳跃表、压缩列表</title>
    <updated>2026-08-08T02:04:49.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Redis" scheme="https://xilidou.com/categories/Redis/"/>
    <category term="redis" scheme="https://xilidou.com/tags/redis/"/>
    <category term="list" scheme="https://xilidou.com/tags/list/"/>
    <category term="map" scheme="https://xilidou.com/tags/map/"/>
    <category term="hash" scheme="https://xilidou.com/tags/hash/"/>
    <content>
      <![CDATA[<p>这周开始学习 Redis，看看Redis是怎么实现的。所以会写一系列关于 Redis的文章。这篇文章关于 Redis 的基础数据。阅读这篇文章你可以了解：</p><ul><li>动态字符串（SDS）</li><li>链表</li><li>字典</li></ul><p>三个数据结构 Redis 是怎么实现的。</p><span id="more"></span><p>R</p><p>SDS （Simple Dynamic String）是 Redis 最基础的数据结构。直译过来就是”简单的动态字符串“。Redis 自己实现了一个动态的字符串，而不是直接使用了 C 语言中的字符串。</p><p>sds 的数据结构：</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><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">sdshdr</span> &#123;</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// buf 中已占用空间的长度</span></span><br><span class="line">    <span class="keyword">int</span> len;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// buf 中剩余可用空间的长度</span></span><br><span class="line">    <span class="keyword">int</span> <span class="built_in">free</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 数据空间</span></span><br><span class="line">    <span class="keyword">char</span> buf[];</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>所以一个 SDS 的就如下图：</p><p><img data-src="/images/2019-04-25-022158.jpg" alt="sds"></p><p>所以我们看到，sds 包含3个参数。buf 的长度 len，buf 的剩余长度，以及buf。</p><p>为什么这么设计呢？</p><ul><li><p>可以直接获取字符串长度。<br>  C 语言中，获取字符串的长度需要用指针遍历字符串，时间复杂度为 O(n)，而 SDS 的长度，直接从len 获取复杂度为 O(1)。</p></li><li><p>杜绝缓冲区溢出。<br>  由于C 语言不记录字符串长度，如果增加一个字符传的长度，如果没有注意就可能溢出，覆盖了紧挨着这个字符的数据。对于SDS 而言增加字符串长度需要验证 free的长度，如果free 不够就会扩容整个 buf，防止溢出。</p></li><li><p>减少修改字符串长度时造成的内存再次分配。<br>  redis 作为高性能的内存数据库，需要较高的相应速度。字符串也很大概率的频繁修改。 SDS 通过未使用空间这个参数，将字符串的长度和底层buf的长度之间的额关系解除了。buf的长度也不是字符串的长度。基于这个分设计 SDS 实现了空间的预分配和惰性释放。</p><ol><li>预分配<br>  如果对 SDS 修改后，如果 len 小于 1MB 那 len &#x3D; 2 * len + 1byte。 这个 1 是用于保存空字节。<br>  如果 SDS 修改后 len 大于 1MB 那么 len &#x3D; 1MB + len + 1byte。</li><li>惰性释放<br>  如果缩短 SDS 的字符串长度，redis并不是马上减少 SDS 所占内存。只是增加 free 的长度。同时向外提供 API 。真正需要释放的时候，才去重新缩小 SDS 所占的内存</li></ol></li><li><p>二进制安全。<br>  C 语言中的字符串是以 ”\0“ 作为字符串的结束标记。而 SDS 是使用 len 的长度来标记字符串的结束。所以SDS 可以存储字符串之外的任意二进制流。因为有可能有的二进制流在流中就包含了”\0“造成字符串提前结束。也就是说 SDS 不依赖 “\0” 作为结束的依据。</p></li><li><p>兼容C语言<br>  SDS 按照惯例使用 ”\0“ 作为结尾的管理。部分普通C 语言的字符串 API 也可以使用。</p></li></ul><h2 id="链表"><a href="#链表" class="headerlink" title="链表"></a>链表</h2><p>C语言中并没有链表这个数据结构所以 Redis 自己实现了一个。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><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><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 前置节点</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> *<span class="title">prev</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 后置节点</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> *<span class="title">next</span>;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 节点的值</span></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>非常典型的双向链表的数据结构。</p><p>同时为双向链表提供了如下操作的函数：</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><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></pre></td><td class="code"><pre><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="comment"> */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">listIter</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 当前迭代到的节点</span></span><br><span class="line">    listNode *next;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 迭代的方向</span></span><br><span class="line">    <span class="keyword">int</span> direction;</span><br><span class="line"></span><br><span class="line">&#125; listIter;</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="comment"> */</span></span><br><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">list</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 表头节点</span></span><br><span class="line">    listNode *head;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 表尾节点</span></span><br><span class="line">    listNode *tail;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 节点值复制函数</span></span><br><span class="line">    <span class="keyword">void</span> *(*dup)(<span class="keyword">void</span> *ptr);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 节点值释放函数</span></span><br><span class="line">    <span class="keyword">void</span> (*<span class="built_in">free</span>)(<span class="keyword">void</span> *ptr);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 节点值对比函数</span></span><br><span class="line">    <span class="keyword">int</span> (*match)(<span class="keyword">void</span> *ptr, <span class="keyword">void</span> *key);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 链表所包含的节点数量</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">long</span> len;</span><br><span class="line"></span><br><span class="line">&#125; <span class="built_in">list</span>;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>链表的结构比较简单，数据结构如下：</p><p><img data-src="/images/2019-04-25-22159.jpg" alt="list"></p><p>总结一下性质：</p><ul><li>双向链表，某个节点寻找上一个或者下一个节点时间复杂度 O(1)。</li><li>list 记录了 head 和 tail，寻找 head 和 tail 的时间复杂度为 O(1)。</li><li>获取链表的长度 len 时间复杂度 O(1)。</li></ul><h2 id="字典"><a href="#字典" class="headerlink" title="字典"></a>字典</h2><p>字典数据结构极其类似 java 中的 Hashmap。</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><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 class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dictEntry</span> &#123;</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 键</span></span><br><span class="line">    <span class="keyword">void</span> *key;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 值</span></span><br><span class="line">    <span class="class"><span class="keyword">union</span> &#123;</span></span><br><span class="line">        <span class="keyword">void</span> *val;</span><br><span class="line">        <span class="keyword">uint64_t</span> u64;</span><br><span class="line">        <span class="keyword">int64_t</span> s64;</span><br><span class="line">    &#125; v;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 指向下个哈希表节点，形成链表</span></span><br><span class="line">    <span class="class"><span class="keyword">struct</span> <span class="title">dictEntry</span> *<span class="title">next</span>;</span></span><br><span class="line"></span><br><span class="line">&#125; dictEntry;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>实际上哈希表节点就是一个单项列表的节点。保存了一下下一个节点的指针。 key 就是节点的键，v是这个节点的值。这个 v 既可以是一个指针，也可以是一个 <code>uint64_t</code>或者 <code>int64_t</code> 整数。*next 指向下一个节点。</p><p>通过一个哈希表的数组把各个节点链接起来：</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><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></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">dictht</span> &#123;</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 哈希表数组</span></span><br><span class="line">    dictEntry **table;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 哈希表大小</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">long</span> size;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 哈希表大小掩码，用于计算索引值</span></span><br><span class="line">    <span class="comment">// 总是等于 size - 1</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">long</span> sizemask;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 该哈希表已有节点的数量</span></span><br><span class="line">    <span class="keyword">unsigned</span> <span class="keyword">long</span> used;</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>dictht</p><p>通过图示我们观察：</p><p><img data-src="/images/2019-04-25-022159.jpg" alt="dictht.png"></p><p>实际上，如果对java 的基本数据结构了解的同学就会发现，这个数据结构和 java 中的 HashMap 是很类似的，就是数组加链表的结构。</p><p>字典的数据结构：</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><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"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dict</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 类型特定函数</span></span><br><span class="line">    dictType *type;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 私有数据</span></span><br><span class="line">    <span class="keyword">void</span> *privdata;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 哈希表</span></span><br><span class="line">    dictht ht[<span class="number">2</span>];</span><br><span class="line"></span><br><span class="line">    <span class="comment">// rehash 索引</span></span><br><span class="line">    <span class="comment">// 当 rehash 不在进行时，值为 -1</span></span><br><span class="line">    <span class="keyword">int</span> rehashidx; <span class="comment">/* rehashing not in progress if rehashidx == -1 */</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 目前正在运行的安全迭代器的数量</span></span><br><span class="line">    <span class="keyword">int</span> iterators; <span class="comment">/* number of iterators currently running */</span></span><br><span class="line"></span><br><span class="line">&#125; dict;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>其中的dictType 是一组方法，代码如下：</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><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"><span class="comment">/*</span></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">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dictType</span> &#123;</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// 计算哈希值的函数</span></span><br><span class="line">    <span class="function"><span class="keyword">unsigned</span> <span class="title">int</span> <span class="params">(*hashFunction)</span><span class="params">(<span class="keyword">const</span> <span class="keyword">void</span> *key)</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 复制键的函数</span></span><br><span class="line">    <span class="keyword">void</span> *(*keyDup)(<span class="keyword">void</span> *privdata, <span class="keyword">const</span> <span class="keyword">void</span> *key);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 复制值的函数</span></span><br><span class="line">    <span class="keyword">void</span> *(*valDup)(<span class="keyword">void</span> *privdata, <span class="keyword">const</span> <span class="keyword">void</span> *obj);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 对比键的函数</span></span><br><span class="line">    <span class="keyword">int</span> (*keyCompare)(<span class="keyword">void</span> *privdata, <span class="keyword">const</span> <span class="keyword">void</span> *key1, <span class="keyword">const</span> <span class="keyword">void</span> *key2);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 销毁键的函数</span></span><br><span class="line">    <span class="keyword">void</span> (*keyDestructor)(<span class="keyword">void</span> *privdata, <span class="keyword">void</span> *key);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// 销毁值的函数</span></span><br><span class="line">    <span class="keyword">void</span> (*valDestructor)(<span class="keyword">void</span> *privdata, <span class="keyword">void</span> *obj);</span><br><span class="line"></span><br><span class="line">&#125; dictType;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>字典的数据结构如下图：</p><p><img data-src="/images/2019-04-25-022200.jpg" alt="dict"></p><p>这里我们可以看到一个dict 拥有两个 dictht。一般来说只使用 ht[0],当扩容的时候发生了rehash的时候，ht[1]才会被使用。</p><p>当我们观察或者研究一个hash结构的时候偶我们首先要考虑的这个 dict 如何插入一个数据？</p><p>我们梳理一下插入数据的逻辑。</p><ul><li><p>计算Key 的 hash 值。找到 hash 映射到 table 数组的位置。</p></li><li><p>如果数据已经有一个 key 存在了。那就意味着发生了 hash 碰撞。新加入的节点，就会作为链表的一个节点接到之前节点的 next 指针上。</p></li><li><p>如果 key 发生了多次碰撞，造成链表的长度越来越长。会使得字典的查询速度下降。为了维持正常的负载。Redis 会对 字典进行 rehash 操作。来增加 table 数组的长度。所以我们要着重了解一下 Redis 的 rehash。步骤如下：</p><ol><li>根据ht[0] 的数据和操作的类型（扩大或缩小），分配 ht[1] 的大小。</li><li>将 ht[0] 的数据 rehash 到 ht[1] 上。</li><li>rehash 完成以后，将ht[1] 设置为 ht[0]，生成一个新的ht[1]备用。</li></ol></li><li><p>渐进式的 rehash 。<br>其实如果字典的 key 数量很大，达到千万级以上，rehash 就会是一个相对较长的时间。所以为了字典能够在 rehash 的时候能够继续提供服务。Redis 提供了一个渐进式的 rehash 实现，rehash的步骤如下：</p><ol><li>分配 ht[1] 的空间，让字典同时持有 ht[1] 和 ht[0]。</li><li>在字典中维护一个 rehashidx，设置为 0 ，表示字典正在 rehash。</li><li>在rehash期间，每次对字典的操作除了进行指定的操作以外，都会根据 ht[0] 在 rehashidx 上对应的键值对 rehash 到 ht[1]上。</li><li>随着操作进行， ht[0] 的数据就会全部 rehash 到 ht[1] 。设置ht[0] 的 rehashidx 为 -1，渐进的 rehash 结束。</li></ol></li></ul><p>这样保证数据能够平滑的进行 rehash。防止 rehash 时间过久阻塞线程。</p><ul><li>在进行 rehash 的过程中，如果进行了 delete 和 update 等操作，会在两个哈希表上进行。如果是 find 的话优先在ht[0] 上进行，如果没有找到，再去 ht[1] 中查找。如果是 insert 的话那就只会在 ht[1]中插入数据。这样就会保证了 ht[1] 的数据只增不减，ht[0]的数据只减不增。</li></ul>]]>
    </content>
    <id>https://xilidou.com/2018/03/12/redis-data/</id>
    <link href="https://xilidou.com/2018/03/12/redis-data/"/>
    <published>2018-03-12T12:46:44.000Z</published>
    <summary>
      <![CDATA[<p>这周开始学习 Redis，看看Redis是怎么实现的。所以会写一系列关于 Redis的文章。这篇文章关于 Redis 的基础数据。阅读这篇文章你可以了解：</p>
<ul>
<li>动态字符串（SDS）</li>
<li>链表</li>
<li>字典</li>
</ul>
<p>三个数据结构 Redis 是怎么实现的。</p>]]>
    </summary>
    <title>Redis 的基础数据结构（一） 可变字符串、链表、字典</title>
    <updated>2026-08-08T02:04:49.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="多线程" scheme="https://xilidou.com/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    <category term="线程池" scheme="https://xilidou.com/tags/%E7%BA%BF%E7%A8%8B%E6%B1%A0/"/>
    <content>
      <![CDATA[<p>最近在看<a href="https://union-click.jd.com/jdc?d=igYcKg">《Java并发编程的艺术》</a>回顾线程池的原理和参数的时候发现一个问题，如果 corePoolSize &#x3D; 0 且 阻塞队列是无界的。线程池将如何工作？</p><p>我们先回顾一下书里面描述线程池<code>execute()</code>工作的逻辑：</p><ol><li>如果当前运行的线程，少于corePoolSize，则创建一个新的线程来执行任务。</li><li>如果运行的线程等于或多于 corePoolSize，将任务加入 BlockingQueue。</li><li>如果 BlockingQueue 内的任务超过上限，则创建新的线程来处理任务。</li><li>如果创建的线程数是单钱运行的线程超出 maximumPoolSize，任务将被拒绝策略拒绝。</li></ol><p>看了这四个步骤，其实描述上是有一个漏洞的。如果核心线程数是0，阻塞队列也是无界的，会怎样？如果按照上文的逻辑，应该没有线程会被运行，然后线程无限的增加到队列里面。然后呢？</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><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="class"><span class="keyword">class</span> <span class="title">threadTest</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">static</span> ThreadPoolExecutor executor = <span class="keyword">new</span> ThreadPoolExecutor(<span class="number">0</span>,<span class="number">1</span>,<span class="number">0</span>, TimeUnit.MILLISECONDS,<span class="keyword">new</span> LinkedBlockingQueue&lt;Runnable&gt;());</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line">        AtomicInteger atomicInteger = <span class="keyword">new</span> AtomicInteger();</span><br><span class="line">        <span class="keyword">while</span> (<span class="keyword">true</span>) &#123;</span><br><span class="line">            executor.execute(() -&gt; &#123;</span><br><span class="line">                System.out.println(atomicInteger.getAndAdd(<span class="number">1</span>));</span><br><span class="line">            &#125;);</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>System.out.println(atomicInteger.getAndAdd(1));</code>语句执行了，与上面的描述矛盾了。到底发生了什么？线程池创建线程的逻辑是什么？我们还是从源码来看看到底线程池的逻辑是什么？</p><h2 id="ctl"><a href="#ctl" class="headerlink" title="ctl"></a>ctl</h2><p>要了解线程池，我们首先要了解的线程池里面的状态控制的参数 ctl。</p><ul><li>线程池的ctl是一个原子的 AtomicInteger。</li><li>这个ctl包含两个参数 ：<ul><li>workerCount 激活的线程数</li><li>runState 当前线程池的状态</li></ul></li><li>它的低29位用于存放当前的线程数, 因此一个线程池在理论上最大的线程数是 536870911; 高 3 位是用于表示当前线程池的状态, 其中高三位的值和状态对应如下:<ul><li>111: RUNNING</li><li>000: SHUTDOWN</li><li>001: STOP</li><li>010: TIDYING</li><li>110: TERMINATED</li></ul></li></ul><p>为了能够使用 ctl 线程池提供了三个方法:</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="comment">// Packing and unpacking ctl</span></span><br><span class="line"><span class="comment">// 获取线程池的状态</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">int</span> <span class="title">runStateOf</span><span class="params">(<span class="keyword">int</span> c)</span>     </span>&#123; <span class="keyword">return</span> c &amp; ~CAPACITY; &#125;</span><br><span class="line"><span class="comment">// 获取线程池的工作线程数</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">int</span> <span class="title">workerCountOf</span><span class="params">(<span class="keyword">int</span> c)</span>  </span>&#123; <span class="keyword">return</span> c &amp; CAPACITY; &#125;</span><br><span class="line"><span class="comment">// 根据工作线程数和线程池状态获取 ctl</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">int</span> <span class="title">ctlOf</span><span class="params">(<span class="keyword">int</span> rs, <span class="keyword">int</span> wc)</span> </span>&#123; <span class="keyword">return</span> rs | wc; &#125;</span><br></pre></td></tr></table></figure><h2 id="execute"><a href="#execute" class="headerlink" title="execute"></a>execute</h2><p>外界通过 execute 这个方法来向线程池提交任务。</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><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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">execute</span><span class="params">(Runnable command)</span> </span>&#123;</span><br><span class="line">       <span class="keyword">if</span> (command == <span class="keyword">null</span>)</span><br><span class="line">           <span class="keyword">throw</span> <span class="keyword">new</span> NullPointerException();</span><br><span class="line">       <span class="keyword">int</span> c = ctl.get();</span><br><span class="line"></span><br><span class="line">       <span class="comment">//如果工作线程数小于核心线程数，</span></span><br><span class="line">       <span class="keyword">if</span> (workerCountOf(c) &lt; corePoolSize) &#123;</span><br><span class="line">           <span class="comment">//执行addWork，提交为核心线程,提交成功return。提交失败重新获取ctl</span></span><br><span class="line">           <span class="keyword">if</span> (addWorker(command, <span class="keyword">true</span>))</span><br><span class="line">               <span class="keyword">return</span>;</span><br><span class="line">           c = ctl.get();</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">if</span> (isRunning(c) &amp;&amp; workQueue.offer(command)) &#123;</span><br><span class="line"></span><br><span class="line">           <span class="comment">//recheck 需要再次检查,主要目的是判断加入到阻塞队里中的线程是否可以被执行</span></span><br><span class="line">           <span class="keyword">int</span> recheck = ctl.get();</span><br><span class="line"></span><br><span class="line">           <span class="comment">//如果线程池状态不为running，将任务从阻塞队列里面移除，启用拒绝策略</span></span><br><span class="line">           <span class="keyword">if</span> (! isRunning(recheck) &amp;&amp; remove(command))</span><br><span class="line">               reject(command);</span><br><span class="line">           <span class="comment">// 如果线程池的工作线程为零，则调用addWoker提交任务</span></span><br><span class="line">           <span class="keyword">else</span> <span class="keyword">if</span> (workerCountOf(recheck) == <span class="number">0</span>)</span><br><span class="line">               addWorker(<span class="keyword">null</span>, <span class="keyword">false</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">else</span> <span class="keyword">if</span> (!addWorker(command, <span class="keyword">false</span>))</span><br><span class="line">           reject(command);</span><br><span class="line">   &#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="addWorker"><a href="#addWorker" class="headerlink" title="addWorker"></a>addWorker</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><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><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">boolean</span> <span class="title">addWorker</span><span class="params">(Runnable firstTask, <span class="keyword">boolean</span> core)</span> </span>&#123;</span><br><span class="line">    retry:</span><br><span class="line">    <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">        <span class="keyword">int</span> c = ctl.get();</span><br><span class="line">        <span class="comment">//获取线程池状态</span></span><br><span class="line">        <span class="keyword">int</span> rs = runStateOf(c);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Check if queue empty only if necessary.</span></span><br><span class="line">        <span class="comment">// 判断是否可以添加任务。</span></span><br><span class="line">        <span class="keyword">if</span> (rs &gt;= SHUTDOWN &amp;&amp;</span><br><span class="line">            ! (rs == SHUTDOWN &amp;&amp;</span><br><span class="line">               firstTask == <span class="keyword">null</span> &amp;&amp;</span><br><span class="line">               ! workQueue.isEmpty()))</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">            <span class="comment">//获取工作线程数量</span></span><br><span class="line">            <span class="keyword">int</span> wc = workerCountOf(c);</span><br><span class="line">            <span class="comment">//是否大于线程池上限，是否大于核心线程数，或者最大线程数</span></span><br><span class="line">            <span class="keyword">if</span> (wc &gt;= CAPACITY ||</span><br><span class="line">                wc &gt;= (core ? corePoolSize : maximumPoolSize))</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">            <span class="comment">//CAS 增加工作线程数</span></span><br><span class="line">            <span class="keyword">if</span> (compareAndIncrementWorkerCount(c))</span><br><span class="line">                <span class="keyword">break</span> retry;</span><br><span class="line">            c = ctl.get();  <span class="comment">// Re-read ctl</span></span><br><span class="line">            <span class="comment">//如果线程池状态改变，回到开始重新来</span></span><br><span class="line">            <span class="keyword">if</span> (runStateOf(c) != rs)</span><br><span class="line">                <span class="keyword">continue</span> retry;</span><br><span class="line">            <span class="comment">// else CAS failed due to workerCount change; retry inner loop</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">boolean</span> workerStarted = <span class="keyword">false</span>;</span><br><span class="line">    <span class="keyword">boolean</span> workerAdded = <span class="keyword">false</span>;</span><br><span class="line">    Worker w = <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//上面的逻辑是考虑是否能够添加线程，如果可以就cas的增加工作线程数量</span></span><br><span class="line">    <span class="comment">//下面正式启动线程</span></span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment">//新建worker</span></span><br><span class="line">        w = <span class="keyword">new</span> Worker(firstTask);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//获取当前线程</span></span><br><span class="line">        <span class="keyword">final</span> Thread t = w.thread;</span><br><span class="line">        <span class="keyword">if</span> (t != <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="comment">//获取可重入锁</span></span><br><span class="line">            <span class="keyword">final</span> ReentrantLock mainLock = <span class="keyword">this</span>.mainLock;</span><br><span class="line">            <span class="comment">//锁住</span></span><br><span class="line">            mainLock.lock();</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                <span class="comment">// Recheck while holding lock.</span></span><br><span class="line">                <span class="comment">// Back out on ThreadFactory failure or if</span></span><br><span class="line">                <span class="comment">// shut down before lock acquired.</span></span><br><span class="line">                <span class="keyword">int</span> rs = runStateOf(ctl.get());</span><br><span class="line">                <span class="comment">// rs &lt; SHUTDOWN ==&gt; 线程处于RUNNING状态</span></span><br><span class="line">                <span class="comment">// 或者线程处于SHUTDOWN状态，且firstTask == null（可能是workQueue中仍有未执行完成的任务，创建没有初始任务的worker线程执行）</span></span><br><span class="line">                <span class="keyword">if</span> (rs &lt; SHUTDOWN ||</span><br><span class="line">                    (rs == SHUTDOWN &amp;&amp; firstTask == <span class="keyword">null</span>)) &#123;</span><br><span class="line">                    <span class="comment">// 当前线程已经启动，抛出异常</span></span><br><span class="line">                    <span class="keyword">if</span> (t.isAlive()) <span class="comment">// precheck that t is startable</span></span><br><span class="line">                        <span class="keyword">throw</span> <span class="keyword">new</span> IllegalThreadStateException();</span><br><span class="line">                    <span class="comment">//workers 是一个 HashSet 必须在 lock的情况下操作。</span></span><br><span class="line">                    workers.add(w);</span><br><span class="line">                    <span class="keyword">int</span> s = workers.size();</span><br><span class="line">                    <span class="comment">//设置 largeestPoolSize 标记workAdded</span></span><br><span class="line">                    <span class="keyword">if</span> (s &gt; largestPoolSize)</span><br><span class="line">                        largestPoolSize = s;</span><br><span class="line">                    workerAdded = <span class="keyword">true</span>;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                mainLock.unlock();</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">//如果添加成功，启动线程</span></span><br><span class="line">            <span class="keyword">if</span> (workerAdded) &#123;</span><br><span class="line">                t.start();</span><br><span class="line">                workerStarted = <span class="keyword">true</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        <span class="comment">//启动线程失败，回滚。</span></span><br><span class="line">        <span class="keyword">if</span> (! workerStarted)</span><br><span class="line">            addWorkerFailed(w);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> workerStarted;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>先看看 <code>addWork()</code> 的两个参数，第一个是需要提交的线程 Runnable firstTask，第二个参数是 boolean 类型，表示是否为核心线程。</p><p>execute() 中有三处调用了 <code>addWork()</code> 我们逐一分析。</p><ul><li>第一次，条件 <code>if (workerCountOf(c) &lt; corePoolSize)</code> 这个很好理解，工作线程数少于核心线程数，提交任务。所以 <code>addWorker(command, true)</code>。</li><li>第二次，如果 <code>workerCountOf(recheck) == 0</code> 如果worker的数量为0，那就 <code>addWorker(null,false)</code>。为什么这里是 <code>null</code> ？之前已经把 command 提交到阻塞队列了 <code>workQueue.offer(command)</code> 。所以提交一个空线程，直接从阻塞队列里面取就可以了。</li><li>第三次，如果线程池没有 RUNNING 或者 offer 阻塞队列失败，<code>addWorker(command,false)</code>，很好理解，对应的就是，阻塞队列满了，将任务提交到，非核心线程池。与最大线程池比较。</li></ul><p>至此，重新归纳<code>execute()</code>的逻辑应该是：</p><ol><li>如果当前运行的线程，少于corePoolSize，则创建一个新的线程来执行任务。</li><li>如果运行的线程等于或多于 corePoolSize，将任务加入 BlockingQueue。</li><li>如果加入 BlockingQueue 成功，需要二次检查线程池的状态如果线程池没有处于 Running，则从 BlockingQueue 移除任务，启动拒绝策略。</li><li>如果线程池处于 Running状态，则检查工作线程（worker）是否为0。如果为0，则创建新的线程来处理任务。如果启动线程数大于maximumPoolSize，任务将被拒绝策略拒绝。</li><li>如果加入 BlockingQueue 。失败,则创建新的线程来处理任务。</li><li>如果启动线程数大于maximumPoolSize，任务将被拒绝策略拒绝。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>回顾我开始提出的问题：</p><blockquote><p>如果 corePoolSize &#x3D; 0 且 阻塞队列是无界的。线程池将如何工作？</p></blockquote><p>这个问题应该就不难回答了。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p><a href="https://union-click.jd.com/jdc?d=igYcKg">《Java并发编程的艺术》</a>是一本学习 java 并发编程的好书，在这里推荐给大家。</p><p>同时，希望大家在阅读技术数据的时候要仔细思考，结合源码，发现，提出问题，解决问题。这样的学习才能高效且透彻。</p><p>欢迎关注我的微信公众号</p><p><img data-src="/images/2019-04-25-022202.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2018/02/09/thread-corepoolsize/</id>
    <link href="https://xilidou.com/2018/02/09/thread-corepoolsize/"/>
    <published>2018-02-09T23:42:27.000Z</published>
    <summary>
      <![CDATA[<p>最近在看<a href="https://union-click.jd.com/jdc?d=igYcKg">《Java并发编程的艺术》</a>回顾线程池的原理和参数的时候发现一个问题，如果 corePoolSize &#x3D; 0 且 阻塞队列是无界的。线程池将如何工作？</p>
<p>我们先回顾一下书里面描述线程池<code>execute()</code>工作的逻辑：</p>
<ol>
<li>如果当前运行的线程，少于corePoolSize，则创建一个新的线程来执行任务。</li>
<li>如果运行的线程等于或多于 corePoolSize，将任务加入 BlockingQueue。</li>
<li>如果 BlockingQueue 内的任务超过上限，则创建新的线程来处理任务。</li>
<li>如果创建的线程数是单钱运行的线程超出 maximumPoolSize，任务将被拒绝策略拒绝。</li>
</ol>
<p>看了这四个步骤，其实描述上是有一个漏洞的。如果核心线程数是0，阻塞队列也是无界的，会怎样？如果按照上文的逻辑，应该没有线程会被运行，然后线程无限的增加到队列里面。然后呢？</p>]]>
    </summary>
    <title>线程池 execute() 的工作逻辑</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="锁" scheme="https://xilidou.com/tags/%E9%94%81/"/>
    <category term="cas" scheme="https://xilidou.com/tags/cas/"/>
    <category term="高并发" scheme="https://xilidou.com/tags/%E9%AB%98%E5%B9%B6%E5%8F%91/"/>
    <content>
      <![CDATA[<p>CAS 是现代操作系统，解决并发问题的一个重要手段，最近在看 <code>eureka</code> 的源码的时候。遇到了很多 CAS 的操作。今天就系统的回顾一下 Java 中的CAS。</p><p>阅读这篇文章你将会了解到：</p><ul><li>什么是 CAS</li><li>CAS 实现原理是什么？</li><li>CAS 在现实中的应用<ul><li>自旋锁</li><li>原子类型</li><li>限流器</li></ul></li><li>CAS 的缺点</li></ul><span id="more"></span><h2 id="什么是-CAS"><a href="#什么是-CAS" class="headerlink" title="什么是 CAS"></a>什么是 CAS</h2><p>CAS: 全称Compare and swap，字面意思:”比较并交换“，一个 CAS 涉及到以下操作：</p><blockquote><p>我们假设内存中的原数据V，旧的预期值A，需要修改的新值B。</p><ol><li>比较 A 与 V 是否相等。（比较）</li><li>如果比较相等，将 B 写入 V。（交换）</li><li>返回操作是否成功。</li></ol></blockquote><p>当多个线程同时对某个资源进行CAS操作，只能有一个线程操作成功，但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见 CAS 其实是一个乐观锁。</p><h2 id="CAS-是怎么实现的"><a href="#CAS-是怎么实现的" class="headerlink" title="CAS 是怎么实现的"></a>CAS 是怎么实现的</h2><p>跟随AtomInteger的代码我们一路往下，就能发现最终调用的是 <code>sum.misc.Unsafe</code> 这个类。看名称 Unsafe 就是一个不安全的类，这个类是利用了 Java 的类和包在可见性的的规则中的一个恰到好处处的漏洞。Unsafe 这个类为了速度，在Java的安全标准上做出了一定的妥协。</p><p>再往下寻找我们发现 Unsafe的<code>compareAndSwapInt</code> 是 Native 的方法：</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="function"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">native</span> <span class="keyword">boolean</span> <span class="title">compareAndSwapInt</span><span class="params">(Object var1, <span class="keyword">long</span> var2, <span class="keyword">int</span> var4, <span class="keyword">int</span> var5)</span></span>;</span><br></pre></td></tr></table></figure><p>也就是说，这几个 CAS 的方法应该是使用了本地的方法。所以这几个方法的具体实现需要我们自己去 jdk 的源码中搜索。</p><p>于是我下载一个 OpenJdk 的源码继续向下探索，我们发现在 <code>/jdk9u/hotspot/src/share/vm/unsafe.cpp</code> 中有这样的代码：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&#123;CC <span class="string">&quot;compareAndSetInt&quot;</span>,   CC <span class="string">&quot;(&quot;</span> OBJ <span class="string">&quot;J&quot;</span><span class="string">&quot;I&quot;</span><span class="string">&quot;I&quot;</span><span class="string">&quot;)Z&quot;</span>,  FN_PTR(Unsafe_CompareAndSetInt)&#125;,</span><br></pre></td></tr></table></figure><p>这个涉及到，JNI 的调用，感兴趣的同学可以自行学习。我们搜索 <code>Unsafe_CompareAndSetInt</code>后发现:</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></pre></td><td class="code"><pre><span class="line">UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSetInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) &#123;</span><br><span class="line">  oop p = JNIHandles::resolve(obj);</span><br><span class="line">  jint* addr = (jint *)index_oop_from_field_offset_long(p, offset);</span><br><span class="line"></span><br><span class="line">  <span class="keyword">return</span> (jint)(Atomic::cmpxchg(x, addr, e)) == e;</span><br><span class="line">&#125; UNSAFE_END</span><br></pre></td></tr></table></figure><p>最终我们终于看到了核心代码 <code>Atomic::cmpxchg</code>。</p><p>继续向底层探索，在文件<code>java/jdk9u/hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.hpp</code>有这样的代码:</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">inline</span> jint     <span class="title">Atomic::cmpxchg</span>    <span class="params">(jint     exchange_value, <span class="keyword">volatile</span> jint*     dest, jint     compare_value, cmpxchg_memory_order order)</span> </span>&#123;</span><br><span class="line">  <span class="keyword">int</span> mp = os::is_MP();</span><br><span class="line">  <span class="function">__asm__ <span class="title">volatile</span> <span class="params">(LOCK_IF_MP(%<span class="number">4</span>) <span class="string">&quot;cmpxchgl %1,(%3)&quot;</span></span></span></span><br><span class="line"><span class="params"><span class="function">                    : <span class="string">&quot;=a&quot;</span> (exchange_value)</span></span></span><br><span class="line"><span class="params"><span class="function">                    : <span class="string">&quot;r&quot;</span> (exchange_value), <span class="string">&quot;a&quot;</span> (compare_value), <span class="string">&quot;r&quot;</span> (dest), <span class="string">&quot;r&quot;</span> (mp)</span></span></span><br><span class="line"><span class="params"><span class="function">                    : <span class="string">&quot;cc&quot;</span>, <span class="string">&quot;memory&quot;</span>)</span></span>;</span><br><span class="line">  <span class="keyword">return</span> exchange_value;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们通过文件名可以知道，针对不同的操作系统,JVM 对于 Atomic::cmpxchg 应该有不同的实现。由于我们服务基本都是使用的是64位linux，所以我们就看看linux_x86 的实现。</p><p>我们继续看代码：</p><ul><li><code>__asm__</code> 的意思是这个是一段内嵌汇编代码。也就是在 C 语言中使用汇编代码。</li><li>这里的 <code>volatile</code>和 JAVA 有一点类似，但不是为了内存的可见性，而是告诉编译器对访问该变量的代码就不再进行优化。</li><li><code>LOCK_IF_MP(%4)</code> 的意思就比较简单，就是如果操作系统是多线程的，那就增加一个 LOCK。</li><li><code>cmpxchgl</code> 就是汇编版的“比较并交换”。但是我们知道比较并交换，有三个步骤，不是原子的。所以在多核情况下加一个 LOCK，由CPU硬件保证他的原子性。</li><li>我们再看看 LOCK 是怎么实现的呢？我们去Intel的官网上看看，可以知道LOCK在的早期实现是直接将 cup 的总线阻塞，这样的实现可见效率是很低下的。后来优化为X86 cpu 有锁定一个特定内存地址的能力，当这个特定内存地址被锁定后，它就可以阻止其他的系统总线读取或修改这个内存地址。</li></ul><p>关于 CAS 的底层探索我们就到此为止。我们总结一下 JAVA 的 cas 是怎么实现的：</p><ul><li>java 的 cas 利用的的是 unsafe 这个类提供的 cas 操作。</li><li>unsafe 的cas 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg</li><li>Atomic::cmpxchg 的实现使用了汇编的 cas 操作，并使用 cpu 硬件提供的 lock信号保证其原子性</li></ul><h2 id="CAS-的应用"><a href="#CAS-的应用" class="headerlink" title="CAS 的应用"></a>CAS 的应用</h2><p>了解了 CAS 的原理我们继续就看看 CAS 的应用：</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><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="class"><span class="keyword">class</span> <span class="title">SpinLock</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">private</span> AtomicReference&lt;Thread&gt; sign =<span class="keyword">new</span> AtomicReference&lt;&gt;();</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">lock</span><span class="params">()</span></span>&#123;</span><br><span class="line">    Thread current = Thread.currentThread();</span><br><span class="line">    <span class="keyword">while</span>(!sign .compareAndSet(<span class="keyword">null</span>, current))&#123;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">unlock</span> <span class="params">()</span></span>&#123;</span><br><span class="line">    Thread current = Thread.currentThread();</span><br><span class="line">    sign .compareAndSet(current, <span class="keyword">null</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>所谓自旋锁，我觉得这个名字相当的形象，在lock()的时候，一直while()循环，直到 cas 操作成功为止。</p><h3 id="AtomicInteger-的-incrementAndGet"><a href="#AtomicInteger-的-incrementAndGet" class="headerlink" title="AtomicInteger 的 incrementAndGet()"></a>AtomicInteger 的 incrementAndGet()</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">int</span> <span class="title">getAndAddInt</span><span class="params">(Object var1, <span class="keyword">long</span> var2, <span class="keyword">int</span> var4)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">int</span> var5;</span><br><span class="line">    <span class="keyword">do</span> &#123;</span><br><span class="line">        var5 = <span class="keyword">this</span>.getIntVolatile(var1, var2);</span><br><span class="line">    &#125; <span class="keyword">while</span>(!<span class="keyword">this</span>.compareAndSwapInt(var1, var2, var5, var5 + var4));</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> var5;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>与自旋锁有异曲同工之妙，就是一直while，直到操作成功为止。</p><h3 id="令牌桶限流器"><a href="#令牌桶限流器" class="headerlink" title="令牌桶限流器"></a>令牌桶限流器</h3><p>所谓令牌桶限流器，就是系统以恒定的速度向桶内增加令牌。每次请求前从令牌桶里面获取令牌。如果获取到令牌就才可以进行访问。当令牌桶内没有令牌的时候，拒绝提供服务。我们来看看 <code>eureka</code> 的限流器是如何使用 CAS 来维护多线程环境下对 token 的增加和分发的。</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><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><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RateLimiter</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">long</span> rateToMsConversion;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> AtomicInteger consumedTokens = <span class="keyword">new</span> AtomicInteger();</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> AtomicLong lastRefillTime = <span class="keyword">new</span> AtomicLong(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Deprecated</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">RateLimiter</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>(TimeUnit.SECONDS);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">RateLimiter</span><span class="params">(TimeUnit averageRateUnit)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">switch</span> (averageRateUnit) &#123;</span><br><span class="line">            <span class="keyword">case</span> SECONDS:</span><br><span class="line">                rateToMsConversion = <span class="number">1000</span>;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">case</span> MINUTES:</span><br><span class="line">                rateToMsConversion = <span class="number">60</span> * <span class="number">1000</span>;</span><br><span class="line">                <span class="keyword">break</span>;</span><br><span class="line">            <span class="keyword">default</span>:</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">&quot;TimeUnit of &quot;</span> + averageRateUnit + <span class="string">&quot; is not supported&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">//提供给外界获取 token 的方法</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">acquire</span><span class="params">(<span class="keyword">int</span> burstSize, <span class="keyword">long</span> averageRate)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> acquire(burstSize, averageRate, System.currentTimeMillis());</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">acquire</span><span class="params">(<span class="keyword">int</span> burstSize, <span class="keyword">long</span> averageRate, <span class="keyword">long</span> currentTimeMillis)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (burstSize &lt;= <span class="number">0</span> || averageRate &lt;= <span class="number">0</span>) &#123; <span class="comment">// Instead of throwing exception, we just let all the traffic go</span></span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//添加token</span></span><br><span class="line">        refillToken(burstSize, averageRate, currentTimeMillis);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//消费token</span></span><br><span class="line">        <span class="keyword">return</span> consumeToken(burstSize);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">refillToken</span><span class="params">(<span class="keyword">int</span> burstSize, <span class="keyword">long</span> averageRate, <span class="keyword">long</span> currentTimeMillis)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">long</span> refillTime = lastRefillTime.get();</span><br><span class="line">        <span class="keyword">long</span> timeDelta = currentTimeMillis - refillTime;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//根据频率计算需要增加多少 token</span></span><br><span class="line">        <span class="keyword">long</span> newTokens = timeDelta * averageRate / rateToMsConversion;</span><br><span class="line">        <span class="keyword">if</span> (newTokens &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">long</span> newRefillTime = refillTime == <span class="number">0</span></span><br><span class="line">                    ? currentTimeMillis</span><br><span class="line">                    : refillTime + newTokens * rateToMsConversion / averageRate;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// CAS 保证有且仅有一个线程进入填充</span></span><br><span class="line">            <span class="keyword">if</span> (lastRefillTime.compareAndSet(refillTime, newRefillTime)) &#123;</span><br><span class="line">                <span class="keyword">while</span> (<span class="keyword">true</span>) &#123;</span><br><span class="line">                    <span class="keyword">int</span> currentLevel = consumedTokens.get();</span><br><span class="line">                    <span class="keyword">int</span> adjustedLevel = Math.min(currentLevel, burstSize); <span class="comment">// In case burstSize decreased</span></span><br><span class="line">                    <span class="keyword">int</span> newLevel = (<span class="keyword">int</span>) Math.max(<span class="number">0</span>, adjustedLevel - newTokens);</span><br><span class="line">                    <span class="comment">// while true 直到更新成功为止</span></span><br><span class="line">                    <span class="keyword">if</span> (consumedTokens.compareAndSet(currentLevel, newLevel)) &#123;</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><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">boolean</span> <span class="title">consumeToken</span><span class="params">(<span class="keyword">int</span> burstSize)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">while</span> (<span class="keyword">true</span>) &#123;</span><br><span class="line">            <span class="keyword">int</span> currentLevel = consumedTokens.get();</span><br><span class="line">            <span class="keyword">if</span> (currentLevel &gt;= burstSize) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// while true 直到没有token 或者 获取到为止</span></span><br><span class="line">            <span class="keyword">if</span> (consumedTokens.compareAndSet(currentLevel, currentLevel + <span class="number">1</span>)) &#123;</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">true</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">reset</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        consumedTokens.set(<span class="number">0</span>);</span><br><span class="line">        lastRefillTime.set(<span class="number">0</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>所以梳理一下 CAS 在令牌桶限流器的作用。就是保证在多线程情况下，不阻塞线程的填充token 和消费token。</p><h3 id="归纳"><a href="#归纳" class="headerlink" title="归纳"></a>归纳</h3><p>通过上面的三个应用我们归纳一下 CAS 的应用场景：</p><ul><li>CAS 的使用能够避免线程的阻塞。</li><li>多数情况下我们使用的是 while true 直到成功为止。</li></ul><h2 id="CAS-缺点"><a href="#CAS-缺点" class="headerlink" title="CAS 缺点"></a>CAS 缺点</h2><ol><li>ABA 的问题，就是一个值从A变成了B又变成了A，使用CAS操作不能发现这个值发生变化了，处理方式是可以使用携带类似时间戳的版本AtomicStampedReference</li><li>性能问题，我们使用时大部分时间使用的是 while true 方式对数据的修改，直到成功为止。优势就是相应极快，但当线程数不停增加时，性能下降明显，因为每个线程都需要执行，占用CPU时间。</li></ol><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>CAS 是整个编程重要的思想之一。整个计算机的实现中都有CAS的身影。微观上看汇编的 CAS 是实现操作系统级别的原子操作的基石。从编程语言角度来看 CAS 是实现多线程非阻塞操作的基石。宏观上看，在分布式系统中，我们可以使用 CAS 的思想利用类似<code>Redis</code>的外部存储，也能实现一个分布式锁。</p><p>从某个角度来说架构就将微观的实现放大，或者底层思想就是将宏观的架构进行微缩。计算机的思想是想通的，所以说了解底层的实现可以提升架构能力，提升架构的能力同样可加深对底层实现的理解。计算机知识浩如烟海，但是套路有限。抓住基础的几个套路突破，从思想和思维的角度学习计算机知识。不要将自己的精力花费在不停的追求新技术的脚步上，跟随‘start guide line’只能写一个demo，所得也就是一个demo而已。</p><p>停下脚步，回顾基础和经典或许对于技术的提升更大一些。</p><p>希望这篇文章对大家有所帮助。</p><p>徒手撸框架系列文章地址：</p><p><a href="https://www.xilidou.com/2018/01/22/merge-request/">徒手撸框架–高并发环境下的请求合并</a><br><a href="https://www.xilidou.com/2018/01/08/spring-ioc/">徒手撸框架–实现IoC</a><br><a href="https://www.xilidou.com/2018/01/13/spring-aop/">徒手撸框架–实现Aop</a></p><p>欢迎关注我的微信公众号<br><img data-src="/images/2019-04-25-022207.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2018/02/01/java-cas/</id>
    <link href="https://xilidou.com/2018/02/01/java-cas/"/>
    <published>2018-02-01T13:56:06.000Z</published>
    <summary>
      <![CDATA[<p>CAS 是现代操作系统，解决并发问题的一个重要手段，最近在看 <code>eureka</code> 的源码的时候。遇到了很多 CAS 的操作。今天就系统的回顾一下 Java 中的CAS。</p>
<p>阅读这篇文章你将会了解到：</p>
<ul>
<li>什么是 CAS</li>
<li>CAS 实现原理是什么？</li>
<li>CAS 在现实中的应用<ul>
<li>自旋锁</li>
<li>原子类型</li>
<li>限流器</li>
</ul>
</li>
<li>CAS 的缺点</li>
</ul>]]>
    </summary>
    <title>JAVA 中的 CAS</title>
    <updated>2026-08-08T02:04:49.444Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="高并发" scheme="https://xilidou.com/tags/%E9%AB%98%E5%B9%B6%E5%8F%91/"/>
    <category term="请求合并" scheme="https://xilidou.com/tags/%E8%AF%B7%E6%B1%82%E5%90%88%E5%B9%B6/"/>
    <content>
      <![CDATA[<p>在高并发系统中，我们经常遇到这样的需求：系统产生大量的请求，但是这些请求实时性要求不高。我们就可以将这些请求合并，达到一定数量我们统一提交。最大化的利用系统性IO,提升系统的吞吐性能。</p><p>所以请求合并框架需要考虑以下两个需求：</p><ol><li>当请求收集到一定数量时提交数据</li><li>一段时间后如果请求没有达到指定的数量也进行提交</li></ol><p>我们就聊聊一如何实现这样一个需求。</p><p>阅读这篇文章你将会了解到:</p><ul><li>ScheduledThreadPoolExecutor</li><li>阻塞队列</li><li>线程安全的参数</li><li>LockSupport的使用</li></ul><span id="more"></span><h1 id="设计思路和实现"><a href="#设计思路和实现" class="headerlink" title="设计思路和实现"></a>设计思路和实现</h1><p>我们就聊一聊实现这个东西的具体思路是什么。希望大家能够学习到分析问题，设计模块的一些套路。</p><h2 id="1-底层使用什么数据结构来持有需要合并的请求？"><a href="#1-底层使用什么数据结构来持有需要合并的请求？" class="headerlink" title="1. 底层使用什么数据结构来持有需要合并的请求？"></a>1. 底层使用什么数据结构来持有需要合并的请求？</h2><ul><li>既然我们的系统是在高并发的环境下使用，那我们肯定不能使用，普通的<code>ArrayList</code>来持有。我们可以使用阻塞队列来持有需要合并的请求。</li><li>我们的数据结构需要提供一个 add() 的方法给外部，用于提交数据。当外部add数据以后，需要检查队列里面的数据的个数是否达到我们限额？达到数量提交数据，不达到继续等待。</li><li>数据结构还需要提供一个timeOut()的方法，外部有一个计时器定时调用这个timeOut方法，如果方法被调用，则直接向远程提交数据。</li><li>条件满足的时候线程执行提交动作，条件不满足的时候线程应当暂停，等待队列达到提交数据的条件。所以我们可以考虑使用 <code>LockSupport.park()</code>和<code>LockSupport.unpark</code> 来暂停和激活操作线程。</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><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><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">FlushThread</span>&lt;<span class="title">Item</span>&gt; <span class="keyword">implements</span> <span class="title">Runnable</span></span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">final</span> String name;</span><br><span class="line"></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="keyword">int</span> bufferSize;</span><br><span class="line">        <span class="comment">//操作间隔</span></span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">int</span> flushInterval;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//上一次提交的时间。</span></span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">volatile</span> <span class="keyword">long</span> lastFlushTime;</span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">volatile</span> Thread writer;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//持有数据的阻塞队列</span></span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">final</span> BlockingQueue&lt;Item&gt; queue;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//达成条件后具体执行的方法</span></span><br><span class="line">        <span class="keyword">private</span> <span class="keyword">final</span> Processor&lt;Item&gt; processor;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//构造函数</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="title">FlushThread</span><span class="params">(String name, <span class="keyword">int</span> bufferSize, <span class="keyword">int</span> flushInterval,<span class="keyword">int</span> queueSize,Processor&lt;Item&gt; processor)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">this</span>.name = name;</span><br><span class="line">            <span class="keyword">this</span>.bufferSize = bufferSize;</span><br><span class="line">            <span class="keyword">this</span>.flushInterval = flushInterval;</span><br><span class="line">            <span class="keyword">this</span>.lastFlushTime = System.currentTimeMillis();</span><br><span class="line">            <span class="keyword">this</span>.processor = processor;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">this</span>.queue = <span class="keyword">new</span> ArrayBlockingQueue&lt;&gt;(queueSize);</span><br><span class="line"></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="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">add</span><span class="params">(Item item)</span></span>&#123;</span><br><span class="line">            <span class="keyword">boolean</span> result = queue.offer(item);</span><br><span class="line">            flushOnDemand();</span><br><span class="line">            <span class="keyword">return</span> result;</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">timeOut</span><span class="params">()</span></span>&#123;</span><br><span class="line">            <span class="comment">//超过两次提交超过时间间隔</span></span><br><span class="line">            <span class="keyword">if</span>(System.currentTimeMillis() - lastFlushTime &gt;= flushInterval)&#123;</span><br><span class="line">                start();</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="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">start</span><span class="params">()</span></span>&#123;</span><br><span class="line">            LockSupport.unpark(writer);</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="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">flushOnDemand</span><span class="params">()</span></span>&#123;</span><br><span class="line">            <span class="keyword">if</span>(queue.size() &gt;= bufferSize)&#123;</span><br><span class="line">                start();</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">flush</span><span class="params">()</span></span>&#123;</span><br><span class="line">            lastFlushTime = System.currentTimeMillis();</span><br><span class="line">            List&lt;Item&gt; temp = <span class="keyword">new</span> ArrayList&lt;&gt;(bufferSize);</span><br><span class="line">            <span class="keyword">int</span> size = queue.drainTo(temp,bufferSize);</span><br><span class="line">            <span class="keyword">if</span>(size &gt; <span class="number">0</span>)&#123;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    processor.process(temp);</span><br><span class="line">                &#125;<span class="keyword">catch</span> (Throwable e)&#123;</span><br><span class="line">                    log.error(<span class="string">&quot;process error&quot;</span>,e);</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="comment">//根据数据的尺寸和时间间隔判断是否提交</span></span><br><span class="line">        <span class="function"><span class="keyword">private</span> <span class="keyword">boolean</span> <span class="title">canFlush</span><span class="params">()</span></span>&#123;</span><br><span class="line">            <span class="keyword">return</span> queue.size() &gt; bufferSize || System.currentTimeMillis() - lastFlushTime &gt; flushInterval;</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            writer = Thread.currentThread();</span><br><span class="line">            writer.setName(name);</span><br><span class="line"></span><br><span class="line">            <span class="keyword">while</span> (!writer.isInterrupted())&#123;</span><br><span class="line">                <span class="keyword">while</span> (!canFlush())&#123;</span><br><span class="line">                    <span class="comment">//如果线程没有被打断，且不达到执行的条件，则阻塞线程</span></span><br><span class="line">                    LockSupport.park(<span class="keyword">this</span>);</span><br><span class="line">                &#125;</span><br><span class="line">                flush();</span><br><span class="line">            &#125;</span><br><span class="line"></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="2-如何实现定时提交呢？"><a href="#2-如何实现定时提交呢？" class="headerlink" title="2. 如何实现定时提交呢？"></a>2. 如何实现定时提交呢？</h2><p>通常我们遇到定时相关的需求，首先想到的应该是使用 <code>ScheduledThreadPoolExecutor</code>定时来调用FlushThread 的 timeOut 方法,如果你想到的是 <code>Thread.sleep()</code>…那需要再努力学习，多看源码了。</p><h2 id="3-怎样进一步的提升系统的吞吐量？"><a href="#3-怎样进一步的提升系统的吞吐量？" class="headerlink" title="3. 怎样进一步的提升系统的吞吐量？"></a>3. 怎样进一步的提升系统的吞吐量？</h2><p>我们使用的<code>FlushThread</code> 实现了 <code>Runnable</code> 所以我们可以考虑使用线程池来持有多个<code>FlushThread</code>。</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><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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Flusher</span>&lt;<span class="title">Item</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> FlushThread&lt;Item&gt;[] flushThreads;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> AtomicInteger index;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//防止多个线程同时执行。增加一个随机数间隔</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Random r = <span class="keyword">new</span> Random();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> delta = <span class="number">50</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> ScheduledExecutorService TIMER = <span class="keyword">new</span> ScheduledThreadPoolExecutor(<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> ExecutorService POOL = Executors.newCachedThreadPool();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">Flusher</span><span class="params">(String name,<span class="keyword">int</span> bufferSiz,<span class="keyword">int</span> flushInterval,<span class="keyword">int</span> queueSize,<span class="keyword">int</span> threads,Processor&lt;Item&gt; processor)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">this</span>.flushThreads = <span class="keyword">new</span> FlushThread[threads];</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span>(threads &gt; <span class="number">1</span>)&#123;</span><br><span class="line">            index = <span class="keyword">new</span> AtomicInteger();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; threads; i++) &#123;</span><br><span class="line">            <span class="keyword">final</span> FlushThread&lt;Item&gt; flushThread = <span class="keyword">new</span> FlushThread&lt;Item&gt;(name+ <span class="string">&quot;-&quot;</span> + i,bufferSiz,flushInterval,queueSize,processor);</span><br><span class="line">            flushThreads[i] = flushThread;</span><br><span class="line">            POOL.submit(flushThread);</span><br><span class="line">            <span class="comment">//定时调用 timeOut()方法。</span></span><br><span class="line">            TIMER.scheduleAtFixedRate(flushThread::timeOut, r.nextInt(delta), flushInterval, TimeUnit.MILLISECONDS);</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">// 对 index 取模，保证多线程都能被add</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">add</span><span class="params">(Item item)</span></span>&#123;</span><br><span class="line">        <span class="keyword">int</span> len = flushThreads.length;</span><br><span class="line">        <span class="keyword">if</span>(len == <span class="number">1</span>)&#123;</span><br><span class="line">            <span class="keyword">return</span> flushThreads[<span class="number">0</span>].add(item);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">int</span> mod = index.incrementAndGet() % len;</span><br><span class="line">        <span class="keyword">return</span> flushThreads[mod].add(item);</span><br><span class="line"></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">private</span> <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">FlushThread</span>&lt;<span class="title">Item</span>&gt; <span class="keyword">implements</span> <span class="title">Runnable</span></span>&#123;</span><br><span class="line">        ...省略</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="4-面向接口编程，提升系统扩展性："><a href="#4-面向接口编程，提升系统扩展性：" class="headerlink" title="4. 面向接口编程，提升系统扩展性："></a>4. 面向接口编程，提升系统扩展性：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">Processor</span>&lt;<span class="title">T</span>&gt; </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">void</span> <span class="title">process</span><span class="params">(List&lt;T&gt; list)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h1 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h1><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></pre></td><td class="code"><pre><span class="line"><span class="comment">//实现 Processor 将 String 全部输出</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">PrintOutProcessor</span> <span class="keyword">implements</span> <span class="title">Processor</span>&lt;<span class="title">String</span>&gt;</span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">process</span><span class="params">(List&lt;String&gt; list)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        System.out.println(<span class="string">&quot;start flush&quot;</span>);</span><br><span class="line"></span><br><span class="line">        list.forEach(System.out::println);</span><br><span class="line"></span><br><span class="line">        System.out.println(<span class="string">&quot;end flush&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><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><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Test</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line"></span><br><span class="line">        Flusher&lt;String&gt; stringFlusher = <span class="keyword">new</span> Flusher&lt;&gt;(<span class="string">&quot;test&quot;</span>,<span class="number">5</span>,<span class="number">1000</span>,<span class="number">30</span>,<span class="number">1</span>,<span class="keyword">new</span> PrintOutProcessor());</span><br><span class="line"></span><br><span class="line">        <span class="keyword">int</span> index = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">while</span> (<span class="keyword">true</span>)&#123;</span><br><span class="line">            stringFlusher.add(String.valueOf(index++));</span><br><span class="line">            Thread.sleep(<span class="number">1000</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></pre></td></tr></table></figure><p>执行的结果：</p><figure class="highlight shell"><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><br><span class="line">start flush</span><br><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">end flush</span><br><span class="line">start flush</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">end flush</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>我们发现并没有达到10个数字就触发了flush。因为出发了超时提交，虽然还没有达到规定的5<br>个数据，但还是执行了 flush。</p><p>如果我们去除 <code>Thread.sleep(1000);</code> 再看看结果：</p><figure class="highlight shell"><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">start flush</span><br><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">end flush</span><br><span class="line">start flush</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">end flush</span><br></pre></td></tr></table></figure><p>每5个数一次提交。完美。。。。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>一个比较生动的例子给大家讲解了一些多线程的具体运用。学习多线程应该多思考多动手，才会有比较好的效果。希望这篇文章大家读完以后有所收获，欢迎交流。</p><p>github地址:<a href="https://github.com/diaozxin007/framework">https://github.com/diaozxin007/framework</a></p><p>徒手撸框架系列文章地址：</p><p><a href="https://www.xilidou.com/2018/01/08/spring-ioc/">徒手撸框架–实现IoC</a><br><a href="https://www.xilidou.com/2018/01/13/spring-aop/">徒手撸框架–实现Aop</a></p><p>欢迎关注我的微信公众号<br><img data-src="/images/2019-04-25-022226.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2018/01/22/merge-request/</id>
    <link href="https://xilidou.com/2018/01/22/merge-request/"/>
    <published>2018-01-22T19:10:45.000Z</published>
    <summary>
      <![CDATA[<p>在高并发系统中，我们经常遇到这样的需求：系统产生大量的请求，但是这些请求实时性要求不高。我们就可以将这些请求合并，达到一定数量我们统一提交。最大化的利用系统性IO,提升系统的吞吐性能。</p>
<p>所以请求合并框架需要考虑以下两个需求：</p>
<ol>
<li>当请求收集到一定数量时提交数据</li>
<li>一段时间后如果请求没有达到指定的数量也进行提交</li>
</ol>
<p>我们就聊聊一如何实现这样一个需求。</p>
<p>阅读这篇文章你将会了解到:</p>
<ul>
<li>ScheduledThreadPoolExecutor</li>
<li>阻塞队列</li>
<li>线程安全的参数</li>
<li>LockSupport的使用</li>
</ul>]]>
    </summary>
    <title>徒手撸框架--高并发环境下的请求合并</title>
    <updated>2026-08-08T02:04:49.445Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="aop" scheme="https://xilidou.com/tags/aop/"/>
    <category term="spring" scheme="https://xilidou.com/tags/spring/"/>
    <content>
      <![CDATA[<p>上一讲我们讲解了Spring 的 IoC 实现。大家可以去我的博客查看<a href="https://www.xilidou.com/2018/01/08/spring-ioc/">点击链接</a>，这一讲我们继续说说 Spring 的另外一个重要特性 AOP。之前在看过的大部分教程，对于Spring Aop的实现讲解的都不太透彻，大部分文章介绍了Spring Aop的底层技术使用了动态代理，至于Spring Aop的具体实现都语焉不详。这类文章看以后以后，我脑子里浮现的就是这样一个画面：</p><p><img data-src="/images/2019-04-25-022211.jpg" alt="画马"></p><p>我的想法就是，带领大家，首先梳理 Spring Aop的实现，然后屏蔽细节，自己实现一个Aop框架。加深对Spring Aop的理解。在了解上图1-4步骤的同时，补充 4 到 5 步骤之间的其他细节。</p><p>读完这篇文章你将会了解：</p><ul><li>Aop是什么？</li><li>为什么要使用Aop？</li><li>Spirng 实现Aop的思路是什么</li><li>自己根据Spring 思想实现一个 Aop框架</li></ul><span id="more"></span><h2 id="Aop-是什么"><a href="#Aop-是什么" class="headerlink" title="Aop 是什么"></a>Aop 是什么</h2><p>面向切面的程序设计（aspect-oriented programming，AOP）。通过预编译方式和运行期动态代理实现程序功能统一维护的一种技术。</p><h2 id="为什么需要使用Aop"><a href="#为什么需要使用Aop" class="headerlink" title="为什么需要使用Aop"></a>为什么需要使用Aop</h2><p>面向切面编程，实际上就是通过预编译或者动态代理技术在不修改源代码的情况下给原来的程序统一添加功能的一种技术。我们看几个关键词，第一个是“动态代理技术”，这个就是Spring Aop实现底层技术。第二个“不修改源代码”，这个就是Aop最关键的地方，也就是我们平时所说的非入侵性。。第三个“添加功能”，不改变原有的源代码，为程序添加功能。</p><p>举个例子：如果某天你需要统计若干方法的执行时间，如果不是用Aop技术，你要做的就是为每一个方法开始的时候获取一个开始时间，在方法结束的时候获取结束时间。二者之差就是方法的执行时间。如果对每一个需要统计的方法都做如上的操作，那代码简直就是灾难。如果我们使用Aop技术，在不修改代码的情况下，添加一个统计方法执行时间的切面。代码就变得十分优雅。具体这个切面怎么实现？看完下面的文章你一定就会知道。</p><h2 id="Spring-Aop-是怎么实现的"><a href="#Spring-Aop-是怎么实现的" class="headerlink" title="Spring Aop 是怎么实现的"></a>Spring Aop 是怎么实现的</h2><p>所谓：</p><blockquote><p>计算机程序 &#x3D; 数据结构 + 算法</p></blockquote><p>在阅读过Spring源码之后，你就会对这个说法理解更深入了。</p><p>Spring Aop实现的代码非常非常的绕。也就是说 Spring 为了灵活做了非常深层次的抽象。同时 Spring为了兼容 <code>@AspectJ</code> 的Aop协议，使用了很多 Adapter （适配器）模式又进一步的增加了代码的复杂程度。<br>Spring 的 Aop 实现主要以下几个步骤：</p><ol><li>初始化 Aop 容器。</li><li>读取配置文件。</li><li>将配置文件装换为 Aop 能够识别的数据结构 – <code>Advisor</code>。这里展开讲一讲这个advisor。Advisor对象中包又含了两个重要的数据结构，一个是 <code>Advice</code>，一个是 <code>Pointcut</code>。<code>Advice</code>的作用就是描述一个切面的行为，<code>pointcut</code>描述的是切面的位置。两个数据结的组合就是”在哪里，干什么“。这样 <code>Advisor</code> 就包含了”在哪里干什么“的信息，就能够全面的描述切面了。</li><li>Spring 将这个 Advisor 转换成自己能够识别的数据结构 – <code>AdvicedSupport</code>。Spirng 动态的将这些方法拦截器织入到对应的方法。</li><li>生成动态代理代理。</li><li>提供调用，在使用的时候，调用方调用的就是代理方法。也就是已经织入了增强方法的方法。</li></ol><h2 id="自己实现一个-Aop-框架"><a href="#自己实现一个-Aop-框架" class="headerlink" title="自己实现一个 Aop 框架"></a>自己实现一个 Aop 框架</h2><p>同样，我也是参考了Aop的设计。只实现了基于方法的拦截器。去除了很多的实现细节。</p><p>使用上一讲的 IoC 框架管理对象。使用 Cglib 作为动态代理的基础类。使用 maven 管理 jar 包和 module。所以上一讲的 IoC 框架会作为一个 modules 引入项目。</p><p>下面我们就来实现我们的Aop 框架吧。</p><p>首先来看看代码的基本结构。</p><p><img data-src="/images/2019-04-25-22212.jpg" alt="代码结构"></p><p>代码结构比上一讲的 IoC 复杂不少。我们首先对包每个包都干了什么做一个简单介绍。</p><ul><li><code>invocation</code> 描述的就是一个方法的调用。注意这里指的是“方法的调用”，而不是调用这个动作。</li><li><code>interceptor</code> 大家最熟悉的拦截器，拦截器拦截的目标就是 <code>invcation</code> 包里面的调用。</li><li><code>advisor</code> 这个包里的对象，都是用来描述切面的数据结构。</li><li><code>adapter</code> 这个包里面是一些适配器方法。对于”适配器”不了解的同学可以去看看”设计模式”里面的”适配模式”。他的作用就是将 <code>advice</code> 包里的对象适配为 <code>interceptor</code>。</li><li><code>bean</code> 描述我们 json 配置文件的对象。</li><li><code>core</code> 我们框架的核心逻辑。</li></ul><p>这个时候宏观的看我们大概梳理出了一条路线， <code>adaper</code> 将 <code>advisor</code> 适配为 <code>interceptor</code> 去拦截 <code>invoction</code>。</p><p>下面我们从这个链条的最末端讲起：</p><h3 id="invocation"><a href="#invocation" class="headerlink" title="invocation"></a><code>invocation</code></h3><p>首先 <code>MethodInvocation</code> 作为所有方法调用的接口。要描述一个方法的调用包含三个方法，获取方法本身<code>getMethod</code>,获取方法的参数<code>getArguments</code>，还有执行方法本身<code>proceed()</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="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">MethodInvocation</span> </span>&#123;</span><br><span class="line">    <span class="function">Method <span class="title">getMethod</span><span class="params">()</span></span>;</span><br><span class="line">    Object[] getArguments();</span><br><span class="line">    <span class="function">Object <span class="title">proceed</span><span class="params">()</span> <span class="keyword">throws</span> Throwable</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>ProxyMethodInvocation</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">ProxyMethodInvocation</span> <span class="keyword">extends</span> <span class="title">MethodInvocation</span> </span>&#123;</span><br><span class="line">    <span class="function">Object <span class="title">getProxy</span><span class="params">()</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="interceptor"><a href="#interceptor" class="headerlink" title="interceptor"></a><code>interceptor</code></h3><p><code>AopMethodInterceptor</code> 是 Aop 容器所有拦截器都要实现的接口：</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> <span class="class"><span class="keyword">interface</span> <span class="title">AopMethodInterceptor</span> </span>&#123;</span><br><span class="line">    <span class="function">Object <span class="title">invoke</span><span class="params">(MethodInvocation mi)</span> <span class="keyword">throws</span> Throwable</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同时我们实现了两种拦截器<code>BeforeMethodAdviceInterceptor</code>和<code>AfterRunningAdviceInterceptor</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">BeforeMethodAdviceInterceptor</span> <span class="keyword">implements</span> <span class="title">AopMethodInterceptor</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> BeforeMethodAdvice advice;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">BeforeMethodAdviceInterceptor</span><span class="params">(BeforeMethodAdvice advice)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.advice = advice;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Object <span class="title">invoke</span><span class="params">(MethodInvocation mi)</span> <span class="keyword">throws</span> Throwable </span>&#123;</span><br><span class="line">        advice.before(mi.getMethod(),mi.getArguments(),mi);</span><br><span class="line">        <span class="keyword">return</span> mi.proceed();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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="class"><span class="keyword">class</span> <span class="title">AfterRunningAdviceInterceptor</span> <span class="keyword">implements</span> <span class="title">AopMethodInterceptor</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> AfterRunningAdvice advice;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">AfterRunningAdviceInterceptor</span><span class="params">(AfterRunningAdvice advice)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.advice = advice;</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="function"><span class="keyword">public</span> Object <span class="title">invoke</span><span class="params">(MethodInvocation mi)</span> <span class="keyword">throws</span> Throwable </span>&#123;</span><br><span class="line">        Object returnVal = mi.proceed();</span><br><span class="line">        advice.after(returnVal,mi.getMethod(),mi.getArguments(),mi);</span><br><span class="line">        <span class="keyword">return</span> returnVal;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>看了上面的代码我们发现，实际上 <code>mi.proceed()</code>才是执行原有的方法。而<code>advice</code>我们上文就说过，是描述增强的方法”干什么“的数据结构，所以对于这个before拦截器，我们就把advice对应的增强方法放在了真正执行的方法前面。而对于after拦截器而言，就放在了真正执行的方法后面。</p><p>这个时候我们过头来看最关键的 <code>ReflectioveMethodeInvocation</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ReflectioveMethodeInvocation</span> <span class="keyword">implements</span> <span class="title">ProxyMethodInvocation</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">ReflectioveMethodeInvocation</span><span class="params">(Object proxy, Object target, Method method, Object[] arguments, List&lt;AopMethodInterceptor&gt; interceptorList)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.proxy = proxy;</span><br><span class="line">        <span class="keyword">this</span>.target = target;</span><br><span class="line">        <span class="keyword">this</span>.method = method;</span><br><span class="line">        <span class="keyword">this</span>.arguments = arguments;</span><br><span class="line">        <span class="keyword">this</span>.interceptorList = interceptorList;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">final</span> Object proxy;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">final</span> Object target;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">final</span> Method method;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> Object[] arguments = <span class="keyword">new</span> Object[<span class="number">0</span>];</span><br><span class="line"></span><br><span class="line">    <span class="comment">//存储所有的拦截器</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">final</span> List&lt;AopMethodInterceptor&gt; interceptorList;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> currentInterceptorIndex = -<span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Object <span class="title">getProxy</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> proxy;</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="function"><span class="keyword">public</span> Method <span class="title">getMethod</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> method;</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[] getArguments() &#123;</span><br><span class="line">        <span class="keyword">return</span> arguments;</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="function"><span class="keyword">public</span> Object <span class="title">proceed</span><span class="params">()</span> <span class="keyword">throws</span> Throwable </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//执行完所有的拦截器后，执行目标方法</span></span><br><span class="line">        <span class="keyword">if</span>(currentInterceptorIndex == <span class="keyword">this</span>.interceptorList.size() - <span class="number">1</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> invokeOriginal();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//迭代的执行拦截器。回顾上面的讲解，我们实现的拦击都会执行 im.proceed() 实际上又会调用这个方法。实现了一个递归的调用，直到执行完所有的拦截器。</span></span><br><span class="line">        AopMethodInterceptor interceptor = interceptorList.get(++currentInterceptorIndex);</span><br><span class="line">        <span class="keyword">return</span> interceptor.invoke(<span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> Object <span class="title">invokeOriginal</span><span class="params">()</span> <span class="keyword">throws</span> Throwable</span>&#123;</span><br><span class="line">        <span class="keyword">return</span> ReflectionUtils.invokeMethodUseReflection(target,method,arguments);</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>在实际的运用中，我们的方法很可能被多个方法的拦截器所增强。所以我们，使用了一个list来保存所有的拦截器。所以我们需要递归的去增加拦截器。当处理完了所有的拦截器之后，才会真正调用调用被增强的方法。我们可以认为，前文所述的动态的织入代码就发生在这里。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CglibMethodInvocation</span> <span class="keyword">extends</span> <span class="title">ReflectioveMethodeInvocation</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> MethodProxy methodProxy;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">CglibMethodInvocation</span><span class="params">(Object proxy, Object target, Method method, Object[] arguments, List&lt;AopMethodInterceptor&gt; interceptorList, MethodProxy methodProxy)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">super</span>(proxy, target, method, arguments, interceptorList);</span><br><span class="line">        <span class="keyword">this</span>.methodProxy = methodProxy;</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="function"><span class="keyword">protected</span> Object <span class="title">invokeOriginal</span><span class="params">()</span> <span class="keyword">throws</span> Throwable </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> methodProxy.invoke(target,arguments);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>CglibMethodInvocation</code> 只是重写了 <code>invokeOriginal</code> 方法。使用代理类来调用被增强的方法。</p><h3 id="advisor"><a href="#advisor" class="headerlink" title="advisor"></a>advisor</h3><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Advisor</span> </span>&#123;</span><br><span class="line">    <span class="comment">//干什么</span></span><br><span class="line">    <span class="keyword">private</span> Advice advice;</span><br><span class="line">    <span class="comment">//在哪里</span></span><br><span class="line">    <span class="keyword">private</span> Pointcut pointcut;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如上文所说，advisor 描述了在哪里，干什么。</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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AdvisedSupport</span> <span class="keyword">extends</span> <span class="title">Advisor</span> </span>&#123;</span><br><span class="line">    <span class="comment">//目标对象</span></span><br><span class="line">    <span class="keyword">private</span> TargetSource targetSource;</span><br><span class="line">    <span class="comment">//拦截器列表</span></span><br><span class="line">    <span class="keyword">private</span> List&lt;AopMethodInterceptor&gt; list = <span class="keyword">new</span> LinkedList&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addAopMethodInterceptor</span><span class="params">(AopMethodInterceptor interceptor)</span></span>&#123;</span><br><span class="line">        list.add(interceptor);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addAopMethodInterceptors</span><span class="params">(List&lt;AopMethodInterceptor&gt; interceptors)</span></span>&#123;</span><br><span class="line">        list.addAll(interceptors);</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>这个<code>AdvisedSupport</code>就是 我们Aop框架能够理解的数据结构，这个时候问题就变成了–对于哪个目标，增加哪些拦截器。</p><h3 id="core"><a href="#core" class="headerlink" title="core"></a><code>core</code></h3><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><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CglibAopProxy</span> <span class="keyword">implements</span> <span class="title">AopProxy</span></span>&#123;</span><br><span class="line">    <span class="keyword">private</span> AdvisedSupport advised;</span><br><span class="line">    <span class="keyword">private</span> Object[] constructorArgs;</span><br><span class="line">    <span class="keyword">private</span> Class&lt;?&gt;[] constructorArgTypes;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">CglibAopProxy</span><span class="params">(AdvisedSupport config)</span></span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.advised = config;</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="function"><span class="keyword">public</span> Object <span class="title">getProxy</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> getProxy(<span class="keyword">null</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Object <span class="title">getProxy</span><span class="params">(ClassLoader classLoader)</span> </span>&#123;</span><br><span class="line">        Class&lt;?&gt; rootClass = advised.getTargetSource().getTagetClass();</span><br><span class="line">        <span class="keyword">if</span>(classLoader == <span class="keyword">null</span>)&#123;</span><br><span class="line">            classLoader = ClassUtils.getDefultClassLoader();</span><br><span class="line">        &#125;</span><br><span class="line">        Enhancer enhancer = <span class="keyword">new</span> Enhancer();</span><br><span class="line">        enhancer.setSuperclass(rootClass.getSuperclass());</span><br><span class="line">        <span class="comment">//增加拦截器的核心方法</span></span><br><span class="line">        Callback callbacks = getCallBack(advised);</span><br><span class="line">        enhancer.setCallback(callbacks);</span><br><span class="line">        enhancer.setClassLoader(classLoader);</span><br><span class="line">        <span class="keyword">if</span>(constructorArgs != <span class="keyword">null</span> &amp;&amp; constructorArgs.length &gt; <span class="number">0</span>)&#123;</span><br><span class="line">            <span class="keyword">return</span> enhancer.create(constructorArgTypes,constructorArgs);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> enhancer.create();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="keyword">private</span> Callback <span class="title">getCallBack</span><span class="params">(AdvisedSupport advised)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">new</span> DynamicAdvisedIcnterceptor(advised.getList(),advised.getTargetSource());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>CglibAopProxy</code>就是我们代理对象生成的核心方法。使用 cglib 生成代理类。我们可以与之前ioc框架的代码。比较发现区别就在于：</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">Callback callbacks = getCallBack(advised);</span><br><span class="line">enhancer.setCallback(callbacks);</span><br></pre></td></tr></table></figure><p>callback与之前不同了，而是写了一个<code>getCallback()</code>的方法，我们就来看看 getCallback 里面的 <code>DynamicAdvisedIcnterceptor</code>到底干了啥。</p><p>篇幅问题，这里不会介绍 cglib 的使用，对于callback的作用，不理解的同学需要自行学习。</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DynamicAdvisedInterceptor</span> <span class="keyword">implements</span> <span class="title">MethodInterceptor</span></span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">final</span> List&lt;AopMethodInterceptor&gt; interceptorList;</span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">final</span> TargetSource targetSource;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">DynamicAdvisedInterceptor</span><span class="params">(List&lt;AopMethodInterceptor&gt; interceptorList, TargetSource targetSource)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.interceptorList = interceptorList;</span><br><span class="line">        <span class="keyword">this</span>.targetSource = targetSource;</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="function"><span class="keyword">public</span> Object <span class="title">intercept</span><span class="params">(Object obj, Method method, Object[] args, MethodProxy proxy)</span> <span class="keyword">throws</span> Throwable </span>&#123;</span><br><span class="line">        MethodInvocation invocation = <span class="keyword">new</span> CglibMethodInvocation(obj,targetSource.getTagetObject(),method, args,interceptorList,proxy);</span><br><span class="line">        <span class="keyword">return</span> invocation.proceed();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里需要注意，<code>DynamicAdvisedInterceptor</code>这个类实现的 MethodInterceptor 是 gclib的接口，并非我们之前的 AopMethodInterceptor。</p><p>我们近距离观察 intercept 这个方法我们看到：</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">MethodInvocation invocation = <span class="keyword">new</span> CglibMethodInvocation(obj,targetSource.getTagetObject(),method, args,interceptorList,proxy);</span><br></pre></td></tr></table></figure><p>通过这行代码，我们的整个逻辑终于连起来了。也就是这个动态的拦截器，把我们通过 <code>CglibMethodInvocation</code> 织入了增强代码的方法，委托给了 cglib 来生成代理对象。</p><p>至此我们的 Aop 的核心功能就实现了。</p><h4 id="AopBeanFactoryImpl"><a href="#AopBeanFactoryImpl" class="headerlink" title="AopBeanFactoryImpl"></a>AopBeanFactoryImpl</h4><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AopBeanFactoryImpl</span> <span class="keyword">extends</span> <span class="title">BeanFactoryImpl</span></span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> ConcurrentHashMap&lt;String,AopBeanDefinition&gt; aopBeanDefinitionMap = <span class="keyword">new</span> ConcurrentHashMap&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> ConcurrentHashMap&lt;String,Object&gt; aopBeanMap = <span class="keyword">new</span> ConcurrentHashMap&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Object <span class="title">getBean</span><span class="params">(String name)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        Object aopBean = aopBeanMap.get(name);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span>(aopBean != <span class="keyword">null</span>)&#123;</span><br><span class="line">            <span class="keyword">return</span> aopBean;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span>(aopBeanDefinitionMap.containsKey(name))&#123;</span><br><span class="line">            AopBeanDefinition aopBeanDefinition = aopBeanDefinitionMap.get(name);</span><br><span class="line">            AdvisedSupport advisedSupport = getAdvisedSupport(aopBeanDefinition);</span><br><span class="line">            aopBean = <span class="keyword">new</span> CglibAopProxy(advisedSupport).getProxy();</span><br><span class="line">            aopBeanMap.put(name,aopBean);</span><br><span class="line">            <span class="keyword">return</span> aopBean;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">super</span>.getBean(name);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">registerBean</span><span class="params">(String name, AopBeanDefinition aopBeanDefinition)</span></span>&#123;</span><br><span class="line">        aopBeanDefinitionMap.put(name,aopBeanDefinition);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> AdvisedSupport <span class="title">getAdvisedSupport</span><span class="params">(AopBeanDefinition aopBeanDefinition)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line"></span><br><span class="line">        AdvisedSupport advisedSupport = <span class="keyword">new</span> AdvisedSupport();</span><br><span class="line">        List&lt;String&gt; interceptorNames = aopBeanDefinition.getInterceptorNames();</span><br><span class="line">        <span class="keyword">if</span>(interceptorNames != <span class="keyword">null</span> &amp;&amp; !interceptorNames.isEmpty())&#123;</span><br><span class="line">            <span class="keyword">for</span> (String interceptorName : interceptorNames) &#123;</span><br><span class="line"></span><br><span class="line">                Advice advice = (Advice) getBean(interceptorName);</span><br><span class="line"></span><br><span class="line">                Advisor advisor = <span class="keyword">new</span> Advisor();</span><br><span class="line">                advisor.setAdvice(advice);</span><br><span class="line"></span><br><span class="line">                <span class="keyword">if</span>(advice <span class="keyword">instanceof</span> BeforeMethodAdvice)&#123;</span><br><span class="line">                    AopMethodInterceptor interceptor = BeforeMethodAdviceAdapter.getInstants().getInterceptor(advisor);</span><br><span class="line">                    advisedSupport.addAopMethodInterceptor(interceptor);</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                <span class="keyword">if</span>(advice <span class="keyword">instanceof</span> AfterRunningAdvice)&#123;</span><br><span class="line">                    AopMethodInterceptor interceptor = AfterRunningAdviceAdapter.getInstants().getInterceptor(advisor);</span><br><span class="line">                    advisedSupport.addAopMethodInterceptor(interceptor);</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        TargetSource targetSource = <span class="keyword">new</span> TargetSource();</span><br><span class="line">        Object object = getBean(aopBeanDefinition.getTarget());</span><br><span class="line">        targetSource.setTagetClass(object.getClass());</span><br><span class="line">        targetSource.setTagetObject(object);</span><br><span class="line">        advisedSupport.setTargetSource(targetSource);</span><br><span class="line">        <span class="keyword">return</span> advisedSupport;</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><code>AopBeanFactoryImpl</code>是我们产生代理对象的工厂类，继承了上一讲我们实现的 IoC 容器的BeanFactoryImpl。重写了 getBean方法，如果是一个切面代理类，我们使用Aop框架生成代理类，如果是普通的对象，我们就用原来的IoC容器进行依赖注入。<br><code>getAdvisedSupport</code>就是获取 Aop 框架认识的数据结构。</p><p>剩下没有讲到的类都比较简单，大家看源码就行。与核心逻辑无关。</p><h3 id="写个方法测试一下"><a href="#写个方法测试一下" class="headerlink" title="写个方法测试一下"></a>写个方法测试一下</h3><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">StartTimeBeforeMethod</span> <span class="keyword">implements</span> <span class="title">BeforeMethodAdvice</span></span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">before</span><span class="params">(Method method, Object[] args, Object target)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">long</span> startTime = System.currentTimeMillis();</span><br><span class="line">        System.out.println(<span class="string">&quot;开始计时&quot;</span>);</span><br><span class="line">        ThreadLocalUtils.set(startTime);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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="class"><span class="keyword">class</span> <span class="title">EndTimeAfterMethod</span> <span class="keyword">implements</span> <span class="title">AfterRunningAdvice</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Object <span class="title">after</span><span class="params">(Object returnVal, Method method, Object[] args, Object target)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">long</span> endTime = System.currentTimeMillis();</span><br><span class="line">        <span class="keyword">long</span> startTime = ThreadLocalUtils.get();</span><br><span class="line">        ThreadLocalUtils.remove();</span><br><span class="line">        System.out.println(<span class="string">&quot;方法耗时：&quot;</span> + (endTime - startTime) + <span class="string">&quot;ms&quot;</span>);</span><br><span class="line">        <span class="keyword">return</span> returnVal;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>方法开始前，记录时间，保存到 ThredLocal里面，方法结束记录时间，打印时间差。完成统计。</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">public</span> <span class="class"><span class="keyword">class</span> <span class="title">TestService</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">testMethod</span><span class="params">()</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;this is a test method&quot;</span>);</span><br><span class="line">        Thread.sleep(<span class="number">1000</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>配置文件：</p><figure class="highlight json"><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><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">&quot;name&quot;</span>:<span class="string">&quot;beforeMethod&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;className&quot;</span>:<span class="string">&quot;com.xilidou.framework.aop.test.StartTimeBeforeMethod&quot;</span></span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">&quot;name&quot;</span>:<span class="string">&quot;afterMethod&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;className&quot;</span>:<span class="string">&quot;com.xilidou.framework.aop.test.EndTimeAfterMethod&quot;</span></span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">&quot;name&quot;</span>:<span class="string">&quot;testService&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;className&quot;</span>:<span class="string">&quot;com.xilidou.framework.aop.test.TestService&quot;</span></span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">&quot;name&quot;</span>:<span class="string">&quot;testServiceProxy&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;className&quot;</span>:<span class="string">&quot;com.xilidou.framework.aop.core.ProxyFactoryBean&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;target&quot;</span>:<span class="string">&quot;testService&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;interceptorNames&quot;</span>:[</span><br><span class="line">      <span class="string">&quot;beforeMethod&quot;</span>,</span><br><span class="line">      <span class="string">&quot;afterMethod&quot;</span></span><br><span class="line">    ]</span><br><span class="line">  &#125;</span><br><span class="line">]</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><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="class"><span class="keyword">class</span> <span class="title">MainTest</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        AopApplictionContext aopApplictionContext = <span class="keyword">new</span> AopApplictionContext(<span class="string">&quot;application.json&quot;</span>);</span><br><span class="line">        aopApplictionContext.init();</span><br><span class="line">        TestService testService = (TestService) aopApplictionContext.getBean(<span class="string">&quot;testServiceProxy&quot;</span>);</span><br><span class="line">        testService.testMethod();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最终我们的执行结果：</p><figure class="highlight shell"><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><br><span class="line">this is a test method</span><br><span class="line">方法耗时：1015ms</span><br><span class="line"></span><br><span class="line">Process finished with exit code 0</span><br></pre></td></tr></table></figure><p>至此 Aop 框架完成。</p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>Spring 的两大核心特性 IoC 与 Aop 两大特性就讲解完了，希望大家通过我写的两篇文章能够深入理解两个特性。</p><p>Spring的源码实在是复杂，阅读起来常常给人极大的挫败感，但是只要能够坚持，并采用一些行之有效的方法。还是能够理解Spring的代码。并且从中汲取营养。</p><p>下一篇文章，我会给大家讲讲阅读开源代码的一些方法和我自己的体会，敬请期待。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>github：<a href="https://github.com/diaozxin007/framework">https://github.com/diaozxin007/framework</a></p><p>欢迎关注我的微信公众号<br><img data-src="/images/2019-04-25-022212.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2018/01/13/spring-aop/</id>
    <link href="https://xilidou.com/2018/01/13/spring-aop/"/>
    <published>2018-01-13T10:12:48.000Z</published>
    <summary>
      <![CDATA[<p>上一讲我们讲解了Spring 的 IoC 实现。大家可以去我的博客查看<a href="https://www.xilidou.com/2018/01/08/spring-ioc/">点击链接</a>，这一讲我们继续说说 Spring 的另外一个重要特性 AOP。之前在看过的大部分教程，对于Spring Aop的实现讲解的都不太透彻，大部分文章介绍了Spring Aop的底层技术使用了动态代理，至于Spring Aop的具体实现都语焉不详。这类文章看以后以后，我脑子里浮现的就是这样一个画面：</p>
<p><img src="/images/2019-04-25-022211.jpg" alt="画马"></p>
<p>我的想法就是，带领大家，首先梳理 Spring Aop的实现，然后屏蔽细节，自己实现一个Aop框架。加深对Spring Aop的理解。在了解上图1-4步骤的同时，补充 4 到 5 步骤之间的其他细节。</p>
<p>读完这篇文章你将会了解：</p>
<ul>
<li>Aop是什么？</li>
<li>为什么要使用Aop？</li>
<li>Spirng 实现Aop的思路是什么</li>
<li>自己根据Spring 思想实现一个 Aop框架</li>
</ul>]]>
    </summary>
    <title>徒手撸框架--实现Aop</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="spring" scheme="https://xilidou.com/tags/spring/"/>
    <category term="ioc" scheme="https://xilidou.com/tags/ioc/"/>
    <content>
      <![CDATA[<p>Spring 作为 J2ee 开发事实上的标准，是每个Java开发人员都需要了解的框架。但是Spring 的 IoC 和 Aop 的特性，对于初级的Java开发人员来说还是比较难于理解的。所以我就想写一系列的文章给大家讲解这些特性。从而能够进一步深入了解 Spring 框架。</p><p>读完这篇文章，你将会了解：</p><ul><li>什么是依赖注入和控制反转</li><li>Ioc有什么用</li><li>Spring的 Ioc 是怎么实现的</li><li>按照Spring的思路开发一个简单的Ioc框架</li></ul><span id="more"></span><h2 id="IoC-是什么"><a href="#IoC-是什么" class="headerlink" title="IoC 是什么"></a>IoC 是什么</h2><p>wiki百科的解释是：</p><blockquote><p>控制反转（Inversion of Control，缩写为IoC），是面向对象编程中的一种设计原则，可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入（Dependency Injection，简称DI）。通过控制反转，对象在被创建的时候，由一个调控系统内所有对象的外界实体，将其所依赖的对象的引用传递给它。也可以说，依赖被注入到对象中。</p></blockquote><h2 id="Ioc-有什么用"><a href="#Ioc-有什么用" class="headerlink" title="Ioc 有什么用"></a>Ioc 有什么用</h2><p>看完上面的解释你一定没有理解什么是 Ioc，因为是第一次看见上面的话也觉得云里雾里。</p><p>不过通过上面的描述我们可以大概的了解到，使用IoC的目的是为了解耦。也就是说IoC 是解耦的一种方法。</p><p>我们知道Java 是一门面向对象的语言，在 Java 中 Everything is Object，我们的程序就是由若干对象组成的。当我们的项目越来越大，合作的开发者越来越多的时候，我们的类就会越来越多，类与类之间的引用就会成指数级的增长。如下图所示：</p><p><img data-src="/images/2019-04-25-022213.jpg" alt="混乱"></p><p>这样的工程简直就是灾难，如果我们引入 Ioc 框架。由框架来维护类的生命周期和类之间的引用。我们的系统就会变成这样：</p><p><img data-src="/images/2019-04-25-022214.jpg" alt="有结构"></p><p>这个时候我们发现，我们类之间的关系都由 IoC 框架负责维护类，同时将类注入到需要的类中。也就是类的使用者只负责使用，而不负责维护。把专业的事情交给专业的框架来完成。大大的减少开发的复杂度。</p><p>用一个类比来理解这个问题。Ioc 框架就是我们生活中的房屋中介，首先中介会收集市场上的房源，分别和各个房源的房东建立联系。当我们需要租房的时候，并不需要我们四处寻找各类租房信息。我们直接找房屋中介，中介就会根据你的需求提供相应的房屋信息。大大提升了租房的效率，减少了你与各类房东之间的沟通次数。</p><h2 id="Spring-的-IoC-是怎么实现的"><a href="#Spring-的-IoC-是怎么实现的" class="headerlink" title="Spring 的 IoC 是怎么实现的"></a>Spring 的 IoC 是怎么实现的</h2><p>了解Spring框架最直接的方法就阅读Spring的源码。但是Spring的代码抽象的层次很高，且处理的细节很高。对于大多数人来说不是太容易理解。我读了Spirng的源码以后以我的理解做一个总结,Spirng IoC 主要是以下几个步骤。</p><pre><code>1. 初始化 IoC 容器。2. 读取配置文件。3. 将配置文件转换为容器识别对的数据结构（这个数据结构在Spring中叫做 BeanDefinition4. 利用数据结构依次实例化相应的对象5. 注入对象之间的依赖关系</code></pre><h2 id="自己实现一个IoC框架"><a href="#自己实现一个IoC框架" class="headerlink" title="自己实现一个IoC框架"></a>自己实现一个IoC框架</h2><p>为了方便，我们参考 Spirng 的 IoC 实现，去除所有与核心原理无关的逻辑。极简的实现 IoC 的框架。 项目使用 json 作为配置文件。使用 maven 管理 jar 包的依赖。</p><p>在这个框架中我们的对象都是单例的，并不支持Spirng的多种作用域。框架的实现使用了cglib 和 Java 的反射。项目中我还使用了 lombok 用来简化代码。</p><p>下面我们就来编写 IoC 框架吧。</p><p>首先我们看看这个框架的基本结构：</p><p><img data-src="/images/2019-04-25-022215.jpg" alt="基本结构"></p><p>从宏观上观察一下这个框架，包含了3个package、在包 bean 中定义了我们框架的数据结构。core 是我们框架的核心逻辑所在。utils 是一些通用工具类。接下来我们就逐一讲解一下：</p><h3 id="1-bean-定义了框架的数据结构"><a href="#1-bean-定义了框架的数据结构" class="headerlink" title="1. bean 定义了框架的数据结构"></a>1. bean 定义了框架的数据结构</h3><p><code>BeanDefinition</code> 是我们项目的核心数据结构。用于描述我们需要 IoC 框架管理的对象。</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="meta">@Data</span></span><br><span class="line"><span class="meta">@ToString</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">BeanDefinition</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String name;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String className;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String interfaceName;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> List&lt;ConstructorArg&gt; constructorArgs;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> List&lt;PropertyArg&gt; propertyArgs;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>包含了对象的 name，class的名称。如果是接口的实现，还有该对象实现的接口。以及构造函数的传参的列表 <code>constructorArgs</code> 和需要注入的参数列表 &#96;propertyArgs。</p><h3 id="2-再看看我们的工具类包里面的对象"><a href="#2-再看看我们的工具类包里面的对象" class="headerlink" title="2. 再看看我们的工具类包里面的对象"></a>2. 再看看我们的工具类包里面的对象</h3><p><code>ClassUtils</code> 负责处理 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><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="class"><span class="keyword">class</span> <span class="title">ClassUtils</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> ClassLoader <span class="title">getDefultClassLoader</span><span class="params">()</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> Thread.currentThread().getContextClassLoader();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> Class <span class="title">loadClass</span><span class="params">(String className)</span></span>&#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> getDefultClassLoader().loadClass(className);</span><br><span class="line">        &#125; <span class="keyword">catch</span> (ClassNotFoundException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们只写了一个方法，就是通过 className 这个参数获取对象的 Class。</p><p><code>BeanUtils</code> 负责处理对象的实例化，这里我们使用了 cglib 这个工具包，代码如下：</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="class"><span class="keyword">class</span> <span class="title">BeanUtils</span> </span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> &lt;T&gt; <span class="function">T <span class="title">instanceByCglib</span><span class="params">(Class&lt;T&gt; clz,Constructor ctr,Object[] args)</span> </span>&#123;</span><br><span class="line">        Enhancer enhancer = <span class="keyword">new</span> Enhancer();</span><br><span class="line">        enhancer.setSuperclass(clz);</span><br><span class="line">        enhancer.setCallback(NoOp.INSTANCE);</span><br><span class="line">        <span class="keyword">if</span>(ctr == <span class="keyword">null</span>)&#123;</span><br><span class="line">            <span class="keyword">return</span> (T) enhancer.create();</span><br><span class="line">        &#125;<span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> (T) enhancer.create(ctr.getParameterTypes(),args);</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>ReflectionUtils</code> 主要通过 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><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ReflectionUtils</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">injectField</span><span class="params">(Field field,Object obj,Object value)</span> <span class="keyword">throws</span> IllegalAccessException </span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(field != <span class="keyword">null</span>) &#123;</span><br><span class="line">            field.setAccessible(<span class="keyword">true</span>);</span><br><span class="line">            field.set(obj, value);</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>injectField(Field field,Object obj,Object value)</code> 这个方法的作用就是，设置 obj 的 field 为 value。</p><p><code>JsonUtils</code> 的作用就是为了解析我们的json配置文件。代码比较长，与我们的 IoC 原理关系不大，感兴趣的同学可以自行从github上下载代码看看。</p><p>有了这几个趁手的工具，我们就可以开始完成 Ioc 框架的核心代码了。</p><h3 id="3-核心逻辑"><a href="#3-核心逻辑" class="headerlink" title="3. 核心逻辑"></a>3. 核心逻辑</h3><p>我的 IoC 框架，目前只支持一种 ByName 的注入。所以我们的 BeanFactory 就只有一个方法：</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> <span class="class"><span class="keyword">interface</span> <span class="title">BeanFactory</span> </span>&#123;</span><br><span class="line">    <span class="function">Object <span class="title">getBean</span><span class="params">(String name)</span> <span class="keyword">throws</span> Exception</span>;</span><br><span class="line">&#125;</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><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">BeanFactoryImpl</span> <span class="keyword">implements</span> <span class="title">BeanFactory</span></span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> ConcurrentHashMap&lt;String,Object&gt; beanMap = <span class="keyword">new</span> ConcurrentHashMap&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> ConcurrentHashMap&lt;String,BeanDefinition&gt; beanDefineMap= <span class="keyword">new</span> ConcurrentHashMap&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Set&lt;String&gt; beanNameSet = Collections.synchronizedSet(<span class="keyword">new</span> HashSet&lt;&gt;());</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Object <span class="title">getBean</span><span class="params">(String name)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="comment">//查找对象是否已经实例化过</span></span><br><span class="line">        Object bean = beanMap.get(name);</span><br><span class="line">        <span class="keyword">if</span>(bean != <span class="keyword">null</span>)&#123;</span><br><span class="line">            <span class="keyword">return</span> bean;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//如果没有实例化，那就需要调用createBean来创建对象</span></span><br><span class="line">        bean =  createBean(beanDefineMap.get(name));</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span>(bean != <span class="keyword">null</span>) &#123;</span><br><span class="line"></span><br><span class="line">            <span class="comment">//对象创建成功以后，注入对象需要的参数</span></span><br><span class="line">            populatebean(bean);</span><br><span class="line"></span><br><span class="line">            <span class="comment">//再把对象存入Map中方便下次使用。</span></span><br><span class="line">            beanMap.put(name,bean;</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">return</span> bean;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">registerBean</span><span class="params">(String name, BeanDefinition bd)</span></span>&#123;</span><br><span class="line">        beanDefineMap.put(name,bd);</span><br><span class="line">        beanNameSet.add(name);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">private</span> Object <span class="title">createBean</span><span class="params">(BeanDefinition beanDefinition)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        String beanName = beanDefinition.getClassName();</span><br><span class="line">        Class clz = ClassUtils.loadClass(beanName);</span><br><span class="line">        <span class="keyword">if</span>(clz == <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> Exception(<span class="string">&quot;can not find bean by beanName&quot;</span>);</span><br><span class="line">        &#125;</span><br><span class="line">        List&lt;ConstructorArg&gt; constructorArgs = beanDefinition.getConstructorArgs();</span><br><span class="line">        <span class="keyword">if</span>(constructorArgs != <span class="keyword">null</span> &amp;&amp; !constructorArgs.isEmpty())&#123;</span><br><span class="line">            List&lt;Object&gt; objects = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">            <span class="keyword">for</span> (ConstructorArg constructorArg : constructorArgs) &#123;</span><br><span class="line">                objects.add(getBean(constructorArg.getRef()));</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> BeanUtils.instanceByCglib(clz,clz.getConstructor(),objects.toArray());</span><br><span class="line">        &#125;<span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">return</span> BeanUtils.instanceByCglib(clz,<span class="keyword">null</span>,<span class="keyword">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="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">populatebean</span><span class="params">(Object bean)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        Field[] fields = bean.getClass().getSuperclass().getDeclaredFields();</span><br><span class="line">        <span class="keyword">if</span> (fields != <span class="keyword">null</span> &amp;&amp; fields.length &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">for</span> (Field field : fields) &#123;</span><br><span class="line">                String beanName = field.getName();</span><br><span class="line">                beanName = StringUtils.uncapitalize(beanName);</span><br><span class="line">                <span class="keyword">if</span> (beanNameSet.contains(field.getName())) &#123;</span><br><span class="line">                    Object fieldBean = getBean(beanName);</span><br><span class="line">                    <span class="keyword">if</span> (fieldBean != <span class="keyword">null</span>) &#123;</span><br><span class="line">                        ReflectionUtils.injectField(field,bean,fieldBean);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</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>首先我们看到在 BeanFactory 的实现中。我们有两 HashMap，beanMap 和 beanDefineMap。 beanDefineMap 存储的是对象的名称和对象对应的数据结构的映射。beanMap 用于保存 beanName和实例化之后的对象。</p><p>容器初始化的时候，会调用 <code>BeanFactoryImpl.registerBean</code> 方法。把 对象的 BeanDefination 数据结构，存储起来。</p><p>当我们调用 getBean() 的方法的时候。会先到 beanMap 里面查找，有没有实例化好的对象。如果没有，就会去beanDefineMap查找这个对象对应的 BeanDefination。再利用DeanDefination去实例化一个对象。</p><p>对象实例化成功以后，我们还需要注入相应的参数，调用 <code>populatebean()</code>这个方法。在 populateBean 这个方法中，会扫描对象里面的Field，如果对象中的 Field 是我们IoC容器管理的对象，那就会调用 我们上文实现的 <code>ReflectionUtils.injectField</code>来注入对象。</p><p>一切准备妥当之后，我们对象就完成了整个 IoC 流程。最后这个对象放入 beanMap 中,方便下一次使用。</p><p>所以我们可以知道 BeanFactory 是管理和生成对象的地方。</p><h3 id="4-容器"><a href="#4-容器" class="headerlink" title="4. 容器"></a>4. 容器</h3><p>我们所谓的容器，就是对BeanFactory的扩展，负责管理 BeanFactory。我们的这个IoC 框架使用 Json 作为配置文件，所以我们容器就命名为 JsonApplicationContext。当然之后你愿意实现 XML 作为配置文件的容器你就可以自己写一个 XmlApplicationContext，如果基于注解的容器就可以叫AnnotationApplcationContext。这些实现留个大家去完成。</p><p>我们看看 ApplicationContext 的代码：</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="class"><span class="keyword">class</span> <span class="title">JsonApplicationContext</span> <span class="keyword">extends</span> <span class="title">BeanFactoryImpl</span></span>&#123;</span><br><span class="line">    <span class="keyword">private</span> String fileName;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">JsonApplicationContext</span><span class="params">(String fileName)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.fileName = fileName;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">init</span><span class="params">()</span></span>&#123;</span><br><span class="line">        loadFile();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">loadFile</span><span class="params">()</span></span>&#123;</span><br><span class="line">        InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName);</span><br><span class="line">        List&lt;BeanDefinition&gt; beanDefinitions = JsonUtils.readValue(is,<span class="keyword">new</span> TypeReference&lt;List&lt;BeanDefinition&gt;&gt;()&#123;&#125;);</span><br><span class="line">        <span class="keyword">if</span>(beanDefinitions != <span class="keyword">null</span> &amp;&amp; !beanDefinitions.isEmpty()) &#123;</span><br><span class="line">            <span class="keyword">for</span> (BeanDefinition beanDefinition : beanDefinitions) &#123;</span><br><span class="line">                registerBean(beanDefinition.getName(), beanDefinition);</span><br><span class="line">            &#125;</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>BeanDefination</code>。然后使用 <code>registerBean</code> 方法。注册这个对象。</p><p>至此，一个简单版的 IoC 框架就完成。</p><h3 id="5-框架的使用"><a href="#5-框架的使用" class="headerlink" title="5. 框架的使用"></a>5. 框架的使用</h3><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><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"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Hand</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">waveHand</span><span class="params">()</span></span>&#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;挥一挥手&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="class"><span class="keyword">class</span> <span class="title">Mouth</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">speak</span><span class="params">()</span></span>&#123;</span><br><span class="line">        System.out.println(<span class="string">&quot;say hello world&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="class"><span class="keyword">class</span> <span class="title">Robot</span> </span>&#123;</span><br><span class="line">    <span class="comment">//需要注入 hand 和 mouth</span></span><br><span class="line">    <span class="keyword">private</span> Hand hand;</span><br><span class="line">    <span class="keyword">private</span> Mouth mouth;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">show</span><span class="params">()</span></span>&#123;</span><br><span class="line">        hand.waveHand();</span><br><span class="line">        mouth.speak();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们需要为我们的 Robot 机器人注入 hand 和 mouth。</p><p>配置文件：</p><figure class="highlight json"><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><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">&quot;name&quot;</span>:<span class="string">&quot;robot&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;className&quot;</span>:<span class="string">&quot;com.xilidou.framework.ioc.entity.Robot&quot;</span></span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">&quot;name&quot;</span>:<span class="string">&quot;hand&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;className&quot;</span>:<span class="string">&quot;com.xilidou.framework.ioc.entity.Hand&quot;</span></span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">&quot;name&quot;</span>:<span class="string">&quot;mouth&quot;</span>,</span><br><span class="line">    <span class="attr">&quot;className&quot;</span>:<span class="string">&quot;com.xilidou.framework.ioc.entity.Mouth&quot;</span></span><br><span class="line">  &#125;</span><br><span class="line">]</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><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="class"><span class="keyword">class</span> <span class="title">Test</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        JsonApplicationContext applicationContext = <span class="keyword">new</span> JsonApplicationContext(<span class="string">&quot;application.json&quot;</span>);</span><br><span class="line">        applicationContext.init();</span><br><span class="line">        Robot aiRobot = (Robot) applicationContext.getBean(<span class="string">&quot;robot&quot;</span>);</span><br><span class="line">        aiRobot.show();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行以后输出：</p><pre><code><figure class="highlight shell"><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><br><span class="line">say hello world</span><br><span class="line"></span><br><span class="line">Process finished with exit code 0</span><br></pre></td></tr></table></figure></code></pre><p>可以看到我们成功的给我的 aiRobot 注入了 hand 和 mouth。</p><p>至此我们 Ioc 框架开发完成。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>这篇文章读完以后相信你一定也实现了一个简单的 IoC 框架。</p><p>虽然说阅读源码是了解框架的最终手段。但是 Spring 框架作为一个生产框架，为了保证通用和稳定，源码必定是高度抽象，且处理大量细节。所以 Spring 的源码阅读起来还是相当困难。希望这篇文章能够帮助理解 Spring Ioc 的实现。</p><p>下一篇文章 应该会是 《徒手撸框架–实现AOP》。</p><h1 id="更新"><a href="#更新" class="headerlink" title="更新"></a>更新</h1><p>感谢 Heeexy 同学为这个不成熟的框架增加了循环依赖的处理。大家可以阅读这篇文章<a href="http://heeexy.com/2018/01/28/IoC/">《极简 Spring 框架 – 浅析循环依赖》</a></p><p>github 地址：<a href="https://github.com/diaozxin007/xilidou-framework">https://github.com/diaozxin007/xilidou-framework</a></p><p>欢迎关注我的微信公众号<br><img data-src="/images/2019-04-25-022216.jpg" alt="二维码"></p>]]>
    </content>
    <id>https://xilidou.com/2018/01/08/spring-ioc/</id>
    <link href="https://xilidou.com/2018/01/08/spring-ioc/"/>
    <published>2018-01-08T19:16:52.000Z</published>
    <summary>
      <![CDATA[<p>Spring 作为 J2ee 开发事实上的标准，是每个Java开发人员都需要了解的框架。但是Spring 的 IoC 和 Aop 的特性，对于初级的Java开发人员来说还是比较难于理解的。所以我就想写一系列的文章给大家讲解这些特性。从而能够进一步深入了解 Spring 框架。</p>
<p>读完这篇文章，你将会了解：</p>
<ul>
<li>什么是依赖注入和控制反转</li>
<li>Ioc有什么用</li>
<li>Spring的 Ioc 是怎么实现的</li>
<li>按照Spring的思路开发一个简单的Ioc框架</li>
</ul>]]>
    </summary>
    <title>徒手撸框架--实现IoC</title>
    <updated>2026-08-08T02:04:49.446Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="随笔" scheme="https://xilidou.com/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="2017" scheme="https://xilidou.com/tags/2017/"/>
    <category term="个人总结" scheme="https://xilidou.com/tags/%E4%B8%AA%E4%BA%BA%E6%80%BB%E7%BB%93/"/>
    <content>
      <![CDATA[<p>2017年结束了</p><p>总的来说2017年是充实的一年，也是在北京从生存逐渐向生活靠拢的一年。</p> <span id="more"></span><h2 id="工作"><a href="#工作" class="headerlink" title="工作"></a>工作</h2><p>在58从T4晋级到了T5，工资也涨了一点。负责的业务也从英才的APP到整个英才的C端，最后负责了新的58速聘业务。从一个项目的单纯的执行者，到某个模块的的架构者，再到现在变成了某个业务的架构者。虽然现在项目的主要关键点还需要与架构师讨论。但是对于如何设计一个完整的业务系统，也是从零到一的突破。</p><p>这一年所在团队的业务也是命途多舛，之前的白领招聘被交接给别的部门。这一路走来十多个版本的迭代就这样交接出去。一度也想到了离职，出去面试了一圈，也拿到了ofo的offer。但是综合考虑，还是留在了58。这边可以从零开始规划架构一个业务。也算是能力的一个考验。</p><p>最终新项目也在12月26日正式上线。如今项目刚刚上线，希望在新的一年业务能有新的气象。</p><h2 id="学习"><a href="#学习" class="headerlink" title="学习"></a>学习</h2><p>17年重新回顾了Java多线程相关的知识，集中的了解分布式系统的知识，入门了解了go语言，用python写了几个小玩具。</p><p>在17年下半年，开始意识到写作的重要性。先在简书上开始写一些技术的相关的文章，每篇文章阅读人数一百出头。虽然不多但是对于自己来说，学习新的技术不再满足自己能看懂，而要自己理解了，再写出来。确实对于学习提出了一更高的要求。下半年一共写了11篇blog。用hexo搭建了自己的静态博客。还为自己的域名进行了备案。</p><p>17年是AI技术爆发的一年。花了一些精力学习了DeepLearning相关的课程，达到了入门级的水平。仅仅只是从理论上了解了如何训练一个神经网络。但是由于下半年新业务起来以后实在太忙，终止了学习，颇为遗憾。在2018年会继续开始AI相关学习。</p><h2 id="生活"><a href="#生活" class="headerlink" title="生活"></a>生活</h2><p>17年完成了一件人生大事就是结婚了。在17年里面完成了求婚，举行婚礼，蜜月旅行几件大事。从此所有的苦两人分担，所有的甜两人分享。谢谢我的爱人。</p><p>还有一件事情，就是养了一只可爱的暹罗猫。取名“皮蛋”。每天多了铲屎，煮猫饭，喂猫的工作。带她接种疫苗三次，接受了绝育。总之猫是一种相当治愈的小动物。她的呼噜声总能帮你赶走一肚子怨气。</p><p>当然，父母总是我坚定的支持者，他们总是那么无私。</p><h2 id="旅游"><a href="#旅游" class="headerlink" title="旅游"></a>旅游</h2><p>2017去了两个国家。澳大利亚和日本。</p><p>澳洲，自然环境优越，海边也是美不胜收，总体来说澳洲人民比较幸运，坐拥相当优渥的自然资源，贫富差距也不是特别大，总体生活在一个比较高的生活水平。</p><p>日本，彬彬有礼，一切事情按部就班，各个流程相当人性化。好吃的东西特别多。作为一个互联网从业人员总感觉日本的互联网并没有像国内融入了百姓日常的生活。</p><h2 id="投资"><a href="#投资" class="headerlink" title="投资"></a>投资</h2><p>17年不再满足把钱放到货币基金的收益了。自己做了一些ETF指数相关的投资。接触了“且慢”平台，跟投了长赢指数。目前年化收益应该可以买一个iPhone吧。</p><p>尝试投资美股港股，但是担心老虎证券的安全性，虽然开户了，但是没有入金，错过了腾讯的大涨和58的翻倍。新的一年应该还会研究一次美股开户。</p><p>至于比特币，也就看看。作为一个比较厌倦风险的人不太会投资比特币吧。</p><h2 id="读书"><a href="#读书" class="headerlink" title="读书"></a>读书</h2><p>虽然17年买了不少书，但是认真读下来的书只有《如何阅读一本书》、《Java 8 in action》、《Netty in action》、《Java 并发编程的艺术》、《重构改善现有设计》、《刻意练习》。</p><h2 id="2018年计划"><a href="#2018年计划" class="headerlink" title="2018年计划"></a>2018年计划</h2><p>买房，希望新的一年在北京站稳脚跟。</p><p>晋级，向T6进发。</p><p>学习，新的一年着重应该聚焦两个相关点吧，一个是自己的老本行，更加深入的研究分布式系统。还有就是重启AI相关的学习。</p><p>博客，每个月应该会有两篇文章。保证一年24篇文章。</p><p>读书，每个月应该完成一本书。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>2017年是生存向生活靠拢的一年。最重要的一点是意识到记录和写作很重要的一年。希望在2018年回望这一篇文章的时候，不会感到遗憾。新的一年加油。</p><h2 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h2><p>一张图片总结一下：</p><p><img data-src="/images/2019-04-25-022204.jpg" alt="图片"></p>]]>
    </content>
    <id>https://xilidou.com/2017/12/26/2017%E6%80%BB%E7%BB%93/</id>
    <link href="https://xilidou.com/2017/12/26/2017%E6%80%BB%E7%BB%93/"/>
    <published>2017-12-26T12:07:37.000Z</published>
    <summary>
      <![CDATA[<p>2017年结束了</p>
<p>总的来说2017年是充实的一年，也是在北京从生存逐渐向生活靠拢的一年。</p>]]>
    </summary>
    <title>2017个人总结</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="随笔" scheme="https://xilidou.com/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="Spring" scheme="https://xilidou.com/tags/Spring/"/>
    <category term="泛型" scheme="https://xilidou.com/tags/%E6%B3%9B%E5%9E%8B/"/>
    <category term="lambda" scheme="https://xilidou.com/tags/lambda/"/>
    <category term="java 8" scheme="https://xilidou.com/tags/java-8/"/>
    <category term="value" scheme="https://xilidou.com/tags/value/"/>
    <content>
      <![CDATA[<p>最近在写项目的时候遇到了几个小问题，记录下来。希望对大家也有所帮助。</p><span id="more"></span><h2 id="如何获取-T-的-class"><a href="#如何获取-T-的-class" class="headerlink" title="如何获取 T 的 class"></a>如何获取 T 的 class</h2><p>在写BaseDao 之类的代码的时候，经常会遇到获取泛型T的class的情况？我们发现并没有<code>T.class</code>这种写法，那怎么办呢？想起之前写的Hibernate 里面有相关的代码。通过反射获取T的class</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="class"><span class="keyword">class</span> <span class="title">AbstractDao</span>&lt;<span class="title">T</span>&gt;</span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Class&lt;T&gt; clz;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">AbstractDao</span><span class="params">()</span></span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.clz = (Class&lt;T&gt;)</span><br><span class="line">                ((ParameterizedType)getClass()</span><br><span class="line">                        .getGenericSuperclass())</span><br><span class="line">                        .getActualTypeArguments()[<span class="number">0</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><h2 id="Spring中的-Value-加载时间"><a href="#Spring中的-Value-加载时间" class="headerlink" title="Spring中的 @Value 加载时间"></a>Spring中的 @Value 加载时间</h2><p>首先 <code>@Value</code> 注解可以方便的获取配置文件<code>*.properties</code>的参数。</p><p>在写代码的时候遇到这样一个问题。为了减少重复代码，我们通常需要写一个抽象类把共有的方法抽象到Abstract 类中。<code>AbstractDao&lt;T&gt;</code> 如果遇到需要向这个class的构造方法注入参数。且这个参数是通过抽象方法获取的。且这个数据是使用 Spring的 @Value 注解获取的。这个描述比较绕，我们直接看代码：</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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AbstractDao</span>&lt;<span class="title">T</span>&gt;</span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> tableId;</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">AbstractDao</span><span class="params">()</span></span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.tableId = setTableId();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">abstract</span> <span class="keyword">int</span> <span class="title">setTableId</span><span class="params">()</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">UserDao</span> <span class="keyword">extends</span> <span class="title">AbstractDao</span>&lt;<span class="title">User</span>&gt;</span>&#123;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@Value(&quot;$&#123;tableid.user&#125;&quot;)</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> userTableId;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">int</span> <span class="title">setTableId</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> buserTableId;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码运行起来以后，我们发现 userTableId，并不能取到相应的值,这个时候<code>@Value</code>失效了。实际上这个问题的根源是因为<code>@Value</code>的加载是发生在对象实例化之后。也就是首先调用对象的构造函数，然后再获取配置文件中的数据。</p><p>解决的方案是使用注解 <code>@PostConstruct</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">AbstractDao</span>&lt;<span class="title">T</span>&gt;</span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> tableId;</span><br><span class="line">    </span><br><span class="line">    <span class="meta">@PostConstruct</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">init</span><span class="params">()</span></span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.tableId = setTableId();</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">abstract</span> <span class="keyword">int</span> <span class="title">setTableId</span><span class="params">()</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="使用-Java-8-的-lambda-和-stream-来-merge-List"><a href="#使用-Java-8-的-lambda-和-stream-来-merge-List" class="headerlink" title="使用 Java 8 的 lambda 和 stream 来 merge List"></a>使用 Java 8 的 lambda 和 stream 来 merge List</h2><p>在使用微服务架构以后。我们经常会遇到 Merge两个List的场景。比如我们从索引里面获取了一个 <code>List&lt;Long&gt;</code> 包含的是对象的ID的 list。由于前端对象展示的元素需要。用这个ID 的list分别从两个服务批量的查询得到 <code>List&lt;A&gt;</code> 和 <code>List&lt;B&gt;</code>，然后将两个List合二为一成为一个<code>List&lt;C&gt;</code>，返回给前端作为列表页展示。</p><p>看代码：<br>首先我们有一个对象 A：</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">public</span> <span class="class"><span class="keyword">class</span> <span class="title">A</span></span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">long</span> id;</span><br><span class="line">    <span class="keyword">private</span> String aStr;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>有另一个对象 B：</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">public</span> <span class="class"><span class="keyword">class</span> <span class="title">B</span></span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">long</span> id;</span><br><span class="line">    <span class="keyword">private</span> String bStr;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>页面需要的对象C：</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="class"><span class="keyword">class</span> <span class="title">C</span></span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">long</span> id;</span><br><span class="line">    <span class="keyword">private</span> String aStr;</span><br><span class="line">    <span class="keyword">private</span> String bStr;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如何有效的把 <code>List&lt;A&gt;</code> 和 <code>List&lt;B&gt;</code> merge 为一个 List<C>呢? </p><p>总体思路，因为两个list从不同服务里面获取。有可能两个服务出于健壮性的考虑会抛弃某些查询不到的对象，所以两个list的长度有可能不一致。所以使用一个Map&lt;Long，B&gt; 作为索引。<br>如果直接写代码会相当繁琐。如果使用 Java 8 的新特性<code>Lamabda</code>和<code>stream api</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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> C <span class="title">getCForAAndB</span><span class="params">(A a,B b)</span></span>&#123;</span><br><span class="line">    C c = <span class="keyword">new</span> C();</span><br><span class="line">    c.setId(a.getId());</span><br><span class="line">    c.setAstr(a.getAStr())</span><br><span class="line">    c.setBstr(b.getBStr())</span><br><span class="line">    <span class="keyword">return</span> c; </span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">private</span> List&lt;C&gt; <span class="title">mergeList</span><span class="params">(List&lt;A&gt; aList,List&lt;B&gt; bList)</span></span>&#123;</span><br><span class="line">    <span class="comment">//映射Map</span></span><br><span class="line">    Map&lt;Long,B&gt; bMap = bList.parallelStream()</span><br><span class="line">        .collect(Collectors.toMap(B::getId,b -&gt; b));</span><br><span class="line">        </span><br><span class="line">    <span class="keyword">return</span> aList.parallelStream()</span><br><span class="line">            .map(a -&gt; getCForAAndB(a,bMap.get(a.getId)))</span><br><span class="line">            .collect(Collectors.toList());</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>这样merge的代码简洁明了。</p><p>如果不明白的同学可以参考我之前的 Java 8 教程。</p>]]>
    </content>
    <id>https://xilidou.com/2017/11/28/%E6%9C%80%E8%BF%91%E9%81%87%E5%88%B0%E7%9A%84%E5%87%A0%E4%B8%AA%E5%B0%8F%E9%A2%98%E9%9B%86%E5%90%88/</id>
    <link href="https://xilidou.com/2017/11/28/%E6%9C%80%E8%BF%91%E9%81%87%E5%88%B0%E7%9A%84%E5%87%A0%E4%B8%AA%E5%B0%8F%E9%A2%98%E9%9B%86%E5%90%88/"/>
    <published>2017-11-28T17:22:30.000Z</published>
    <summary>
      <![CDATA[<p>最近在写项目的时候遇到了几个小问题，记录下来。希望对大家也有所帮助。</p>]]>
    </summary>
    <title>最近遇到的几个问题集合</title>
    <updated>2026-08-08T02:04:49.447Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Redis" scheme="https://xilidou.com/categories/Redis/"/>
    <category term="后端" scheme="https://xilidou.com/tags/%E5%90%8E%E7%AB%AF/"/>
    <category term="redis" scheme="https://xilidou.com/tags/redis/"/>
    <category term="分布式锁" scheme="https://xilidou.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/"/>
    <content>
      <![CDATA[<p>上周花了点时间研究了 Redis 的作者提的 RedLock 的算法来实现一个分布式锁，<a href="https://www.xilidou.com/2017/10/23/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/">文章地址</a>。在官方的文档最下面发现了这样一句话。</p><blockquote><h1 id="Analysis-of-RedLock"><a href="#Analysis-of-RedLock" class="headerlink" title="Analysis of RedLock"></a>Analysis of RedLock</h1><p>Martin Kleppmann <a href="http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html">analyzed Redlock here</a>. I disagree with the analysis and posted my reply to <a href="http://antirez.com/news/101">his analysis here</a>.</p></blockquote><p>突然觉得事情好像没有那么简单，就点进去看了看。仔细读了读文章，发现了一个不得了的世界。于是静下心来研究了 Martin 对 RedLock 的批评，还有 RedLock 作者 antirez 的反击。</p><span id="more"></span><h1 id="Martin-的批评"><a href="#Martin-的批评" class="headerlink" title="Martin 的批评"></a>Martin 的批评</h1><p>Martin上来就问，我们要锁来干啥呢？两个原因：</p><ol><li>提升效率，用锁来保证一个任务没有必要被执行两次。比如（很昂贵的计算）</li><li>保证正确，使用锁来保证任务按照正常的步骤执行，防止两个节点同时操作一份数据，造成文件冲突，数据丢失。</li></ol><p>对于第一种原因，我们对锁是有一定宽容度的，就算发生了两个节点同时工作，对系统的影响也仅仅是多付出了一些计算的成本，没什么额外的影响。这个时候 使用单点的 Redis 就能很好的解决问题，没有必要使用RedLock，维护那么多的Redis实例，提升系统的维护成本。</p><p>对于第二种原因，对正确性严格要求的场景（比如订单，或者消费），就算使用了 RedLock 算法仍然不能保证锁的正确性。</p><p>我们分析一下 RedLock 的有啥缺陷吧：<br><img data-src="/images/unsafe-lock.png" alt="unsafe-lock"></p><p>作者 Martin 给出这张图，首先我们上一讲说过，RedLock中，为了防止死锁，锁是具有过期时间的。这个过期时间被 Martin 抓住了小辫子。</p><ul><li>如果 Client 1 在持有锁的时候，发生了一次很长时间的 FGC 超过了锁的过期时间。锁就被释放了。</li><li>这个时候 Client 2 又获得了一把锁，提交数据。</li><li>这个时候 Client 1 从 FGC 中苏醒过来了，又一次提交数据。</li></ul><p>这还了得，数据就发生了错误。RedLock 只是保证了锁的高可用性，并没有保证锁的正确性。</p><p>这个时候也许你会说，如果 Client 1 在提交任务之前去查询一下锁的持有者是不自己就能解决这个问题？<br>答案是否定的，FGC 会发生在任何时候，如果 FGC 发生在查询之后，一样会有如上讨论的问题。</p><p>那换一个没有 GC 的编程语言？<br>答案还是否定的， FGC 只是造成系统停顿的原因之一，IO或者网络的堵塞或波动都可能造成系统停顿。</p><p>文章读到这里，我都绝望了，还好 Martin给出了一个解决的方案：</p><p><img data-src="/images/fencing-tokens.png" alt="fencing-tokens"></p><p>为锁增加一个 token-fencing。</p><ul><li>获取锁的时候，还需要获取一个递增的token，在上图中 Client 1 还获得了一个 token&#x3D;33的 fencing。</li><li>发生了上文的 FGC 问题后，Client 获取了 token&#x3D;34 的锁。</li><li>在提交数据的时候，需要判断token的大小，如果token 小于 上一次提交的 token 数据就会被拒绝。</li></ul><p>我们其实可以理解这个 token-fencing 就是一个乐观锁，或者一个 CAS。</p><p>Martin 还指出了，RedLock 是一个<strong>严重依赖系统时钟</strong>的分布式系统。</p><p>还是这个过期时间的小辫子。如果某个 Redis Master的系统时间发生了错误，造成了它持有的锁提前过期被释放。</p><ul><li>Client 1 从 A、B、C、D、E五个节点中，获取了 A、B、C三个节点获取到锁，我们认为他持有了锁</li><li>这个时候，由于 B 的系统时间比别的系统走得快，B就会先于其他两个节点优先释放锁。</li><li>Clinet 2 可以从 B、D、E三个节点获取到锁。在整个分布式系统就造成 两个 Client 同时持有锁了。</li></ul><p>这个时候 Martin 又提出了一个相当重要的关于分布式系统的设计要点：</p><p>好的分布式系统应当是异步的，且不能时间作为安全保障的。因为在分布式系统中有会程序暂停，网络延迟，系统时间错误，这些因数都不能影响分布式系统的安全性，只能影响系统的活性（liveness property）。换句话说，就是在极端情况下，<strong>分布式系统顶多在有限的时间内不能给出结果，但是不能给出错误的结果</strong>。</p><p>所以总结一下 Martin 对 RedLock 的批评：</p><ul><li>对于提升效率的场景下，RedLock 太重。</li><li>对于对正确性要求极高的场景下，RedLock 并不能保证正确性。</li></ul><p>这个时候感觉醍醐灌顶，简直写的太好了。</p><p>RedLock 的作者，同时也Redis 的作者对 Martin的文章也做了回应，条理也是相当的清楚。</p><h1 id="antirez-的回应"><a href="#antirez-的回应" class="headerlink" title="antirez 的回应"></a>antirez 的回应</h1><p>antirez 看到了 Martin 的文章以后，就写了一篇文章回应。剧情会不会反转呢？</p><p>antirez 总结了 Martin 对 RedLock的指控：</p><ol><li>分布式的锁具有一个自动释放的功能。锁的互斥性，只在过期时间之内有效，锁过期释放以后就会造成多个Client 持有锁。</li><li>RedLock 整个系统是建立在，一个在实际系统无法保证的系统模型上的。在这个例子中就是系统假设时间是同步且可信的。</li></ol><p>对于第一个问题：<br>antirez 洋洋洒洒的写了很多，仔细看半天，也没有解决我心中的疑问。回顾一下RedLock 获取锁的步骤：</p><ol><li>获取开始时间</li><li>去各个节点获取锁</li><li>再次获取时间。</li><li>计算获取锁的时间，检查获取锁的时间是否小于获取锁的时间。</li><li>持有锁，该干啥干啥去</li></ol><p>如果，程序在1-3步之间发生了阻塞，RedLock可以感知到锁已经过期，没有问题。<br>如果，程序在第 4 步之后发生了阻塞？怎么办？？？<br>答案是，其他<strong>具有自动释放锁的分布式锁都没办解决这个问题</strong>。</p><p>对于第二个指控：<br>antirez 认为，首先在实际的系统中，从两个方面来看：</p><ol><li>系统暂停，网络延迟。</li><li>系统的时间发生阶跃。</li></ol><p>对于第一个问题。上文已经提到了，RedLock做了一些微小的工作，但是没办法完全避免。其他带有自动释放的分布式锁也没有办法。</p><p>第二个问题，Martin认为系统时间的阶跃主要来自两个方面：</p><ol><li>人为修改。</li><li>从NTP服务收到了一个跳跃时时钟更新。</li></ol><p>对于人为修改，能说啥呢？人要搞破坏没办法避免。<br>NTP受到一个阶跃时钟更新，对于这个问题，需要通过运维来保证。需要将阶跃的时间更新到服务器的时候，应当采取小步快跑的方式。多次修改，每次更新时间尽量小。****</p><p>说个题外话，读到这里我突然理解了运维同学的邮件：<br><img data-src="/images/Screenshot%202017-10-29%203.43.22.png" alt="Screenshot 2017-10-29 3.43.22"></p><p>所以严格来说确实， RedLock建立在了 Time 是可信的模型上，理论上 Time 也是发生错误，但是在现实中，良好的运维和工程一些机制是可以最大限度的保证 Time 可信。</p><p>最后， antirez 还打出了一个暴击，既然 Martin 提出的系统使用 fecting token 保证数据的顺序处理。还需要 RedLock，或者别的分布式锁 干啥？？</p><h1 id="回顾"><a href="#回顾" class="headerlink" title="回顾"></a>回顾</h1><p>看完二人的博客来往，感觉就是看武侠戏里面的高手过招，相当得爽快。二人思路清晰，Martin 上来就看到RedLock的死穴，一顿猛打，antirez见招拆招成功化解。<br>至于二人谁对谁错？<br>我觉得，每一个系统设计都有自己的侧重或者局限。工程也不是完美的。在现实中工程中不存在完美的解决方案。我们应当深入了解其中的原理，了解解决方案的优缺点。明白选用方案的局限性。是否可以接受方案的局限带来的后果。<br>架构本来就是一门平衡的艺术。</p><h1 id="最后"><a href="#最后" class="headerlink" title="最后"></a>最后</h1><p>Martin 推荐使用ZooKeeper 实现分布事务锁。Zookeeper 和 Redis的锁有什么区别？ Zookeeper解决了Redis没有解决的问题了么？且听下回分解。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><ol><li><a href="https://redis.io/topics/distlock#distributed-locks-with-redis">Distributed locks with Redis</a></li><li><a href="https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html">How to do distributed locking</a></li><li><a href="http://antirez.com/news/101">Is Redlock safe?</a></li><li><a href="http://zhangtielei.com/posts/blog-redlock-reasoning.html">基于Redis的分布式锁到底安全吗（上）？</a></li></ol>]]>
    </content>
    <id>https://xilidou.com/2017/10/29/Redis-RedLock-%E5%AE%8C%E7%BE%8E%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E4%B9%88%EF%BC%9F/</id>
    <link href="https://xilidou.com/2017/10/29/Redis-RedLock-%E5%AE%8C%E7%BE%8E%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E4%B9%88%EF%BC%9F/"/>
    <published>2017-10-29T16:21:09.000Z</published>
    <summary>
      <![CDATA[<p>上周花了点时间研究了 Redis 的作者提的 RedLock 的算法来实现一个分布式锁，<a href="https://www.xilidou.com/2017/10/23/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/">文章地址</a>。在官方的文档最下面发现了这样一句话。</p>
<blockquote>
<h1 id="Analysis-of-RedLock"><a href="#Analysis-of-RedLock" class="headerlink" title="Analysis of RedLock"></a>Analysis of RedLock</h1><p>Martin Kleppmann <a href="http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html">analyzed Redlock here</a>. I disagree with the analysis and posted my reply to <a href="http://antirez.com/news/101">his analysis here</a>.</p>
</blockquote>
<p>突然觉得事情好像没有那么简单，就点进去看了看。仔细读了读文章，发现了一个不得了的世界。于是静下心来研究了 Martin 对 RedLock 的批评，还有 RedLock 作者 antirez 的反击。</p>]]>
    </summary>
    <title>Redis RedLock 完美的分布式锁么？</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="Java 8" scheme="https://xilidou.com/tags/Java-8/"/>
    <category term="stream" scheme="https://xilidou.com/tags/stream/"/>
    <content>
      <![CDATA[<h2 id="1-简单使用"><a href="#1-简单使用" class="headerlink" title="1.简单使用"></a>1.简单使用</h2><p>书接上回，我们这一讲要讨论 JAVA 8 的新的 API 流。如果我们有这样一个需求，需要挑选出菜谱里面卡路里小于1000，且卡路里排名前三的菜品的名称。</p><span id="more"></span><p>如果使用<code>JAVA 7</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><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> List&lt;String&gt; <span class="title">findDish</span><span class="params">(List&lt;Dish&gt; menu)</span></span>&#123;</span><br><span class="line">    List&lt;Dish&gt; lowCaloricDishes = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span> (Dish dish : lowCaloricDishes) &#123;</span><br><span class="line">        <span class="keyword">if</span>(dish.getCalories() &lt; <span class="number">1000</span> )&#123;</span><br><span class="line">            lowCaloricDishes.add(dish);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    Collections.sort(lowCaloricDishes, <span class="keyword">new</span> Comparator&lt;Dish&gt;() &#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">compare</span><span class="params">(Dish o1, Dish o2)</span> </span>&#123;</span><br><span class="line">            <span class="keyword">return</span> Integer.compare(o1.getCalories(),o2.getCalories());</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line"></span><br><span class="line">    List&lt;Dish&gt; result = lowCaloricDishes.subList(<span class="number">0</span>,<span class="number">3</span>);</span><br><span class="line">    List&lt;String&gt; resultName = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (Dish dish : result) &#123;</span><br><span class="line">        resultName.add(dish.getName());</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> resultName;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过上面的例子我们可以看到我们使用了很多的中间变量，来存储中介结果，lowCaloricDishes、result、resultName。相当繁琐。如果我们使用 <code>JAVA 8</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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> List&lt;String&gt; <span class="title">findDishWithSteam</span><span class="params">(List&lt;Dish&gt; menu)</span></span>&#123;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">    List&lt;String&gt; resultName = menu.stream()</span><br><span class="line">            .filter(dish -&gt; dish.getCalories()&lt;<span class="number">1000</span>)</span><br><span class="line">            .sorted(Comparator.comparing(Dish::getCalories))</span><br><span class="line">            .limit(<span class="number">3</span>)</span><br><span class="line">            .map(Dish::getName)</span><br><span class="line">            .collect(Collectors.toList());</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> resultName;</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>如果我们把 <code>steam()</code> 变换为 <code>parallelStream()</code>，整个操作就变成并行的。代码十分优雅，想到每天我们处理那么多的集合，反正我已经迫不及待的想使用上&#96;JAVA 8了。</p><h2 id="2-流的定义"><a href="#2-流的定义" class="headerlink" title="2. 流的定义"></a>2. 流的定义</h2><p>到底什么是流？书上给的定义是 <em>“从支持数据处理操作的源生成的元素序列”</em>。</p><ul><li>元素序列 和集合一样，我们可以理解为是一堆有序的值。但是集合侧重的是数据，流侧重的是计算。</li><li>源 流会使用一个提供数据的源。比如 <code>menu.stream()</code>中，<code>meun</code>就是这个流的源。</li><li>数据处理操作 流的数据处理功能类似，数据库的操作，同时也支持函数式编程中的操作。</li></ul><p>我们看看接口<code>java.util.stream.Stream</code>都有一些什么方法：</p><p><img data-src="/images/2019-04-25-022225.png" alt="stea"></p><p>我们可以看到，之前我们在上一个例子里面使用的方法，<code>filter()</code>，<code>sorted()</code>，<code>limit()</code>，<code>map()</code> 的返回值也是一个流<code>Stream</code>，也就是说我们可以，把所有的操作串起来。这个是流的一个特点<em>流水线</em></p><p>还有一个特点就是<em>内部迭代</em>，与集合的迭代不同，流的迭代不是显式的迭代。</p><h2 id="3-流的基本操作"><a href="#3-流的基本操作" class="headerlink" title="3. 流的基本操作"></a>3. 流的基本操作</h2><h3 id="1-筛选和切片"><a href="#1-筛选和切片" class="headerlink" title="1. 筛选和切片"></a>1. 筛选和切片</h3><ol><li><p>筛选filter()</p><p> 所谓筛选就是找出符合条件的元素，<code>filter()</code>接受一个返回<code>boolean</code>类型的函数。<br> 比如：</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">filter(dish -&gt; dish.getCalories()&lt;<span class="number">1000</span>)</span><br></pre></td></tr></table></figure></li><li><p>去重distinct()</p><p> 去重的方法我们可以类比<code>SQL</code> 语句中的 distinct</p></li><li><p>截断limit()</p><p> 同样类似 <code>SQL</code>里面的 limit，接受一个 Long 值。返回流中的前n个元素。</p></li><li><p>跳过skip()</p><p> 跳过前n个元素。很好理解</p></li></ol><h3 id="2-映射"><a href="#2-映射" class="headerlink" title="2. 映射"></a>2. 映射</h3><ol><li><p>map()</p><p> map对流中的每一个元素应用函数，可以理解为将元素转化为另一个元素。</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">.map(Dish::getName)</span><br></pre></td></tr></table></figure></li><li><p>flatmap()</p><p> flatmap方法就是把流中的每一个元素都装换为另外一个流，然后合并为一个流。</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><br><span class="line">List&lt;List&lt;String&gt;&gt; lists = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">    lists.add(Arrays.asList(<span class="string">&quot;apple&quot;</span>, <span class="string">&quot;click&quot;</span>));</span><br><span class="line">    lists.add(Arrays.asList(<span class="string">&quot;boss&quot;</span>, <span class="string">&quot;dig&quot;</span>, <span class="string">&quot;qq&quot;</span>, <span class="string">&quot;vivo&quot;</span>));</span><br><span class="line">    lists.add(Arrays.asList(<span class="string">&quot;c#&quot;</span>, <span class="string">&quot;biezhi&quot;</span>));  </span><br></pre></td></tr></table></figure></li></ol><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">lists.stream()</span><br><span class="line">    .flatMap(Collection::stream)</span><br><span class="line">    .filter(str -&gt; str.length() &gt; <span class="number">2</span>)</span><br><span class="line">    .count();</span><br></pre></td></tr></table></figure><h3 id="3-查找匹配"><a href="#3-查找匹配" class="headerlink" title="3. 查找匹配"></a>3. 查找匹配</h3><p>match、anyMatch、allMatch、noneMatch<br>以上方法都能返回一个boolean类型。</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="keyword">boolean</span> hasLowCalories = mune.stream().anyMatch(dish -&gt; dish.getCalories()&lt;<span class="number">1000</span>)</span><br></pre></td></tr></table></figure><h3 id="4-归约"><a href="#4-归约" class="headerlink" title="4. 归约"></a>4. 归约</h3><p><code>reduce(T，BinaryOperator&lt;T&gt;)</code></p><p>reduce 操作是 反复结合每一个元素，直到流被归约成一个值。其中：</p><ul><li><code>T</code> 指的是初始值；</li><li><code>BinaryOperator&lt;T&gt;</code> 两个元素结合起来获得一个元素，举个例子：<br>Lamdba: <code>(a,b)-&gt; a + b</code>。</li></ul><p>所以给定一个 <code>List&lt;Integer&gt;</code> 计算出和所有<code>int</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="keyword">int</span> sum = list.stream().reduce(<span class="number">0</span>,(a,b)-&gt; a + b);</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"><span class="keyword">int</span> sum = list.stream().reduce(<span class="number">0</span>,Integer::sum);</span><br></pre></td></tr></table></figure><h3 id="5-收集数据"><a href="#5-收集数据" class="headerlink" title="5. 收集数据"></a>5. 收集数据</h3><p>一顿操作之后，我们需要把数据收集起来。就需要使用<code>collect()</code>方法。</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">Map&lt;String, Integer&gt; map = meun.stream()</span><br><span class="line">        .collect(Collectors.toMap(Dish::getName, Dish::getCalories));</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://xilidou.com/2017/10/24/JAVA-8%E5%85%A5%E9%97%A8%EF%BC%88%E4%BA%8C%EF%BC%89%E6%B5%81/</id>
    <link href="https://xilidou.com/2017/10/24/JAVA-8%E5%85%A5%E9%97%A8%EF%BC%88%E4%BA%8C%EF%BC%89%E6%B5%81/"/>
    <published>2017-10-24T19:15:04.000Z</published>
    <summary>
      <![CDATA[<h2 id="1-简单使用"><a href="#1-简单使用" class="headerlink" title="1.简单使用"></a>1.简单使用</h2><p>书接上回，我们这一讲要讨论 JAVA 8 的新的 API 流。如果我们有这样一个需求，需要挑选出菜谱里面卡路里小于1000，且卡路里排名前三的菜品的名称。</p>]]>
    </summary>
    <title>JAVA 8入门（二）流</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="Java 8" scheme="https://xilidou.com/tags/Java-8/"/>
    <category term="Lambda" scheme="https://xilidou.com/tags/Lambda/"/>
    <content>
      <![CDATA[<p>机房迁移以后终于可以用上 <code>Java 8</code>了，本教程将会分为三个方面介绍<code>Java 8</code> 的新特性。首先给大家介绍 <code>Java 8</code> 的Lambda 表达式。</p><span id="more"></span><h2 id="1-让代码更灵活"><a href="#1-让代码更灵活" class="headerlink" title="1. 让代码更灵活"></a>1. 让代码更灵活</h2><p>作为程序员，每天除了写代码，最重要的事情就是吃饭了，为了吃饭，我们设计了一个Dish 对象，代码如下：</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">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Dish</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String name;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">boolean</span> vegetarian;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> <span class="keyword">int</span> calories;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> Type type;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">public</span> <span class="class"><span class="keyword">enum</span> <span class="title">Type</span> </span>&#123;MEAT,FISH,OTHER&#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">//省略get set方法</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>作为一个减肥人士，寻求医生建议。医生说，低卡路里饮食，比较健康，为了找出卡路里低于1000的菜品。于是就有了一下代码：</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> List&lt;Dish&gt; <span class="title">filterDish</span><span class="params">(List&lt;Dish&gt; dishes)</span></span>&#123;</span><br><span class="line">    List&lt;Dish&gt; healthDishes = <span class="keyword">new</span> ArraryList&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span>(Dish dish: dishes)&#123;</span><br><span class="line">        <span class="keyword">if</span>(dish.getCalories()&lt;<span class="number">1000</span>)&#123;</span><br><span class="line">            healthDishes.add(dish)</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>后来医生说，不只卡路里要低，而且肉就不要吃了，吃素比较有利于健康，于是含泪写了以下代码：</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></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> List&lt;Dish&gt; <span class="title">filterDish</span><span class="params">(List&lt;Dish&gt; dishes)</span></span>&#123;</span><br><span class="line">    List&lt;Dish&gt; healthDishes = <span class="keyword">new</span> ArraryList&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span>(Dish dish: dishes)&#123;</span><br><span class="line">        <span class="keyword">if</span>(dish.getCalories()&lt;<span class="number">1000</span> &amp;&amp; dish.getVegerarian())&#123;</span><br><span class="line">            healthDishes.add(dish)</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>不能吃肉哪憋得住，于是医生又说你可以吃一点鱼。最为一个有骨气的程序员，已经不想去迎合<del>（产品经理了）</del>医生去修改代码了？有没有什么办法，能快速找出健康食物，万一哪天减肥成功了，又能吃肉了也不用去修改代码？<br>于是我们写这样一段代码：</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="function"><span class="keyword">public</span> <span class="keyword">static</span> List&lt;Dish&gt; <span class="title">filterDish</span><span class="params">(List&lt;Dish&gt; dishes,<span class="keyword">int</span> calorites,<span class="keyword">boolean</span> isMeat,Type type)</span></span>&#123;</span><br><span class="line">    List&lt;Dish&gt; healthDishes = <span class="keyword">new</span> ArraryList&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span>(Dish dish: dishes)&#123;</span><br><span class="line">        <span class="keyword">if</span>(dish.getCalories()&lt;calorites </span><br><span class="line">            &amp;&amp; dish.getVegerarian() == isMeat</span><br><span class="line">            &amp;&amp; dish.getType() == type )&#123;</span><br><span class="line">            healthDishes.add(dish)</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>需求是满足了，但是作为一个有品位的程序员肯定不允许这样代码出现，实在过于繁琐了。万一再加入一个条件怎么办？<br>我们可以考虑将医生的医嘱作为一个方法传入我们的filerDish这个方法，医生说啥就是啥，不必要自己封装一个方法来响应医生的要求？于是我们这么考虑:</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="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">DoctorSaid</span></span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">boolean</span> <span class="title">test</span><span class="params">(Dish dish)</span></span>;</span><br><span class="line">&#125;</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><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="function"><span class="keyword">public</span> <span class="keyword">static</span> List&lt;Dish&gt; <span class="title">filterDish</span><span class="params">(Dish dishes,DoctorSaid doctorSaid)</span></span>&#123;</span><br><span class="line">    List&lt;Dish&gt; lowCaloriesDishes = <span class="keyword">new</span> ArraryList&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span>(Dish dish: dishe)&#123;</span><br><span class="line">        <span class="keyword">if</span>(doctorSaid.test(dish) )&#123;</span><br><span class="line">            healthDishes.add(dish)</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>如果医生说吃 1000 卡路里一下的食物，我们实现一个1000 以下卡路里的食物：</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">boolean</span> DoctorSaidLowCalorites implements DoctorSaid&#123;</span><br><span class="line">    <span class="function"><span class="keyword">boolean</span> <span class="title">test</span><span class="params">(Dish dish)</span></span>&#123;</span><br><span class="line">        <span class="keyword">return</span> dish.getCalorites() &lt; <span class="number">1000</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这样我们只用这样调用filterDish就解决问题了：</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">List&lt;Dish&gt; dishes = filterDish(dishes,<span class="keyword">new</span> DoctorSaidLowCalorites());</span><br></pre></td></tr></table></figure><p>问题来了，对于善变的<del>（产品经理）</del> 医生，总是不能提前准备好所有的接口实现？<br>这个时候我们就可以使用<code>JAVA</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></pre></td><td class="code"><pre><span class="line">List&lt;Dish&gt; dishes = filterDish(dishes,<span class="keyword">new</span> DoctorSaid()&#123;</span><br><span class="line">    <span class="keyword">return</span> dish.getCalorites() &lt; <span class="number">1000</span>;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>稍微好了一点，但是匿名内部类还是有一个不好的地方，就是太啰嗦了其实核心代码就是<code>dish.getCalorites() &lt; 1000</code> 为什么我们要写那么多代码？这个也是java老被诟病的地方,代码十分繁琐。<br>终于我们的Lambda表达式要出场了：</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">List&lt;Dish&gt; dishes = filterDish(dishes, (Dish dish)-&gt; dish.getCalorites() &lt; <span class="number">1000</span>);</span><br></pre></td></tr></table></figure><p>现在看上去好多了，但是作为一个程序员还是很贪心啊，现在只能过滤 <code>Dish</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">Predicate</span>&lt;<span class="title">T</span>&gt;</span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">boolean</span> <span class="title">test</span><span class="params">(T t)</span></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> &lt;T&gt; <span class="function">List&lt;T&gt; <span class="title">filter</span><span class="params">(List&lt;T&gt; list, Predicate&lt;T&gt; p)</span></span>&#123;</span><br><span class="line">    List&lt;T&gt; result = <span class="keyword">new</span> ArrayList&lt;&gt;();</span><br><span class="line">    <span class="keyword">for</span>(T e: list)&#123;</span><br><span class="line">        <span class="keyword">if</span>(p.test(e))&#123;</span><br><span class="line">            result.add(e);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> result;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用泛型让我们的代码更加通用。随便产品经理改需求，从 List 里面按规则过滤符合要求的需求不用多写代码都能搞定了。这时候你是不是觉得胸前的红领巾更加鲜艳了？</p><h2 id="2-实际应用："><a href="#2-实际应用：" class="headerlink" title="2. 实际应用："></a>2. 实际应用：</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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line">jobList.sort((JobInfo job1,JobInfo job2)-&gt;job1.getUpdateTime.compareTo(job2.getUpdateTime);</span><br><span class="line"></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">Thread t = <span class="keyword">new</span> Thread(()-&gt; System.out.println(<span class="string">&quot;Hello world&quot;</span>));</span><br></pre></td></tr></table></figure><h2 id="3-近距离观察Lambda"><a href="#3-近距离观察Lambda" class="headerlink" title="3. 近距离观察Lambda"></a>3. 近距离观察Lambda</h2><h3 id="1-什么是Lambda"><a href="#1-什么是Lambda" class="headerlink" title="1. 什么是Lambda"></a>1. 什么是Lambda</h3><p>所以通过上面例子我们尝试定义一下Lambda 表达式是什么？</p><p>Lambda表达式为简洁的表示可传递的匿名函数的表达式的一种方式。分开来说： </p><ul><li>匿名：没有必要给他取一个函数名称。</li><li>简洁：相对于匿名内部内不需要写很多模板代码</li><li>传递：可以做为参数传递给方法</li><li>函数：不属特定类。但是和方法一样，有参数，函数主题，返回值，有时候还能抛异常。</li></ul><p>标准的Lambda表达式:</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">(Dish dish) -&gt; dish.getCalories() &gt; 1000;</span><br></pre></td></tr></table></figure><p>从上面的标准的表达式：一共三个部分组成<br>    * 参数：<br>    * 箭头：<br>    * Lamdba主体：也就是函数的主体</p><h3 id="2-什么时候使用Lambda"><a href="#2-什么时候使用Lambda" class="headerlink" title="2. 什么时候使用Lambda"></a>2. 什么时候使用Lambda</h3><ul><li>函数式接口：</li></ul><p>所谓函数式接口，我们可以理解为就是只有一个方法的接口。</p><ul><li>函数描述符：</li></ul><p>函数式接口的签名基本上就是Lambda表达式的签名。我们降这种抽象方法叫做函数描述符。举个例子，Ruannable 方法就可以看做一个什么都不接受，什么都不返回的函数。这个时候我们可以发现传入的 Lambda 函数为 <code>()-&gt;void</code>.</p><h3 id="3-Lambda的类型推断"><a href="#3-Lambda的类型推断" class="headerlink" title="3. Lambda的类型推断"></a>3. Lambda的类型推断</h3><p>Java编译器会根据上下文推断使用什么函数式接口来配合Lambda表达式，比如我们之前的例子，以下两种写法都是正确的：</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">(Dish dish) -&gt; dish.getCalories() &gt; <span class="number">1000</span>;</span><br><span class="line"></span><br><span class="line">dish -&gt; dish.getCalories() &gt; <span class="number">1000</span>;</span><br></pre></td></tr></table></figure><p>第二个语句并没有显式的制定<code>dish</code>的类型是<code>Dish</code>，编译器也能正确的编译代码。</p><h3 id="4-方法的引用"><a href="#4-方法的引用" class="headerlink" title="4.方法的引用"></a>4.方法的引用</h3><p>在Lambda中我们可以利用方法的引用来重复使用。可以认为是一个Lamdba带来的语法糖🍬。</p><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">jobList.sort((JobInfo job1,JobInfo job2)-&gt;job1.getUpdateTime.compareTo(job2.getUpdateTime);</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">jobList.sort(comparing(JobInfo::getUpdateTime));</span><br></pre></td></tr></table></figure><p>反正我第一次看到这种写法也不禁感叹，这也行？还有这种操作？</p><p>我们就来看看 JDK 8 中对于方法的引用有以下四种类型：</p><ul><li><p>static 静态方法的引用，这个没啥好说的，语法就是 <code>(ClassName:staticMethod)</code>。</p></li><li><p>任意类型的实例方法的引用，比如 String 方法的 length 方法：</p></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></pre></td><td class="code"><pre><span class="line">(String s1) -&gt; s1.length()</span><br><span class="line"></span><br><span class="line">(String::length)</span><br></pre></td></tr></table></figure><ul><li>现有对象的实例方法的引用：</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></pre></td><td class="code"><pre><span class="line">()-&gt; s.length()</span><br><span class="line"></span><br><span class="line">(s::length)</span><br></pre></td></tr></table></figure><ul><li>构造函数的引用：直接上代码也很好理解<code>(Dish::new)</code>。</li></ul><h4 id="5-现成的函数式接口"><a href="#5-现成的函数式接口" class="headerlink" title="5.现成的函数式接口"></a>5.现成的函数式接口</h4><p>JDK 8 中已经包含了若干现成的函数式接口。在<code>java.util.function</code>中。包括<code>Predicate&lt;T&gt;</code>，<code>Function&lt;T,R&gt;</code>，<code>Consumer&lt;T&gt;</code>。大家可以直接查看源码，在这里就不讲解了。</p><p>第一部分教程结束了，请大家期待《JAVA 8入门（二） 数据流的操作》</p>]]>
    </content>
    <id>https://xilidou.com/2017/10/24/JAVA-8%E5%85%A5%E9%97%A8%EF%BC%88%E4%B8%80%EF%BC%89Lambda%E8%A1%A8%E8%BE%BE%E5%BC%8F/</id>
    <link href="https://xilidou.com/2017/10/24/JAVA-8%E5%85%A5%E9%97%A8%EF%BC%88%E4%B8%80%EF%BC%89Lambda%E8%A1%A8%E8%BE%BE%E5%BC%8F/"/>
    <published>2017-10-24T19:09:08.000Z</published>
    <summary>
      <![CDATA[<p>机房迁移以后终于可以用上 <code>Java 8</code>了，本教程将会分为三个方面介绍<code>Java 8</code> 的新特性。首先给大家介绍 <code>Java 8</code> 的Lambda 表达式。</p>]]>
    </summary>
    <title>JAVA 8入门（一）Lambda表达式</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="工具" scheme="https://xilidou.com/categories/%E5%B7%A5%E5%85%B7/"/>
    <category term="tools" scheme="https://xilidou.com/tags/tools/"/>
    <category term="Alfred" scheme="https://xilidou.com/tags/Alfred/"/>
    <category term="youdao" scheme="https://xilidou.com/tags/youdao/"/>
    <category term="workflow" scheme="https://xilidou.com/tags/workflow/"/>
    <content>
      <![CDATA[<p>最近学习 吴恩达 的DeepLearning 的时候，发现自己的 <code>python</code>水平有点弱。就像想找个练手的东西写一写。想来想去也没有什么想法。今天闲逛知乎的时候发现，有一个用 <code>php</code> 实现的 workflow。可以使用Alfred 调用网易有道的翻译API，查出单词，但是上网一搜使用的 <code>api</code> 网易将会在 2017年12月下线。于是决定自己使用新的<code>api</code>撸一个，提升自己的 <code>python</code> 的水平。</p><p>于是就有了这个小玩具。</p><span id="more"></span><h2 id="项目地址"><a href="#项目地址" class="headerlink" title="项目地址"></a>项目地址</h2><p><a href="https://github.com/diaozxin007/youdao">github.com&#x2F;diaozxin007&#x2F;youdao</a></p>]]>
    </content>
    <id>https://xilidou.com/2017/10/24/%E6%9C%89%E9%81%93-Alfred-Workflow-%E5%A8%81%E5%8A%9B%E5%8A%A0%E5%BC%BA%E7%89%88/</id>
    <link href="https://xilidou.com/2017/10/24/%E6%9C%89%E9%81%93-Alfred-Workflow-%E5%A8%81%E5%8A%9B%E5%8A%A0%E5%BC%BA%E7%89%88/"/>
    <published>2017-10-24T19:06:21.000Z</published>
    <summary>
      <![CDATA[<p>最近学习 吴恩达 的DeepLearning 的时候，发现自己的 <code>python</code>水平有点弱。就像想找个练手的东西写一写。想来想去也没有什么想法。今天闲逛知乎的时候发现，有一个用 <code>php</code> 实现的 workflow。可以使用Alfred 调用网易有道的翻译API，查出单词，但是上网一搜使用的 <code>api</code> 网易将会在 2017年12月下线。于是决定自己使用新的<code>api</code>撸一个，提升自己的 <code>python</code> 的水平。</p>
<p>于是就有了这个小玩具。</p>]]>
    </summary>
    <title>有道 Alfred Workflow 威力加强版</title>
    <updated>2026-08-08T02:04:49.447Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="分布式" scheme="https://xilidou.com/categories/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    <category term="Kafka" scheme="https://xilidou.com/tags/Kafka/"/>
    <category term="分布式" scheme="https://xilidou.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    <content>
      <![CDATA[<p>最近想了解一下分布式消息系统是怎么组成的于是就花了一些时间研究了kafka的实现原理。记录下来方便自己复习和回忆。kafka的设计思想很精妙，可以借鉴到大部分的分布式系统中。</p><span id="more"></span><h2 id="kafka可以解决什么问题？"><a href="#kafka可以解决什么问题？" class="headerlink" title="kafka可以解决什么问题？"></a>kafka可以解决什么问题？</h2><ul><li>kafka可以支持大量数据吞吐。</li><li>可以优雅的处理数据堆积问题。</li><li>低延迟</li><li>支持分布式</li></ul><h2 id="设计理念"><a href="#设计理念" class="headerlink" title="设计理念"></a>设计理念</h2><h3 id="持久化"><a href="#持久化" class="headerlink" title="持久化"></a>持久化</h3><ol><li>尽量线性的读写磁盘。一个硬盘的顺序读写速度一般是4k读写的千倍以上。线性的读写是可以被预测，也能被操作系统大幅的优化的。</li><li>以pagecache为中心的设计风格，使用文件系统并依赖于pagecache要优于维护内存中缓存或其他结构。一方面避免 JVM 中的 gc带来的性能损耗。同时简化了代码实现。</li><li>持久化队列，只需要简单的在文件后面追加写入即可。而不用考虑建立一个索引文件（BTree）。查询和写入的复杂度由 BTree的 O(logN) 减小为线性的 O(1)。大幅提升数据的吞吐量，有利于处理海量数据，且对存储系统的性能要求不高，降低成本。</li><li>考虑将多条消息聚合在一次。减少平均每条消息的开销。</li><li>使用 <a href="https://www.ibm.com/developerworks/linux/library/j-zerocopy/">zero-copy</a>减少字符拷贝时候的开销。</li><li>开启压缩协议，减少数据所占的空间。</li></ol><h3 id="生产者-（Producer）"><a href="#生产者-（Producer）" class="headerlink" title="生产者 （Producer）"></a>生产者 （Producer）</h3><ol><li>Producer 向 Leader Partition 发送消息。</li><li>Producer 可以向任何一个 Partition 询问整个集群的状态，以及谁是 Leader Partition</li><li>Producer 自己决定写入到哪个 Partition。Producer 可以考虑使用何种负载的策略。随机，轮询，按照key分区都可以。</li><li>支持批量操作。消息攒够一定数量再发送，使用适当的延迟换来更高的数据吞吐量。</li></ol><h3 id="消费者-（Consumer）"><a href="#消费者-（Consumer）" class="headerlink" title="消费者 （Consumer）"></a>消费者 （Consumer）</h3><ol><li>消费者直接向 Leader Partition发送一个 fatch 的请求，并制定消费的起始位置（offset），取回offset后的一段数据进行处理。</li><li>Consumer 自己决定 Offset，自己决定从什么地方进行消费。</li><li><strong>Push 和 Pull</strong> 的问题。 消息到底是推还是拉？ kafka 采取的机制是，Producer 向 Broker push 消息。 Consumer 向 Broker Pull 消息。这样做有几个好处。第一，消息消费的速率由 Consumer自己决定。第二，可以聚合的数据批量处理数据，如果使用 push，Broker需要考虑到底要等到多条数据，还是及时发送，Consumer可以尽可能多的拉取数据，保证消息尽可能及时被消费。</li><li>如何记录那些消息被<strong>有效消费</strong>？Topic 被划分为多个有序的分区，保证每个分区任何时候只会被同一个Group里面的 Consumer消费。只需要记录消费的偏移量。同时这个位置可以作为CkeckPonit，定时检查。保证ACK的代价很小。</li><li>如果我们可以使用某个 Consumer 消费数据后，存储到 类似Hadoop的平台上持久化。</li></ol><h3 id="kafka-消息的语义"><a href="#kafka-消息的语义" class="headerlink" title="kafka 消息的语义"></a>kafka 消息的语义</h3><ol><li>消息系统系统一般有以下的语义：<ul><li>At most once：消息可能丢失，但不会重复投递</li><li>At least once：消息不会丢失，但可能会重复投递</li><li>Exactly once：消息不丢失、不重复，会且只会被分发一次（真正想要的）</li></ul></li><li>Producer 发送消息以后，有一个commit的概念，如果commit成功，则意味着消息不会丢失，但是Producer有可能提交成功后，没有收到commit的消息。这有可能造成 at least once 语义。</li><li>从 Consumer 角度来看，我们知道 Offset 是由 Consumer 自己维护。所以何时更新 Offset 就决定了 Consumer 的语义。如果收到消息后更新 Offset，如果 Consumer crash，那新的 Cunsumer再次重启消费，就会造成 At most once 语义（消息会丢，但不重复）。</li><li>如果 Consumser 消费完成后，再更新 Offset。如果 Consumer crash，别的 Consumer 重新用这个 Offser 拉取消息，这个时候就会造成 at least once 的语义（消息不丢，但多次被处理）。</li></ol><p>所以结论：默认Kafka提供at-least-once语义的消息分发，允许用户通过在处理消息之前保存位置信息的方式来提供at-most-once语义。如果我们可以实现消费是<strong>幂等</strong>的，这个时候就可以认为整个系统是Exactly once的了。</p><h3 id="备份"><a href="#备份" class="headerlink" title="备份"></a>备份</h3><ol><li>kafka 对每个 topic 的 partiotion 进行备份，份数由用户自己设置。</li><li>默认情况下 kafka 有一个 Leader 和 0至多个 Follower。</li><li>我们可以认为 Follower 也是一个 Consumer，通过消费 Leader 上的日志然后备份到本地。 </li><li>所有的读写都是在 Leader 上进行的，所以 Follower 真的就只是备份。</li><li>kafka 如何确认一个 Follower 是活的？<ul><li>和 zookeeper 保持联系。</li><li>Follower 复制 Leader 上的消息，且落后的不多（可配置）。</li></ul></li><li>消息同步到所有的 Follower 才认为是提交成功，提交成功才能被消费。所以 Leader 宕机不会造成消息丢失（注意之前的Producer的 at least once 语义）。</li></ol><h3 id="选举"><a href="#选举" class="headerlink" title="选举"></a>选举</h3><ol><li>Leader宕机以后，需要在Follower中选出一个新 Leader。 Kafak动态维护一个同步备份集合（ISR）。这个集合中的 Follower 都能成为 Leader。 一个写入，要同步到所有的 ISR 中才能算做 Commit 成功。同时 ISR 会被持久化到 ZK 中。</li><li>如果全部节点都故障了，kafka会选择第一副本（无需在ISR中） 作为Leader。这个时候会造成丢消息。</li><li>Producer 可以选择是否等待备份响应。所谓的备份相应，是指 ISR 集合中的备份响应。</li></ol>]]>
    </content>
    <id>https://xilidou.com/2017/10/24/Kafka%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E7%AC%94%E8%AE%B0/</id>
    <link href="https://xilidou.com/2017/10/24/Kafka%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E7%AC%94%E8%AE%B0/"/>
    <published>2017-10-24T19:04:15.000Z</published>
    <summary>
      <![CDATA[<p>最近想了解一下分布式消息系统是怎么组成的于是就花了一些时间研究了kafka的实现原理。记录下来方便自己复习和回忆。kafka的设计思想很精妙，可以借鉴到大部分的分布式系统中。</p>]]>
    </summary>
    <title>Kafka实现原理笔记</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="随笔" scheme="https://xilidou.com/categories/%E9%9A%8F%E7%AC%94/"/>
    <category term="后端" scheme="https://xilidou.com/tags/%E5%90%8E%E7%AB%AF/"/>
    <category term="架构" scheme="https://xilidou.com/tags/%E6%9E%B6%E6%9E%84/"/>
    <category term="读书笔记" scheme="https://xilidou.com/tags/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/"/>
    <content>
      <![CDATA[<p>十一假期，本来想找一本股票相关书读一读。机缘巧合就找到了这本武剑锋博士写的<a href="https://book.douban.com/subject/5918414/">《交易系统》</a>。这本书主要讲了上海证券交易系统在技术管理、架构设计、应用调优、切换部署、运行维护等方面的经验和教训。成书的时间大概是在2010年，交易系统的上线时间大概在2008年，聚现在已经接近十年了，但是书中介绍的很多开发时候的原则和思路放在今天来看也有很大的价值可以学习。同时，这也是一本介绍大型系统开发的简要过程的参考书目。</p><p>本书涉及了不少的证券相关知识，涉及证券知识的章节我就略读做了解。对剩下的关于大型系统的设计，管理，架构，优化相关的章节进行了精读，记录相关笔记供自己回顾和思考。</p><span id="more"></span><h1 id="大型系统的管理"><a href="#大型系统的管理" class="headerlink" title="大型系统的管理"></a>大型系统的管理</h1><h2 id="团队管理："><a href="#团队管理：" class="headerlink" title="团队管理："></a>团队管理：</h2><p>将整个系统自上而下的分割为多个松耦合的且相对独立的系统，这样来避免出现整个系统级的问题，遇到问题分而治之。在系统初期设计系统时隐藏细节实现，提前发现系统总体架构上的缺陷，提前修正。各个独立的小系统可用分别的启动，评审，开发测试。<br>配合系统分制的结构，人员也分解为小团队。团队目标一致，团队内的文档，设计，代码均是公开的，团队成员都是知晓的。</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>这一章的第一个小标题就是“为了将来丢弃而现在建设”。大型系统的开发不是一蹴而就的，很多系统的初版都是不太能令人满意的，所以不断的重构是一直贯穿到系统的生命周期之中的。作者所谓的“为了将来丢弃而现在建设”，就是开发过程中准备在将来用更高效的和更易于扩展的代码来替代目前正在编写的模块。<br>重构的过程中，要有精确定义的需求为指导。否则重构结果并不乐观。<br>系统的不断演进和需求不断的增加，会导致系统代码混乱，“熵”不断增加。重构就是为了减少系统的“熵”。<br>系统的不断开发中，会产生“死代码”，需要及时去除。重构中要考虑去除复制张贴的代码，并把这些代码抽象为公共库。</p><p>需求的变更是常态，开发中不应该抗拒和讨厌，对变更进行管理。开发过程中应该为变更做好准备。但是一个系统不能“广泛参数化”。平衡度也很关键。</p><h1 id="系统架构"><a href="#系统架构" class="headerlink" title="系统架构"></a>系统架构</h1><h2 id="系统设计目标"><a href="#系统设计目标" class="headerlink" title="系统设计目标"></a>系统设计目标</h2><p>在设计开始前应当估计系统的极限，留有一定的安全余量，进行开发。</p><p>在大型系统设计中，一直性是重要的考量，有的时候可以牺牲不规则的特性和改进，保证一致性。同时应当保证一致性的文档应该落地。</p><p>高内聚低耦合，这个原则应该是我们初学编程就一直强调的原则，我们应该保证模块之间的正交性质。保证低耦合，我们应该做的就是强制接口定义，隐藏实现细节，不使用公共的数据结构。所谓高内聚是指一个模块只实现一个功能。高内聚的软件易于维护和改进。</p><p>系统应当适当的进行“过度设计”，提升系统的灵活性。</p><p>使用“打包”提升系统的吞吐量。一个是“时间片”，定时进行打包操作一次请求。二是收集足够多的的处理请求后打包处理。在系统优化的过程中，一个是优化算法的实现，二是使用空间换时间的。</p><p>高可用的设计，使用“持久化”和“冗余”提升系统的高可靠性。</p><p>系统的高扩展性，通过以下方式提升：</p><ul><li>按照功能划分模块独立部署</li><li>按照负载划分系统</li><li>避免分布式事务</li><li>在模块间使用异步处理</li></ul><h2 id="系统设计原则"><a href="#系统设计原则" class="headerlink" title="系统设计原则"></a>系统设计原则</h2><p>系统分层设计，系统设计成<strong>金字塔</strong>型，越核心的测系统，设备接入越少，关系越简单，可靠性越高。</p><p>故障隔离设计。</p><p>无单点。</p><p>消息驱动模型的设计：</p><ul><li>前台发消息无ack重发</li><li>后台消息处理需要幂等</li><li>消息在每个界定啊都被存储并加标记</li></ul><p>其实这个设计，很类似Kafka的实现。 </p><h2 id="调优原则"><a href="#调优原则" class="headerlink" title="调优原则"></a>调优原则</h2><p>先将单机的能力提升，再水平扩展</p><p>对大量访问的数据，使用内存方式提高速度（现在看来就是使用缓存），结合磁盘“持久化”。</p><p>优化核心算法。</p><p>使用异步I&#x2F;O。</p><p>传输压缩，打包和流控技术。</p><h2 id="后记"><a href="#后记" class="headerlink" title="后记"></a>后记</h2><p>这本书，提出的一些系统设计和开发原则，现在看来大部分已经在我接触过的的系统中大量的使用。可见系统的基本设计原则，经过快十年的发展也没有太大的变化。十年前的一些原则已经有很多成熟的技术实现了。大型系统的开发逐渐变得容易，系统的设计人员可以将更多的精力来组合各个成熟实现，快速搭建系统。推荐这本书给大家，感受大型系统构建的各个方面细节，开阔视野。</p>]]>
    </content>
    <id>https://xilidou.com/2017/10/24/%E3%80%8A%E4%BA%A4%E6%98%93%E7%B3%BB%E7%BB%9F%EF%BC%9A%E6%9B%B4%E6%96%B0%E4%B8%8E%E8%B7%A8%E8%B6%8A%E3%80%8B%E8%AF%BB%E5%90%8E%E7%AC%94%E8%AE%B0/</id>
    <link href="https://xilidou.com/2017/10/24/%E3%80%8A%E4%BA%A4%E6%98%93%E7%B3%BB%E7%BB%9F%EF%BC%9A%E6%9B%B4%E6%96%B0%E4%B8%8E%E8%B7%A8%E8%B6%8A%E3%80%8B%E8%AF%BB%E5%90%8E%E7%AC%94%E8%AE%B0/"/>
    <published>2017-10-24T18:51:03.000Z</published>
    <summary>
      <![CDATA[<p>十一假期，本来想找一本股票相关书读一读。机缘巧合就找到了这本武剑锋博士写的<a href="https://book.douban.com/subject/5918414/">《交易系统》</a>。这本书主要讲了上海证券交易系统在技术管理、架构设计、应用调优、切换部署、运行维护等方面的经验和教训。成书的时间大概是在2010年，交易系统的上线时间大概在2008年，聚现在已经接近十年了，但是书中介绍的很多开发时候的原则和思路放在今天来看也有很大的价值可以学习。同时，这也是一本介绍大型系统开发的简要过程的参考书目。</p>
<p>本书涉及了不少的证券相关知识，涉及证券知识的章节我就略读做了解。对剩下的关于大型系统的设计，管理，架构，优化相关的章节进行了精读，记录相关笔记供自己回顾和思考。</p>]]>
    </summary>
    <title>《交易系统：更新与跨越》读后笔记</title>
    <updated>2026-08-08T02:04:49.447Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="Netty" scheme="https://xilidou.com/tags/Netty/"/>
    <category term="Apns" scheme="https://xilidou.com/tags/Apns/"/>
    <content>
      <![CDATA[<p>极光推送免费版每分钟600次的请求限制实在是把我恶心坏了，考虑到现在我们 Android 的推送已经全量接入了小米，所以接下来就是要把 iOS 的推送直接接入 APNS 这样就可以彻底摆脱极光的推送。不再受这个600次&#x2F;分钟的限制了。APNS使用 HTTP2 协议进行通信所以自然就想到了使用Netty作为网络框架，进行开发。下面逐个给大家介绍使用 Netty 接入 APNS 的注意事项和接入的时候踩到的坑。</p> <span id="more"></span><h2 id="APNS"><a href="#APNS" class="headerlink" title="APNS"></a>APNS</h2><p>APNS 是 Apple 提供的推送服务。<a href="https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html#//apple_ref/doc/uid/TP40008194-CH8-SW1">官方文档</a><br>下面说一说自己对接入APNS的时候遇到的坑：</p><ol><li>APNS 使用 HTTP&#x2F;2 协议通信，必须使用 TLS 1.2 或以上的加密方式通信，如果使用JDK提供的加密方法，如果使用 JDK7 ，需要对 JVM 的启动参数进行设置，另一个比较简单的解决方法，就是使用 JDK 8 。JDK 8 就能很好的解决加密的问题，或者调用系统的Openssl 进行加解密。具体看大家的线上环境决定。</li><li>在开发的时候还遇到一个比较奇葩的问题，就是使用开发证书可以推送到达，但是切换到线上后发现怎么推送都不能到达，后来仔细读了官方文档，发现在请求头中有一个 <code>apns-topic</code> 字段，如果你的推送证书中包括了不止一个应用，这个字段就是一个必填字段，且为应用的 <code>bundle ID</code>。</li><li>由于使用了 HTTP&#x2F;2 协议，APPLE 推荐尽量复用链接，因为使用了 TLS 加密，每次建立连接的握手会消耗大量的时间。同时还可以使用多个连接提升推送的效率，所以之后的实现中，我使用 <code>Common pool2</code> 作为连接池来提高推送的效率。可以使用 PING 来检查连接是否有效。所以在实现的时候使用了 Netty 的 <code>IdleStateEvent</code> 来检查连接。</li></ol><h2 id="代码的具体实现："><a href="#代码的具体实现：" class="headerlink" title="代码的具体实现："></a>代码的具体实现：</h2><p>首先看看代码的结构：<br>![屏幕快照 2017-05-14 下午11.10.10](<a href="http://7u2r32.com1.z0.glb.clouddn.com/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7">http://7u2r32.com1.z0.glb.clouddn.com/屏幕快照</a> 2017-05-14 下午11.10.10.png)</p><p>Module 中的 Payload 和 PsuhsNotifcation 的是对 APNS 的数据结构的封装，具体可以参考 Apple 提供的文档<br>ApnsConfig 是对整个推送系统的相关设置，提供了一些默认参数。可以根据需要自己进行设置。<br>PingMessage 从名字就能看出，是对链接进行检测的 <code>PING frame</code>。</p><p>下面着重介绍一下Service相关的实现思路。</p><h2 id="具体实现"><a href="#具体实现" class="headerlink" title="具体实现"></a>具体实现</h2><p> Netty 的 Client 的代码我参考了 Netty官方提供的 <a href="https://netty.io/4.1/xref/io/netty/example/http2/helloworld/client/package-summary.html">Example</a>。</p><ol><li><p><code>ApnsConnection</code> 实现了 <code>Connection</code> 接口，主要负责维护和 APNS 的链接。<br> ![屏幕快照 2017-05-14 下午11.20.37](<a href="http://7u2r32.com1.z0.glb.clouddn.com/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7">http://7u2r32.com1.z0.glb.clouddn.com/屏幕快照</a> 2017-05-14 下午11.20.37.png)</p></li><li><p>接池的实现：</p></li></ol><p>链接池相关的代码主要在 <code>ApnsConnectionPool</code> 中。整个链接池的使用了 <code>Commone Pool2</code>作为底层实现，实现的时候主要参考了<code>jedies</code>的链接池的实现。</p><p>3.<code>NettyApnsService</code> 向外暴露了推送的接口。直接调用<code>sendNotification()</code>方法就能推送消息了。</p><p>![屏幕快照 2017-05-14 下午11.26.31](<a href="http://7u2r32.com1.z0.glb.clouddn.com/%E5%B1%8F%E5%B9%95%E5%BF%AB%E7%85%A7">http://7u2r32.com1.z0.glb.clouddn.com/屏幕快照</a> 2017-05-14 下午11.26.31.png)</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>代码地址：<a href="https://github.com/diaozxin007/Netty-apns">https://github.com/diaozxin007/Netty-apns</a></p><p>欢迎大家拍砖指点。</p><p>看开源的代码是快速学习的方法，在整个项目中，参考了很多开源的工程的具体实现。希望对大家有所帮助。</p>]]>
    </content>
    <id>https://xilidou.com/2017/10/24/Netty-Apns%E6%8E%A5%E5%85%A5%E5%AE%9E%E7%8E%B0/</id>
    <link href="https://xilidou.com/2017/10/24/Netty-Apns%E6%8E%A5%E5%85%A5%E5%AE%9E%E7%8E%B0/"/>
    <published>2017-10-24T00:00:39.000Z</published>
    <summary>
      <![CDATA[<p>极光推送免费版每分钟600次的请求限制实在是把我恶心坏了，考虑到现在我们 Android 的推送已经全量接入了小米，所以接下来就是要把 iOS 的推送直接接入 APNS 这样就可以彻底摆脱极光的推送。不再受这个600次&#x2F;分钟的限制了。APNS使用 HTTP2 协议进行通信所以自然就想到了使用Netty作为网络框架，进行开发。下面逐个给大家介绍使用 Netty 接入 APNS 的注意事项和接入的时候踩到的坑。</p>]]>
    </summary>
    <title>Netty-Apns接入实现</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="Future" scheme="https://xilidou.com/tags/Future/"/>
    <category term="多线程" scheme="https://xilidou.com/tags/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
    <content>
      <![CDATA[<h2 id="Future是什么？"><a href="#Future是什么？" class="headerlink" title="Future是什么？"></a>Future是什么？</h2><p>最近写了一些关于<code>netty</code>的相关代码，发现类似<code>netty</code> 的这种异步框架大量的使用一个Future的类。利用这个future类可以实现，代码的异步调用，程序调用耗时的网络或者IO相关的方法的时候，首先获得一个Future的代理类，同时线程并不会被阻塞。继续执行之后的逻辑，直到真正要使用远程调用返回的结果的时候，才需要调用future的<code>get()</code>方法。这样可以提高代码的执行效率。<br>于是就花了一点时间研究future是如何实现的。调用方式如何知道，结果什么时候返回的呢？如果使用一个线程去轮询<code>flag</code> 标记，那么就很难及时的感知对象的改变，同时还很难降低开销。。所以我们需要了解java的等待通知机制。利用这个机制来构建一个节能环保的Future。</p> <span id="more"></span><h2 id="等待通知机制"><a href="#等待通知机制" class="headerlink" title="等待通知机制"></a>等待通知机制</h2><p>一个线程修改了一个对象的值，另一个线程感知到了变化，然后进行相应的操作。一个线程是生产者，另一个线程是消费者。这种模式做到了解耦，隔离了“做什么”和“做什么”。如果要实现这个功能，我们可以利用java内对象内置的等待通知机制来实现。<br>我们知道’java.lang.Object’有以下方法</p><table><thead><tr><th>方法名称</th><th>描述</th></tr></thead><tbody><tr><td>notify（）</td><td>随机选择通知一个在对象上等待的的线程，解除其阻塞状态。</td></tr><tr><td>notfiyAll（）</td><td>解除所有那些在该对象上调用wait方法的线程的阻塞状态</td></tr><tr><td>wait（）</td><td>导致线程进入等待状态。</td></tr><tr><td>wait（long）</td><td>同上，同时设置一个超时时间，线程等待一段时间。</td></tr><tr><td>wait（long，int）</td><td>同上，且为超时时间设置一个单位。</td></tr></tbody></table><p>ps：敲黑板，面试中面试官可能会问，你了解’Object’的哪些方法？如果只答出 <code>toString()</code>的话。估计得出门右转慢走不送了。</p><p>所谓等待通知机制，就是某个线程A调用了对象 O 的<code>wait()</code>方法，另一个线程B调用对象 O 的 <code>notify()</code> 或者 <code>notifyAll()</code> 方法。 线程 A 接收到线程 B 的通知，从wait状态中返回，继续执行后续操作。两个线程通过对象 O 来进行通信。<br>我们看damo：</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><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></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">waitAndNotify</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> Object object = <span class="keyword">new</span> Object();</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">boolean</span> flag = <span class="keyword">true</span>;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException </span>&#123;</span><br><span class="line"></span><br><span class="line">        Thread a = <span class="keyword">new</span> Thread(<span class="keyword">new</span> waitThread(),<span class="string">&quot;wait&quot;</span>);</span><br><span class="line">        a.start();</span><br><span class="line"></span><br><span class="line">        TimeUnit.SECONDS.sleep(<span class="number">5</span>);</span><br><span class="line"></span><br><span class="line">        Thread b = <span class="keyword">new</span> Thread(<span class="keyword">new</span> notifyThread(),<span class="string">&quot;notify&quot;</span>);</span><br><span class="line">        b.start();</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">static</span> <span class="class"><span class="keyword">class</span> <span class="title">waitThread</span> <span class="keyword">implements</span> <span class="title">Runnable</span></span>&#123;</span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="keyword">synchronized</span> (object)&#123;</span><br><span class="line">                <span class="keyword">while</span> (flag)&#123;</span><br><span class="line">                    <span class="keyword">try</span> &#123;</span><br><span class="line">                        System.out.println(Thread.currentThread() + <span class="string">&quot;flag is true wait @&quot;</span> +</span><br><span class="line">                                <span class="keyword">new</span> SimpleDateFormat(<span class="string">&quot;HH:mm:ss&quot;</span>).format(<span class="keyword">new</span> Date()));</span><br><span class="line">                        object.wait();</span><br><span class="line">                    &#125;<span class="keyword">catch</span> (InterruptedException e)&#123;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                System.out.println(Thread.currentThread() + <span class="string">&quot;flag is false go on @&quot;</span>+</span><br><span class="line">                        <span class="keyword">new</span> SimpleDateFormat(<span class="string">&quot;HH:mm:ss&quot;</span>).format(<span class="keyword">new</span> Date()));</span><br><span class="line">            &#125;</span><br><span class="line"></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">static</span> <span class="class"><span class="keyword">class</span> <span class="title">notifyThread</span> <span class="keyword">implements</span> <span class="title">Runnable</span></span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="meta">@Override</span></span><br><span class="line">        <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">            <span class="keyword">synchronized</span> (object)&#123;</span><br><span class="line">                System.out.println(Thread.currentThread() + <span class="string">&quot;lock the thread and change flag&quot;</span> +</span><br><span class="line">                        <span class="keyword">new</span> SimpleDateFormat(<span class="string">&quot;HH:mm:ss&quot;</span>).format(<span class="keyword">new</span> Date()));</span><br><span class="line">                object.notify();</span><br><span class="line">                flag = <span class="keyword">false</span>;</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    TimeUnit.SECONDS.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"></span><br><span class="line">            <span class="keyword">synchronized</span> (object)&#123;</span><br><span class="line">                System.out.println(Thread.currentThread() + <span class="string">&quot;lock the thread again@&quot;</span> +</span><br><span class="line">                        <span class="keyword">new</span> SimpleDateFormat(<span class="string">&quot;HH:mm:ss&quot;</span>).format(<span class="keyword">new</span> Date()));</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    TimeUnit.SECONDS.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><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>程序的输出：</p><blockquote><p>Thread[wait,5,main]flag is true wait @22:46:52<br>Thread[notify,5,main]lock the thread and change flag22:46:57<br>Thread[notify,5,main]lock the thread again@22:47:02<br>Thread[wait,5,main]flag is false go on @22:47:07</p></blockquote><ol><li><code>wait()</code> 和 <code>notify()</code>以及<code>notifyAll()</code> 需要在对象被加锁以后会使用。</li><li>调用<code>notify()</code> 和<code>notifyAll()</code> 后，对象并不是立即就从<code>wait()</code>返回。而是需要对象的锁释放以后，等待线程才会从<code>wait()</code>中返回。</li></ol><h2 id="等待通知经典范式"><a href="#等待通知经典范式" class="headerlink" title="等待通知经典范式"></a>等待通知经典范式</h2><p>通过以上的代码我们可以把等待通知模式进行抽象。<br>wait线程：</p><ol><li>获取对象的锁。</li><li>条件不满足，调用对象<code>wait()</code>方法。</li><li>等待另外线程通知，如果满足条件，继续余下操作执行。<br>伪码如下：</li></ol><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">lock(object)&#123;</span><br><span class="line">    <span class="keyword">while</span>(condition)&#123;</span><br><span class="line">        object.wait();</span><br><span class="line">    &#125;</span><br><span class="line">    doOthers();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>notify线程：</p><ol><li>获取对象的锁。</li><li>修改条件。</li><li>调用对象的<code>notify()</code>或者<code>notifyAll()</code>方法通知等待的线程。</li><li>释放锁.<br>伪码如下:</li></ol><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">lock(object)&#123;</span><br><span class="line">    change(condition);</span><br><span class="line">    objcet.notify();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Future的实现原理："><a href="#Future的实现原理：" class="headerlink" title="Future的实现原理："></a>Future的实现原理：</h2><p>了解了java的等待通知机制，我们来看看如何利用这个机制实现一个简单的Future。<br>首先我们定义一个Future的接口：</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="class"><span class="keyword">interface</span> <span class="title">IData</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function">String <span class="title">getResult</span><span class="params">()</span></span>;</span><br><span class="line"></span><br><span class="line">&#125;</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><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"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RealData</span> <span class="keyword">implements</span> <span class="title">IData</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String result;</span><br><span class="line">    RealData(String str)&#123;</span><br><span class="line">        StringBuilder sb = <span class="keyword">new</span> StringBuilder();</span><br><span class="line">        <span class="comment">//假设一个相当耗时的远程方法</span></span><br><span class="line">        <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; <span class="number">20</span>; i++) &#123;</span><br><span class="line">            sb.append(<span class="string">&quot;i&quot;</span>).append(i);</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                Thread.sleep(<span class="number">1000</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">        result = sb.append(str).toString();</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="function"><span class="keyword">public</span> String <span class="title">getResult</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> result;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>同时还要有一个实现了<code>IData</code>的<code>RealData</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><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="class"><span class="keyword">class</span> <span class="title">FutureData</span> <span class="keyword">implements</span> <span class="title">IData</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> RealData realData;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">volatile</span> <span class="keyword">boolean</span> isReal = <span class="keyword">false</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> String <span class="title">getResult</span><span class="params">()</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">while</span> (!isReal)&#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                wait();</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">        <span class="keyword">return</span> realData.getResult();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">setReault</span><span class="params">(RealData realData)</span></span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(isReal)&#123;</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="keyword">this</span>.realData = realData;</span><br><span class="line">        isReal = <span class="keyword">true</span>;</span><br><span class="line">        notifyAll();</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>可以看出来我们的这个包装类就是一个相当标准的等待通知机制的类。</p><p>再看看我们Service类，在Service中的getData方法被调用的时候，程序只接返回了一个FutureData的代理类，同时起了一个新的线程去执行真正耗时的<code>RealData</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Service</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> IData <span class="title">getData</span><span class="params">(<span class="keyword">final</span> String str)</span></span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> FutureData futureData = <span class="keyword">new</span> FutureData();</span><br><span class="line">        <span class="keyword">new</span> Thread(<span class="keyword">new</span> Runnable() &#123;</span><br><span class="line">            <span class="meta">@Override</span></span><br><span class="line">            <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">                RealData realData = <span class="keyword">new</span> RealData(str);</span><br><span class="line">                futureData.setReault(realData);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;).start();</span><br><span class="line">        <span class="keyword">return</span> futureData;</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>最后看看是如何使用的：</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Clinet</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        Service service = <span class="keyword">new</span> Service();</span><br><span class="line"></span><br><span class="line">        IData data = service.getData(<span class="string">&quot;test&quot;</span>);</span><br><span class="line"></span><br><span class="line">        System.out.println(<span class="string">&quot;a&quot;</span>);</span><br><span class="line">        System.out.println(<span class="string">&quot;b&quot;</span>);</span><br><span class="line"></span><br><span class="line">        System.out.println(<span class="string">&quot;result is &quot;</span> + data.getResult());</span><br><span class="line"></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>执行的结果是：</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">a</span><br><span class="line">b</span><br><span class="line">result is i0i1i2i3i4i5i6i7i8i9i10i11i12i13i14i15i16i17i18i19test</span><br></pre></td></tr></table></figure><p>可见程序并没有因为调用耗时的方法阻塞，先打印了a和b，在程序调用<code>getReslut()</code>才打印出真正的结果。</p><h2 id="总结："><a href="#总结：" class="headerlink" title="总结："></a>总结：</h2><p>通过以上的讲解，我们总结一下future，首先使用future可以实现异步调用，实现future我们使用了java的等待通知机制。这个时候们回过头再来看netty的future就很简单了。</p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>《java并发编程的艺术》<br><a href="http://dantezhao.com/2017/04/23/concurrency-and-parallelism-future/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io">漫谈并发编程：Future模型（Java、Clojure、Scala多语言角度分析）</a></p>]]>
    </content>
    <id>https://xilidou.com/2017/10/24/Futuer%E7%A0%94%E7%A9%B6/</id>
    <link href="https://xilidou.com/2017/10/24/Futuer%E7%A0%94%E7%A9%B6/"/>
    <published>2017-10-24T00:00:07.000Z</published>
    <summary>
      <![CDATA[<h2 id="Future是什么？"><a href="#Future是什么？" class="headerlink" title="Future是什么？"></a>Future是什么？</h2><p>最近写了一些关于<code>netty</code>的相关代码，发现类似<code>netty</code> 的这种异步框架大量的使用一个Future的类。利用这个future类可以实现，代码的异步调用，程序调用耗时的网络或者IO相关的方法的时候，首先获得一个Future的代理类，同时线程并不会被阻塞。继续执行之后的逻辑，直到真正要使用远程调用返回的结果的时候，才需要调用future的<code>get()</code>方法。这样可以提高代码的执行效率。<br>于是就花了一点时间研究future是如何实现的。调用方式如何知道，结果什么时候返回的呢？如果使用一个线程去轮询<code>flag</code> 标记，那么就很难及时的感知对象的改变，同时还很难降低开销。。所以我们需要了解java的等待通知机制。利用这个机制来构建一个节能环保的Future。</p>]]>
    </summary>
    <title>Future研究</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Java" scheme="https://xilidou.com/categories/Java/"/>
    <category term="java" scheme="https://xilidou.com/tags/java/"/>
    <category term="后端" scheme="https://xilidou.com/tags/%E5%90%8E%E7%AB%AF/"/>
    <category term="Hystrix" scheme="https://xilidou.com/tags/Hystrix/"/>
    <content>
      <![CDATA[<h2 id="1、Hystrix是什么"><a href="#1、Hystrix是什么" class="headerlink" title="1、Hystrix是什么"></a>1、Hystrix是什么</h2><p>Hystrix 是Netflix开源的一个针对分布式容错和库。Hystrix的主要功能是隔离分布式系统之间的故障，防止故障带来的雪崩效应。同时也能提供一个分布式服务的优雅的降级方案。从而提高系统的可用性的组件。</p><span id="more"></span><h2 id="2、Hystrix设计理念是什么（其实也是高可用系统设计的理念）"><a href="#2、Hystrix设计理念是什么（其实也是高可用系统设计的理念）" class="headerlink" title="2、Hystrix设计理念是什么（其实也是高可用系统设计的理念）"></a>2、Hystrix设计理念是什么（其实也是高可用系统设计的理念）</h2><ol><li>防止单个系统故障后，造成容器（tomcat，scf）的线程全部占满，影响服务响应。</li><li>使用快速失败和泄洪代替队列等待。</li><li>在系统故障之后提供优雅的降级措施。</li><li>使用隔离技术降低故障影响面。</li><li>提供准实时的监控报警系统。</li><li>提供准实时动态的配置系统。å</li><li>客户端感知下游服务状态，防止错误的发展，而不通过真实的调用就能感知。</li></ol><h2 id="3、Hystrix怎么用"><a href="#3、Hystrix怎么用" class="headerlink" title="3、Hystrix怎么用"></a>3、Hystrix怎么用</h2><ul><li>Hello World：</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><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"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CommandHelloWorld</span> <span class="keyword">extends</span> <span class="title">HystrixCommand</span>&lt;<span class="title">String</span>&gt;</span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String name;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">CommandHelloWorld</span><span class="params">(String name)</span></span>&#123;</span><br><span class="line">        <span class="keyword">super</span>(HystrixCommandGroupKey.Factory.asKey(<span class="string">&quot;ExampleGroup&quot;</span>));</span><br><span class="line">        <span class="keyword">this</span>.name = name;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> String <span class="title">run</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;Hello &quot;</span> + name;</span><br><span class="line">    &#125;</span><br><span class="line">  </span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> ExecutionException, InterruptedException </span>&#123;</span><br><span class="line">    String s = <span class="keyword">new</span> CommandHelloWorld(<span class="string">&quot;BoB&quot;</span>).execute();</span><br><span class="line"></span><br><span class="line">    System.out.println(s);</span><br><span class="line"></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>降级：</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><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="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">CommandHelloFailure</span> <span class="keyword">extends</span> <span class="title">HystrixCommand</span>&lt;<span class="title">String</span>&gt; </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">final</span> String name;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="title">CommandHelloFailure</span><span class="params">(String name)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">super</span>(HystrixCommandGroupKey.Factory.asKey(<span class="string">&quot;ExampleGroup&quot;</span>));</span><br><span class="line">        <span class="keyword">this</span>.name = name;</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="function"><span class="keyword">protected</span> String <span class="title">run</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(<span class="string">&quot;this command always fails&quot;</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="function"><span class="keyword">protected</span> String <span class="title">getFallback</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">&quot;Hello Failure &quot;</span> + name + <span class="string">&quot;!&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="4、Hystrix实现思路分析"><a href="#4、Hystrix实现思路分析" class="headerlink" title="4、Hystrix实现思路分析"></a>4、Hystrix实现思路分析</h2><h3 id="1、数据流"><a href="#1、数据流" class="headerlink" title="1、数据流"></a>1、数据流</h3><p><img data-src="/images/2019-04-25-022253.png" alt="数据流"></p><ol><li>初始化 <code>HystrixCommand</code> 或者 <code>HystrixObservableCommand</code> 对象。</li><li>执行。</li><li>判断是否有缓存？</li><li>判断是否调用链路是否通畅？</li><li>判断线程池&#x2F;队列&#x2F;信号量 是否满了？</li><li>执行<code>HystrixObservableCommand.construct()</code> 或者<code>HystrixCommand.run()</code>方法</li><li>计算调用下游的健康程度</li><li>判断时候需要降级</li><li>完成请求</li></ol><h3 id="2、熔断器"><a href="#2、熔断器" class="headerlink" title="2、熔断器"></a>2、熔断器</h3><p><img data-src="/images/2019-04-25-22254.png" alt="IMG"><br>每个熔断器维护10个buckets窗口，每秒生成一个新的bucket，把最早的bucket抛弃，每个bucket记录了调用的，成功、失败、超时、拒绝的次数，如果失败数量达到某个阈值，就会触发熔断。</p><h3 id="3、隔离"><a href="#3、隔离" class="headerlink" title="3、隔离"></a>3、隔离</h3><h4 id="线程隔离"><a href="#线程隔离" class="headerlink" title="线程隔离"></a>线程隔离</h4><p>每个下游调用使用独立的线程池，而非与请求的调用共用一个线程池，这样可以防止失败的调用占用共用的线程池，造成整个系统拒绝服务。</p><h4 id="优缺点"><a href="#优缺点" class="headerlink" title="优缺点"></a>优缺点</h4><p>优点：<br>相互独立，减少互相影响的风险，总的来说就是隔离解耦，不会互相影响》<br>缺点：<br>过多的线程池造成cpu计算能力的消耗，和增加代码的复杂度。</p><h4 id="信号量隔离"><a href="#信号量隔离" class="headerlink" title="信号量隔离"></a>信号量隔离</h4><p>信号隔离也可以用于限制并发访问，防止阻塞扩散, 与线程隔离最大不同在于执行依赖代码的线程依然是请求线程（该线程需要通过信号申请）,<br>   如果客户端是可信的且可以快速返回，可以使用信号隔离替换线程隔离,降低开销.</p><h3 id="4、请求折叠"><a href="#4、请求折叠" class="headerlink" title="4、请求折叠"></a>4、请求折叠</h3><p>可以使用组件<code>HystrixCollapser</code>把前端的多个请求折叠为单一的一个后端请求。减少线程和链接的开销。</p><h3 id="5、请求缓存"><a href="#5、请求缓存" class="headerlink" title="5、请求缓存"></a>5、请求缓存</h3><p>把请求缓存起来。这个不过多解释了</p>]]>
    </content>
    <id>https://xilidou.com/2017/10/23/Hystrix%E5%85%A5%E9%97%A8%E7%A0%94%E7%A9%B6/</id>
    <link href="https://xilidou.com/2017/10/23/Hystrix%E5%85%A5%E9%97%A8%E7%A0%94%E7%A9%B6/"/>
    <published>2017-10-23T23:59:28.000Z</published>
    <summary>
      <![CDATA[<h2 id="1、Hystrix是什么"><a href="#1、Hystrix是什么" class="headerlink" title="1、Hystrix是什么"></a>1、Hystrix是什么</h2><p>Hystrix 是Netflix开源的一个针对分布式容错和库。Hystrix的主要功能是隔离分布式系统之间的故障，防止故障带来的雪崩效应。同时也能提供一个分布式服务的优雅的降级方案。从而提高系统的可用性的组件。</p>]]>
    </summary>
    <title>Hystrix入门研究</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
  <entry>
    <author>
      <name>Zhengxin Diao</name>
      <email>diaozxin@163.com</email>
    </author>
    <category term="Redis" scheme="https://xilidou.com/categories/Redis/"/>
    <category term="分布式" scheme="https://xilidou.com/tags/%E5%88%86%E5%B8%83%E5%BC%8F/"/>
    <category term="redis" scheme="https://xilidou.com/tags/redis/"/>
    <category term="锁" scheme="https://xilidou.com/tags/%E9%94%81/"/>
    <content>
      <![CDATA[<p>之前我们使用的定时任务都是只部署在了单台机器上，为了解决单点的问题，为了保证一个任务，只被一台机器执行，就需要考虑锁的问题，于是就花时间研究了这个问题。到底怎样实现一个分布式锁呢？</p><p>锁的本质就是<strong>互斥</strong>，保证任何时候能有一个客户端持有同一个锁，如果考虑使用redis来实现一个分布式锁，最简单的方案就是在实例里面创建一个键值，释放锁的时候，将键值删除。但是一个可靠完善的分布式锁需要考虑的细节比较多，我们就来看看如何写一个正确的分布式锁。</p> <span id="more"></span><h2 id="单机版分布式锁-SETNX"><a href="#单机版分布式锁-SETNX" class="headerlink" title="单机版分布式锁 SETNX"></a>单机版分布式锁 SETNX</h2><p>所以我们直接基于 redis 的 setNX (SET if Not eXists)命令，实现一个简单的锁。直接上伪码</p><p>锁的获取：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SET resource_name my_random_value NX PX <span class="number">30000</span></span><br></pre></td></tr></table></figure><p>锁的释放：</p><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> redis.call(<span class="string">&quot;get&quot;</span>,KEYS[<span class="number">1</span>]) == ARGV[<span class="number">1</span>] <span class="keyword">then</span></span><br><span class="line">    <span class="keyword">return</span> redis.call(<span class="string">&quot;del&quot;</span>,KEYS[<span class="number">1</span>])</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>几个细节需要注意：</p><ul><li><p>首先在获取锁的时候我们需要设置设置超时时间。设置超时时间是为了，防止客户端崩溃，或者网络出现问题以后锁一直被持有。真个系统就死锁了。</p></li><li><p>使用 setNX 命令，保证查询和写入两个步骤是原子的</p></li><li><p>在锁释放的时候我们判断了<code>KEYS[1]) == ARGV[1]</code>，在这里 <code>KEYS[1]</code>是从redis里面取出来的value，<code>ARGV[1]</code>是上文生成的<code>my_random_value</code>。之所以进行以上的判断，是为了保证<strong>锁被锁的持有者释放</strong>。我们假设不进行这一步校验：</p><ol><li>客户端A获取锁，后发线程挂起了。时间大于锁的过期时间。</li><li>锁过期后，客户端B获取锁。</li><li>客户端A恢复以后，处理完相关事件，向redis发起 del命令。锁被释放</li><li>客户端C获取锁。这个时候一个系统中同时两个客户端持有锁。</li></ol><p>  造成这个问题的关键，在于客户端B持有的锁，被客户端A释放了。  </p></li><li><p>锁的释放<strong>必须</strong>使用lua脚本，保证操作的原子性。锁的释放包含了<code>get</code>，判断，<code>del</code>三个步骤。如果不能保证三个步骤的原子性，分布式锁就会有并发问题。</p></li></ul><p>注意了以上细节，一个单redis节点的分布式锁就达成了。</p><p>在这个分布式锁中还是存在一个单点的redis。也许你会说，Redis是 master-slave的架构，发生故障的时候切换到slave就好，但是Redis的复制是异步的。</p><ul><li>如果在客户端A在master上拿到了锁。</li><li>在master将数据同步到slave上之前，master宕机。</li><li>客户端B就从slave上又一次拿到了锁。</li></ul><p>这样由于Master的宕机，造成了同时多人持有锁。如果你的系统可用接受短时时间内，有多人持有锁。这个简单的方案就能解决问题。</p><p>但是如果解决这个问题。Redis的官方提供了一个Redlock的解决方案。</p><h2 id="RedLock-的实现"><a href="#RedLock-的实现" class="headerlink" title="RedLock 的实现"></a>RedLock 的实现</h2><p>为了解决，Redis单点的问题。Redis的作者提出了RedLock的解决方案。方案非常的巧妙和简洁。<br>RedLock的核心思想就是，同时使用多个Redis Master来冗余，且这些节点都是完全的独立的，也不需要对这些节点之间的数据进行同步。</p><p>假设我们有N个Redis节点，N应该是一个大于2的奇数。RedLock的实现步骤:</p><ol><li>取得当前时间</li><li>使用上文提到的方法依次获取N个节点的Redis锁。</li><li>如果获取到的锁的数量大于 （N&#x2F;2+1）个,且获取的时间小于锁的有效时间(lock validity time)就认为获取到了一个有效的锁。锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。</li><li>如果获取锁的数量小于 （N&#x2F;2+1），或者在锁的有效时间(lock validity time)内没有获取到足够的说，就认为获取锁失败。这个时候需要向所有节点发送释放锁的消息。</li></ol><p>对于释放锁的实现就很简单了。想所有的Redis节点发起释放的操作，无论之前是否获取锁成功。</p><p>同时需要注意几个细节：</p><ul><li><p>重试获取锁的间隔时间应当是一个随机范围而非一个固定时间。这样可以防止，多客户端同时一起向Redis集群发送获取锁的操作，避免同时竞争。同时获取相同数量锁的情况。（虽然概率很低）</p></li><li><p>如果某master节点故障之后，回复的时间间隔应当大于锁的有效时间。</p><ol><li>假设有A，B，C三个Redis节点。</li><li>客户端foo获取到了A、B两个锁。</li><li>这个时候B宕机，所有内存的数据丢失。</li><li>B节点恢复。</li><li>这个时候客户端bar重新获取锁，获取到B，C两个节点。</li><li>此时又有两个客户端获取到锁了。</li></ol><p>  所以如果恢复的时间将大于锁的有效时间，就可以避免以上情况发生。同时如果性能要求不高，甚至可以开启Redis的持久化选项。</p></li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>了解了Redis分布式的实现以后，其实觉得大多数的分布式系统其实原理很简单，但是为了保证分布式系统的可靠性需要注意很多的细节，琐碎异常。<br>RedLock算法实现的分布式锁就是简单高效，思路相当巧妙。<br>但是RedLock就一定安全么？我还会写一篇文章来讨论这个问题。敬请大家期待。</p>]]>
    </content>
    <id>https://xilidou.com/2017/10/23/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/</id>
    <link href="https://xilidou.com/2017/10/23/Redis%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/"/>
    <published>2017-10-23T23:55:03.000Z</published>
    <summary>
      <![CDATA[<p>之前我们使用的定时任务都是只部署在了单台机器上，为了解决单点的问题，为了保证一个任务，只被一台机器执行，就需要考虑锁的问题，于是就花时间研究了这个问题。到底怎样实现一个分布式锁呢？</p>
<p>锁的本质就是<strong>互斥</strong>，保证任何时候能有一个客户端持有同一个锁，如果考虑使用redis来实现一个分布式锁，最简单的方案就是在实例里面创建一个键值，释放锁的时候，将键值删除。但是一个可靠完善的分布式锁需要考虑的细节比较多，我们就来看看如何写一个正确的分布式锁。</p>]]>
    </summary>
    <title>Redis实现分布式锁</title>
    <updated>2026-08-08T02:04:49.443Z</updated>
  </entry>
</feed>
