<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
<channel>
<atom:link href="https://liuyaowen.cn/feed" rel="self" type="application/rss+xml"/>
<title>刘耀文</title>
<link>https://liuyaowen.cn</link>
<description>刘耀文个人网站，聚焦技术分享、项目展示与成长记录，涵盖前端开发、人工智能、个人作品集等内容，致力于打造专业、有温度的开发者主页。</description>
<language>zh-CN</language>
<copyright>© 刘耀文 </copyright>
<pubDate>Tue, 17 Mar 2026 00:18:27 GMT</pubDate>
<generator>Mix Space CMS (https://github.com/mx-space)</generator>
<docs>https://mx-space.js.org</docs>
<image>
    <url>https://avatars.githubusercontent.com/u/55525531?v=4</url>
    <title>刘耀文</title>
    <link>https://liuyaowen.cn</link>
</image>
<item>
    <title>图像生成怎么突然把“写字”这件事做对了？</title>
    <link>https://liuyaowen.cn/posts/default/20260314</link>
    <pubDate>Sat, 14 Mar 2026 15:17:25 GMT</pubDate>
    <description>最近我有一个很直观的感受：  
这一波图像生成模型，很多已经不再只是“能画出像字的东西”，而是真的开</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://liuyaowen.cn/posts/default/20260314'>https://liuyaowen.cn/posts/default/20260314</a></blockquote>
      <p>最近我有一个很直观的感受：<br>这一波图像生成模型，很多已经不再只是“能画出像字的东西”，而是真的开始<strong>把字写对</strong>了。</p>
<p>这件事其实挺值得停下来想一下。</p>
<p>因为如果你回头看前两年的生图效果，会发现一个很稳定的槽点：<br>画面氛围、构图、光影都已经很强了，但只要图里出现招牌、海报标题、包装文案、UI 文本，基本就会露馅。英文会串字母，中文会缺部件，小字更是一塌糊涂。这个问题当时严重到什么程度？严重到不少关于视觉文本生成的论文，开头第一段就在说：现在的文生图虽然整体 fidelity 很高，但只要把视线移到 text area，破绽就非常明显。 <a href="https://arxiv.org/abs/2305.18259?utm_source=chatgpt.com">oai_citation:0‡arXiv</a></p>
<p>但最近不太一样了。</p>
<p>我自己看到的一些新结果，已经不只是“偶尔能写对几个字”，而是明显能感觉到：模型在处理标题、标语、场景里的招牌文字时，稳定性比以前高了一大截。于是我就有点好奇：<strong>这事到底是怎么被解决的？</strong><br>难道真的是模型大了、数据多了，所以顺手把文字问题也学会了？还是说，研究界其实已经悄悄换了一套解题思路？</p>
<p>我去翻了一圈近两三年的论文之后，得到的结论还挺明确的：</p>
<p><strong>文字问题不是靠 prompt engineering 慢慢磨出来的，也不是模型“顺便学会”的。它本质上是被单独拿出来，当成一个专门问题重新建模了。</strong>  <a href="https://arxiv.org/abs/2305.18259?utm_source=chatgpt.com">oai_citation:1‡arXiv</a></p>
<p>这篇文章就想顺着这个好奇心，聊清楚三件事：</p>
<ol>
<li>为什么图像生成一碰到文字就特别容易翻车；  </li>
<li>最近这些方法到底是怎么解决的；  </li>
<li>为什么我越来越觉得，这条路线最后会走向一种 <strong>glyph-first</strong> 的系统，而不是继续指望 prompt 自己长出排版能力。</li>
</ol>
<hr>
<h2>先说最核心的一点：文字不是普通图像内容</h2>
<p>我觉得理解这个问题，第一步不是去看 attention，也不是去看 OCR loss，而是先承认一件事：</p>
<p><strong>文字和“云、树、衣服、墙面纹理”不是同一类对象。</strong></p>
<p>普通图像内容大多是连续的。<br>云稍微糊一点，还是云；木纹稍微歪一点，也还是木纹。<br>但文字不是这样。文字是低容错的离散符号系统，一个字少一笔、错一个结构，可能立刻就不可读了。</p>
<p>这也是为什么早期很多模型会给人一种很微妙的感觉：<br>远看挺像，近看不对。<br>你会觉得“这里好像应该有字”，但认真一看，其实只是某种<strong>很像文字的纹理</strong>，不是可读的文字。</p>
<p>GlyphControl 在 2023 年就把这个问题讲得很直白：视觉文本生成不是普通 T2I 任务的自然延伸，必须引入额外的 glyph 条件信息，才能稳定生成准确文本。AnyText 也有类似判断：即便图像整体质量已经很高，一旦聚焦到文本区域，问题就会立刻暴露出来。 <a href="https://arxiv.org/abs/2305.18259?utm_source=chatgpt.com">oai_citation:2‡arXiv</a></p>
<pre class="mermaid">flowchart TD
    A[为什么文字特别难] --> B[文字是离散符号]
    A --> C[文字既有语义又有几何]
    A --> D[文字区域通常很小]
    A --> E[文字目标和背景目标并不一致]

    B --> B1[一笔错就可能不可读]
    C --> C1[不仅要知道写什么 还要知道长什么样]
    D --> D1[全图训练时容易被背景梯度淹没]
    E --> E1[背景追求自然连续 文字追求局部精确]</pre><hr>
<h2>为什么以前的模型总是“像有字”，但又写不对？</h2>
<p>如果只从模型结构上看，这个问题其实挺自然的。</p>
<p>传统文生图做条件注入，基本是这条路：</p>
<ul>
<li>图像 latent / patch token 当 Query</li>
<li>文本 token 当 Key / Value</li>
<li>让图像在生成过程中不断去“读”文本条件</li>
</ul>
<p>这个机制对普通语义对齐非常有效。<br>比如 prompt 里有 <code>red car on snow</code>，模型很容易学会：</p>
<ul>
<li>哪些区域该看 <code>car</code></li>
<li>哪些区域该看 <code>snow</code></li>
<li>哪些局部更多受到 <code>red</code> 的影响</li>
</ul>
<p>但问题在于，<strong>文本 token 提供的是语义，不是字形。</strong></p>
<p>token “A” 并不是字母 A 的视觉结构，<br>token “春” 也不是“春”字的具体笔画排列。<br>所以如果系统只吃语义 token，它更容易学会的是：</p>
<blockquote>
<p>这里应该有一段“文字感”</p>
</blockquote>
<p>而不是：</p>
<blockquote>
<p>这里必须是这个字符，而且要笔画对、边界清楚、相邻字符别打架</p>
</blockquote>
<p>这也是为什么过去两三年的论文，几乎都在朝一个方向收敛：</p>
<p><strong>别再只靠 semantic conditioning 了，要把文字从“语言提示”升级成“显式字形条件”。</strong><br>GlyphControl 是这样，AnyText 是这样，FLUX-Text 是这样，TextPixs 也是这样。 <a href="https://arxiv.org/abs/2305.18259?utm_source=chatgpt.com">oai_citation:3‡arXiv</a></p>
<pre class="mermaid">flowchart LR
    A[仅有语义 token] --> B[告诉模型 这里应该有文字]
    B --> C[优点: 和普通 T2I 兼容]
    B --> D[缺点: 没告诉模型这个字具体长什么样]
    D --> E[结果: 容易生成伪字形/串字/错字]

    F[显式 glyph 条件] --> G[告诉模型 这里应该是这个字符形状]
    G --> H[附带位置/大小/区域信息]
    H --> I[结果: 可读性和可控性明显上升]</pre><hr>
<h2>研究界是怎么一步步把这个问题拆开的？</h2>
<p>如果把近几年的工作串起来看，我觉得它不是“某篇论文突然发明了一个神奇模块”，而是经历了一个挺清楚的演化过程。</p>
<h3>第一阶段：先承认“文字生成”是一个独立问题</h3>
<p>这一阶段最重要的，不是技术细节，而是问题被重新命名了。</p>
<p>GlyphControl 很关键的一点，是明确提出 visual text generation 需要 glyph-conditional control，而不是继续指望 character-aware text encoder 或更大的通用模型自己学会。论文还专门构建了 LAION-Glyph 数据集，并用 OCR-based metrics、CLIP score、FID 来评估结果。这个动作很重要，因为它等于在说：<strong>文字渲染已经值得有自己的一套 benchmark 和指标。</strong>  <a href="https://arxiv.org/abs/2305.18259?utm_source=chatgpt.com">oai_citation:4‡arXiv</a></p>
<p>AnyText 则把这件事进一步系统化了。它不只是做文字生成，还把多语言文字生成和编辑放进同一个扩散框架里，并引入 AnyWord-3M 数据集和 AnyText-benchmark。论文里说得很清楚：文字区域的问题不能再被当成普通图像 fidelity 的附属现象，它需要独立建模。 <a href="https://arxiv.org/abs/2311.03054?utm_source=chatgpt.com">oai_citation:5‡arXiv</a></p>
<p>换句话说，第一阶段真正发生的事是：</p>
<blockquote>
<p>大家不再问“为什么模型还不会写字”，<br>而开始问“如果把写字当成一个专门任务，我们应该怎么表示它、训练它、评估它？”</p>
</blockquote>
<hr>
<h3>第二阶段：从 semantic-first 走向 glyph-first</h3>
<p>这是我觉得最关键的转折。</p>
<p>以前的做法，本质上是 semantic-first：<br>先给模型一个字符串的语义表示，然后希望它在图像空间里自己把字符外形“悟出来”。</p>
<p>但后来大家慢慢发现，这条路上限不高。<br>因为语义和字形根本不是一回事。你知道一个词的 meaning，不等于你就知道它在图上该怎么写。</p>
<p>所以 GlyphControl 的解法很直接：<br>不要只告诉模型“这里写 SALE”，还要把 <strong>SALE 的 glyph instruction</strong> 明确给它。这样用户不仅能控制文本内容，还能控制位置和大小。 <a href="https://arxiv.org/abs/2305.18259?utm_source=chatgpt.com">oai_citation:6‡arXiv</a></p>
<p>AnyText 继续沿着这个方向往前走，它的设计里有两个特别重要的模块：</p>
<ul>
<li><strong>Auxiliary latent module</strong>：吃 glyph、position、masked image，生成跟文字相关的 latent feature；</li>
<li><strong>Text embedding module</strong>：借助 OCR 模型去编码 stroke 信息，再把这些 embedding 和 caption embedding 融合起来。</li>
</ul>
<p>这其实已经很能说明问题了：<br>真正有效的文字生成，不是再多喂一点 prompt，而是把<strong>字形、位置、区域</strong>这些原本没被好好表示的东西，变成模型能直接消费的条件。 <a href="https://arxiv.org/abs/2311.03054?utm_source=chatgpt.com">oai_citation:7‡arXiv</a></p>
<p>我自己看到这里的时候，感受挺强的：<br>这已经不是“优化 prompt 理解”了，而是在重新定义输入空间。</p>
<pre class="mermaid">flowchart TD
    A[从 prompt-only 到 glyph-first] --> B[阶段 1 只给字符串语义]
    A --> C[阶段 2 给 glyph + position + region]
    A --> D[阶段 3 再把这些条件深入注入 backbone]

    B --> B1[容易得到像文字的纹理]
    C --> C1[开始能控制字符内容/位置/大小]
    D --> D1[开始追求字符级稳定和场景一致性]</pre><hr>
<h3>第三阶段：问题不只是“有没有 glyph”，而是“glyph 怎么进系统”</h3>
<p>glyph conditioning 成为共识之后，研究重点就开始下沉。</p>
<p>大家不再争论“该不该给 glyph”，而是开始研究：</p>
<ul>
<li>glyph 是当额外输入就够了吗？</li>
<li>还是应该更深地改 backbone？</li>
<li>训练目标是不是也得跟着变？</li>
</ul>
<p>FLUX-Text 是我觉得很典型的一篇。<br>它不是那种特别花哨的“新世界观”，但它很实在。它的思路是：在 FLUX-Fill 这种强 base model 上，用比较轻量的 glyph 和 text embedding 模块增强文字理解与生成，同时尽量保留原有生成能力。更重要的是，它显式提出了 <strong>Regional Text Perceptual Loss</strong>，就是告诉你：<strong>文字区域必须被单独优化。</strong>  <a href="https://arxiv.org/abs/2505.03329?utm_source=chatgpt.com">oai_citation:8‡arXiv</a></p>
<p>这一点其实特别重要。</p>
<p>因为文字区域通常很小，如果训练目标还是全图统一的，那么大部分梯度都来自背景。模型当然会更优先学“把图做漂亮”，而不是“把字写对”。FLUX-Text 的意思某种程度上就是：<br>你不能一边说文字很重要，一边在损失函数里继续拿它当背景噪声的一小块。 <a href="https://arxiv.org/abs/2505.03329?utm_source=chatgpt.com">oai_citation:9‡arXiv</a></p>
<hr>
<h3>第四阶段：从“词级条件”继续下钻到“字符级绑定”</h3>
<p>再往后，问题就会变得更细。</p>
<p>即使你给了 glyph，也不代表字符之间不会互相干扰。<br>很多时候模型出的问题不再是“完全不知道写什么”，而是：</p>
<ul>
<li>相邻字符粘连</li>
<li>一个字符的结构跑到另一个字符那边去</li>
<li>整串文本看起来有大概的样子，但局部字符不稳定</li>
</ul>
<p>这时候 TextPixs 这种工作就很有意思了。</p>
<p>它做了几件非常针对性的事：</p>
<ul>
<li>双流编码：语义文本流 + glyph 视觉流</li>
<li><strong>character-aware attention</strong></li>
<li>OCR-in-the-loop feedback</li>
<li>attention segregation loss</li>
</ul>
<p>这套东西的核心直觉很简单：<br><strong>文字不是词级别对齐就够了，而是要字符级别地对齐。</strong></p>
<p>普通 T2I 里，token 级控制通常已经足够。<br>但文字渲染不一样，字符是最终可读性的最小单位。如果字符之间的 attention 没有被稳定地分开，系统就很容易得到“这是一串字”的整体感觉，却写不对具体每个字。TextPixs 也直接把目标写得很清楚：解决 readable、meaningful、correctly spelled text 的问题。 <a href="https://arxiv.org/abs/2507.06033?utm_source=chatgpt.com">oai_citation:10‡arXiv</a></p>
<pre class="mermaid">flowchart LR
    A[词级/短语级条件] --> B[整体知道这里有一串文字]
    B --> C[问题: 字符边界不清 相互污染]

    D[字符级条件与注意力] --> E[每个字符更独立地绑定区域]
    E --> F[结果: 拼写更稳定 可读性更高]</pre><hr>
<h3>第五阶段：也许模型不该负责“从头学拼写”，而该负责“把文字融合进场景”</h3>
<p>翻到比较新的工作时，我最感兴趣的一条思路，其实不是单纯“准确率又涨了多少”，而是问题定义开始变了。</p>
<p>比如 TextFlux 的意思就很明显：<br>它强调的是 <strong>OCR-free DiT model for high-fidelity multilingual scene text synthesis</strong>，同时把重点放在 glyph accuracy 和 scene integration 上。 <a href="https://arxiv.org/abs/2505.03329?utm_source=chatgpt.com">oai_citation:11‡arXiv</a></p>
<p>这背后有个挺大的范式变化：</p>
<ul>
<li>旧问题：怎么让模型自己从语义里学会 spelling</li>
<li>新问题：怎么把可靠的字符表示自然地融进图像场景</li>
</ul>
<p>我越来越觉得，后者可能才是更长期的方向。</p>
<p>因为如果你让一个通用图像模型同时承担两件事：</p>
<ol>
<li>生成复杂视觉世界  </li>
<li>还得像排版引擎一样精确输出字符</li>
</ol>
<p>那它天然就有点拧巴。<br>但如果你把 spelling 这件事尽量结构化、显式化，让模型把重心放在融合——也就是风格、材质、光照、透视、边缘过渡——这条路反而更合理。</p>
<p>这也是为什么我现在会更愿意把这条研究线理解成：</p>
<blockquote>
<p>不是“模型终于学会写字了”，<br>而是“系统终于不再把文字当普通纹理处理了”。</p>
</blockquote>
<hr>
<h2>把这些方案压成一句话，其实就是三步</h2>
<p>如果不展开细节，把这几年的路线浓缩一下，我觉得就是下面三步：</p>
<h3>第一步：把文字从语义提示升级成字形条件</h3>
<p>也就是从 prompt-only 走向 glyph-first。<br>这是 GlyphControl 和 AnyText 这类方法最早讲清楚的事。 <a href="https://arxiv.org/abs/2305.18259?utm_source=chatgpt.com">oai_citation:12‡arXiv</a></p>
<h3>第二步：把文字区域从整图里单独拎出来优化</h3>
<p>也就是别让全图损失继续淹没文字区域。<br>FLUX-Text 这种 regional text loss 的思路很典型。 <a href="https://arxiv.org/abs/2505.03329?utm_source=chatgpt.com">oai_citation:13‡arXiv</a></p>
<h3>第三步：把控制粒度从词级推进到字符级，再进一步推进到场景融合级</h3>
<p>TextPixs 代表字符级绑定这一步，后续一些工作则更强调 scene integration。 <a href="https://arxiv.org/abs/2507.06033?utm_source=chatgpt.com">oai_citation:14‡arXiv</a></p>
<pre class="mermaid">flowchart TD
    A[文字问题的主线解法] --> B[glyph-first]
    A --> C[region-first]
    A --> D[character-first]
    A --> E[integration-first]

    B --> B1[不只告诉模型写什么 还告诉它长什么样]
    C --> C1[文字区域单独加权优化]
    D --> D1[字符级注意力与约束]
    E --> E1[重点解决和背景如何自然融合]</pre><hr>
<h2>我自己的感受：这可能是图像生成从“会画图”走向“能用”的一个拐点</h2>
<p>我这次翻论文，最大的感受不是“原来某个模块这么 clever”，而是：</p>
<p><strong>文字问题其实特别像一个分水岭。</strong></p>
<p>过去很多视觉生成模型，主要解决的是“生成一张看起来不错的图”。<br>但只要场景变成海报、包装、UI、招牌、广告、知识卡片，评价标准就会立刻变化：</p>
<ul>
<li>画面再美，字错了就没法用；</li>
<li>氛围再好，标题糊了就不能进生产流程；</li>
<li>文字编辑不稳定，就很难真正接入设计工作流。</li>
</ul>
<p>所以文字渲染这件事，表面上看像个细节，实际上逼着整个系统往更工程化、更结构化的方向走。<br>它迫使模型回答一个过去可以回避的问题：</p>
<blockquote>
<p>你到底是在生成“好看的视觉纹理”，<br>还是在生成“可被人类读懂和使用的信息”？</p>
</blockquote>
<p>而最近这些论文给出的共同答案大概是：</p>
<p><strong>如果你想生成的是后者，那就别再把文字当普通图像内容。</strong></p>
<hr>
<h2>参考论文</h2>
<p>[1] Yukang Yang et al., <strong>GlyphControl: Glyph Conditional Control for Visual Text Generation</strong>, NeurIPS 2023. 提出 glyph-conditional control，并构建 LAION-Glyph 与 OCR-based 评测。 <a href="https://arxiv.org/abs/2305.18259?utm_source=chatgpt.com">oai_citation:15‡arXiv</a></p>
<p>[2] Yuxiang Tuo et al., <strong>AnyText: Multilingual Visual Text Generation and Editing</strong>, 2023/2024. 提出 auxiliary latent module、text embedding module，以及 AnyWord-3M / AnyText-benchmark。 <a href="https://arxiv.org/abs/2311.03054?utm_source=chatgpt.com">oai_citation:16‡arXiv</a></p>
<p>[3] Rui Lan et al., <strong>FLUX-Text: A Simple and Advanced Diffusion Transformer Baseline for Scene Text Editing</strong>, 2025. 强调轻量 glyph/text embedding 与 text fidelity，并引入文字区域感知的优化思路。 <a href="https://arxiv.org/abs/2505.03329?utm_source=chatgpt.com">oai_citation:17‡arXiv</a></p>
<p>[4] <strong>TextPixs: Glyph-Conditioned Diffusion with Character-Aware Attention and OCR-in-the-Loop Feedback for Accurate Text Rendering</strong>, 2025. 强调 dual-stream、character-aware attention、OCR-in-the-loop，以及字符级准确性。 <a href="https://arxiv.org/abs/2507.06033?utm_source=chatgpt.com">oai_citation:18‡arXiv</a></p>

      <p style='text-align: right'>
      <a href='https://liuyaowen.cn/posts/default/20260314#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">69b57c050c469e94b73107cd</guid>
  <category>posts</category>
<category>技术</category>
 </item>
  <item>
    <title>把经验写进仓库：关于 Skills 的一些想法</title>
    <link>https://liuyaowen.cn/posts/default/experience-skills-thoughts</link>
    <pubDate>Wed, 11 Mar 2026 09:44:31 GMT</pubDate>
    <description>把经验写进仓库

最近读 OpenAI 那篇讲 Skills 和 Agents SDK 的文章，脑子</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://liuyaowen.cn/posts/default/experience-skills-thoughts'>https://liuyaowen.cn/posts/default/experience-skills-thoughts</a></blockquote>
      <h1>把经验写进仓库</h1>
<p>最近读 OpenAI 那篇讲 Skills 和 Agents SDK 的文章，脑子里一直在转一个很小的问题。</p>
<p>我们到底是在使用 AI，还是在把自己的工作方式，一点点整理成 AI 也能接住的东西。</p>
<p>以前总觉得，所谓“AI 提效”，核心是模型越来越强。它会写代码，会解释报错，会总结文档，也能把一段模糊的需求翻译成还算可用的实现。可真把它放进日常工程里，事情很快又会变得具体起来。</p>
<p>你会发现，问题常常不是它不够聪明，而是它不知道你们平时到底怎么做事。</p>
<p>同样是改完代码之后的“检查一下”，有人会顺手把 format、lint、test 全跑完，有人只会跑当前模块，有人记得补 typecheck，有人则默认 CI 会兜底。<br>同样是准备提 PR，有人会把改动背景、影响范围和验证方式写得很清楚，有人只留下一个简短标题，像往河里扔下一颗石头，剩下的涟漪全交给 reviewer 自己理解。</p>
<p>这些差别，看起来像是个人习惯。可如果往下想一层，它其实是一种没有被正式写下来的经验。</p>
<p>而 Skills 有意思的地方就在这里。</p>
<p>它不是让模型再多会一点东西，也不是再给 prompt 换一个更精致的名字。它更像是在说：如果一件事会一遍遍发生，如果它背后已经有了相对稳定的做法，那为什么不把它认真写下来，写进仓库，写成一种能被调用的能力。</p>
<p>这件事让我有点触动。因为它不是在追求一种更炫的智能感，反而是在做一件很朴素的事：把原本只存在于人脑子里的工作习惯，慢慢整理成系统的一部分。</p>
<h2>Skill 像什么</h2>
<p>如果只从表面看，skill 很容易被理解成“提示词模板”。</p>
<p>但我越来越觉得，它更像是一个很小的工作单元。<br>它知道自己该在什么时候出现，知道自己要完成什么，也知道哪些地方该交给模型判断，哪些地方该交给脚本执行。</p>
<p>OpenAI 的文章里提到，一个 skill 通常会有自己的说明文件，也可能带着脚本、参考资料，甚至一些辅助资源。换句话说，它并不是一句轻飘飘的“帮我做这个”，而是一套被稍微整理过的经验。</p>
<p>我很喜欢这种感觉。</p>
<p>因为很多时候，团队里的知识并不是没有，只是它们以一种很松散的形式存在着。它可能藏在某个资深工程师的习惯里，藏在某次 code review 的评论里，藏在一个只被提过一次的内部约定里。你问起来，大家都知道一点；真要写下来，又总觉得“这种事不是理所当然的吗”。</p>
<p>可一旦要交给 Agent，很多“理所当然”就突然变得不再理所当然了。</p>
<p>它不会天然知道，改了这几个目录意味着什么。<br>它不会天然知道，某一类改动是必须完整验证的，另一类改动却只需要最小检查。<br>它也不会知道，你们习惯怎样描述一次改动，怎样把上下文交代给下一个接手的人。</p>
<p>于是 skill 的意义开始变得清楚起来。<br>它不是替代经验，而是在保存经验。<br>不是制造新的流程，而是把原来那些靠默契维持的东西，慢慢落成可以复用的形状。</p>
<h2>真正难的，不是写“做什么”，而是写“什么时候做”</h2>
<p>我觉得 OpenAI 那篇文章里最值得反复想的一点，是它对 skill description 的强调。</p>
<p>一个 skill 是否真的有用，关键不是写得多完整，而是写得够不够准。<br>尤其是那个“什么时候该触发”的边界。</p>
<p>这件事听起来很细，可其实很像平时和人协作。<br>真正让一段协作顺畅起来的，从来不只是步骤本身，而是时机。</p>
<p>如果你只说“运行验证流程”，这当然没错，但几乎没什么帮助。<br>因为接下来最重要的问题都还没回答：</p>
<p>什么样的改动算需要验证？<br>只是改了文档呢？<br>只是重命名文件呢？<br>如果动到了测试或者构建链路，是不是就应该升级为完整检查？<br>这个动作是建议性的，还是必须性的？</p>
<p>当这些边界没被说清楚时，一个 skill 就很容易变得像一件摆设。它存在，但不稳定；偶尔会被调用，偶尔又会被忽略。最后的问题不在模型，而在于这个能力没有真正定义好自己出现的时刻。</p>
<p>我会觉得，写 skill 有点像在写一种很轻的制度。<br>制度不是靠语气强硬，而是靠边界清楚。<br>什么时候该发生，什么时候不该发生，什么属于例外，什么必须执行，这些东西一旦明确了，后面的流程反而会变简单。</p>
<h2>仓库里其实一直缺一种“写给 Agent 的文档”</h2>
<p>文章里还提到了 <code>AGENTS.md</code>。这个设定我也很喜欢。</p>
<p>它有一点像过去我们写给新同事的 onboarding 文档，只不过这次读者不是人，而是 Agent。</p>
<p>仓库有自己的脾气。<br>目录结构是怎么分的，哪里是核心路径，哪些约定看起来不显眼却不能碰，某些模块的测试为什么要这么跑，某些 API 的行为到底应该信源码还是信文档，这些都是仓库内部的语言。</p>
<p>平时这些东西靠人来理解，问题也不大。人会猜，会追问，会顺着上下文补全很多隐含信息。<br>但 Agent 不太一样。它当然会推理，可它最怕的是那些所有人都默认存在、却没有人认真写下来的东西。</p>
<p>所以 <code>AGENTS.md</code> 给我的感觉，不是又多了一份文档，而是终于出现了一种明确的载体，去承接那些“本来应该早就写清楚”的仓库共识。</p>
<p>如果说 <code>AGENTS.md</code> 更像总说明，那 skill 就更像一个个局部工作流。<br>前者回答“这里是一个什么样的地方”，后者回答“在这里，某件事该怎么做”。</p>
<p>再往下，还有 GitHub Actions 这种更确定的自动化兜底。<br>这样一层层看下来，会觉得这个结构其实很顺。</p>
<p>人负责形成经验。<br>文档负责表达经验。<br>skill 负责调用经验。<br>脚本和 CI 负责执行经验。</p>
<p>这和我过去想象的“AI 改变工程”很不一样。它没有特别戏剧化，反而很像软件工程一直以来最熟悉的路径：把不稳定的手工操作，慢慢整理成稳定的系统行为。</p>
<h2>模型不需要什么都做</h2>
<p>这篇文章里还有一个我很认同的判断：模型不应该负责一切。</p>
<p>这件事说出来很简单，但真正做的时候，太容易贪心了。<br>因为模型看起来什么都能处理，于是我们会不自觉地把理解、判断、执行、格式化输出，全部揉成一团交给它。</p>
<p>结果通常不会太好。</p>
<p>需要理解上下文、做比较、做总结、做判断的事，模型很擅长。<br>但需要按固定顺序执行命令、收集状态、输出可预期结果的事，本质上还是脚本更适合。</p>
<p>这个边界一旦想清楚，很多设计问题就会顺下来。</p>
<p>比如判断一个改动是不是行为变更，这种事要靠模型。它需要读 diff，理解意图，也理解周边上下文。<br>但“先跑哪些命令、失败了该怎么汇总、从哪里收集 git 状态和分支信息”，这些就该尽量交给脚本。</p>
<p>我一直觉得，好的系统不是把能力堆在一个点上，而是把职责分开。<br>模型负责那些本来就不确定的部分。<br>脚本负责那些应该尽量确定的部分。</p>
<p>Skill 之所以让我觉得它更接近“工程”，也在这里。它没有把模型想象成一个全能的主体，而是愿意承认：真正稳定的工作流，往往来自不同能力的合作，而不是单一能力的膨胀。</p>
<h2>说到底，Skills 在整理的是“隐性经验”</h2>
<p>我后来越想越觉得，Skills 最值得珍惜的地方，可能不是它能提升多少效率，而是它逼着团队去面对一件平时不太愿意面对的事：我们的很多工作，其实建立在隐性经验上。</p>
<p>这些经验平时并不显得稀缺。<br>因为总有人知道。<br>一个团队里，总会有一些人对仓库很熟，对流程很熟，对各种“虽然没写，但大家都这么做”的东西很熟。于是问题看起来总能被解决，流程看起来总能被跑通。</p>
<p>可一旦团队变大，项目变复杂，或者引入 Agent，这些隐性的部分就会立刻暴露出来。</p>
<p>因为 AI 不会自动继承团队默契。<br>它也不会天然理解某些“本来不用说”的规则。</p>
<p>于是很多以前能被熟人网络悄悄消化掉的问题，现在都必须被明说了：</p>
<p>什么是必须做的？<br>什么只是建议？<br>哪些步骤必须自动化？<br>哪些判断仍然需要人来做？<br>哪些知识应该写进仓库，而不是留在聊天记录里？</p>
<p>这让我觉得，Skills 表面上是在帮助 Agent，实际上也在帮助团队重新认识自己。<br>你们到底是怎样工作的。<br>哪些经验是可迁移的。<br>哪些流程值得固化。<br>哪些依赖于个人记忆的地方，早就该被替换掉了。</p>
<p>从这个角度看，它不是一个单纯的新功能，而像一次很安静的整理。</p>
<h2>我会从什么地方开始</h2>
<p>如果真要在一个仓库里慢慢把 skill 建起来，我大概不会一上来就做一个很大的系统。</p>
<p>我更愿意从那些已经足够重复、足够明确、而且失败成本又不低的事情开始。</p>
<p>比如验证流程。<br>这几乎是最自然的一类。改动之后该跑什么、顺序是什么、哪些情况需要附加检查、失败时怎么把结果说清楚，这些都很适合被整理成 skill。它高频、重复，而且一旦依赖记忆，就特别容易漏。</p>
<p>再比如 PR 的整理工作。<br>标题怎么起，改动怎么概括，影响范围怎么交代，验证方式怎么写得让 reviewer 不费力。这种事情本身不算难，但很消耗注意力。一个好的 draft 往往就能省掉很多来回解释。</p>
<p>还有文档核对。<br>OpenAI 在文章里提到，涉及平台或 API 行为时，他们会让 Agent 去查当前文档，而不是凭记忆回答。这个原则我很喜欢。因为变化快的东西最怕“我记得好像是这样”。把“先查文档再回答”变成一种默认动作，本质上是在给系统加一个很必要的约束。</p>
<p>再往后，可能是发版前检查。<br>这种流程不一定每天发生，但一旦出错，往往会让人很后悔。版本号、changelog、示例是否可运行、release note 草稿、breaking changes 的确认，这些都很适合被慢慢沉淀下来。</p>
<p>它们有一个共同点：都不是创造性的工作，而是重复性的工作。<br>也正因为如此，它们才更值得被写进仓库。</p>
<h2>我为什么会对这件事有一点点乐观</h2>
<p>这几年关于 AI 的讨论很多，热闹也很多。<br>可越往后，我反而越容易被一些安静的东西打动。不是模型又刷新了哪个 benchmark，不是 Agent 又完成了多复杂的任务，而是像 Skills 这种，看起来没有那么耀眼，却很接近真实工作现场的设计。</p>
<p>它让我觉得，AI 真正进入工程，不一定是靠一次 dramatic 的替代，而更可能是靠这种缓慢的渗透：先接住一个小流程，再接住一种工作习惯，再把一种原本只存在于经验里的做法，慢慢变成仓库的一部分。</p>
<p>这件事的迷人之处在于，它不是在制造新的神话，而是在保存那些原本就有价值的东西。</p>
<p>我们一直说，软件工程是在把手工经验系统化。<br>某种程度上，Skills 只是把这句话继续往前推了一步。</p>
<p>以前我们把经验写成文档，写成脚本，写成 CI。<br>现在，我们开始尝试把经验写成 Agent 也能理解和使用的能力。</p>
<p>我挺喜欢这种变化。</p>
<p>因为它让我第一次觉得，Agent 不是突然闯进工程体系里的一个外来者。<br>它更像一个新的参与者。<br>而一个新的参与者，最重要的不是“它能做多少事”，而是“我们愿不愿意认真把自己的工作方式告诉它”。</p>
<p>说到底，skill 不是在教模型怎么工作。<br>它是在逼我们先想清楚，自己到底是怎么工作的。</p>

      <p style='text-align: right'>
      <a href='https://liuyaowen.cn/posts/default/experience-skills-thoughts#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">69b1397f0c469e94b7308bb0</guid>
  <category>posts</category>
<category>技术</category>
 </item>
  <item>
    <title>给 OpenClaw 装上眼睛：ARM64 服务器无头浏览器实战指南</title>
    <link>https://liuyaowen.cn/posts/default/openclaw-arm64-1772639270</link>
    <pubDate>Wed, 04 Mar 2026 15:47:51 GMT</pubDate>
    <description>很多同学选择在服务器上部署 OpenClaw 作为个人 AI 助手，但默认情况下 OpenClaw </description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://liuyaowen.cn/posts/default/openclaw-arm64-1772639270'>https://liuyaowen.cn/posts/default/openclaw-arm64-1772639270</a></blockquote>
      <p>很多同学选择在服务器上部署 OpenClaw 作为个人 AI 助手，但默认情况下 OpenClaw 无法访问网页。这对于一个需要&quot;上网冲浪&quot;的 AI 助手来说无疑是最大的限制。</p>
<p>好消息是，通过配置无头浏览器（Headless Browser），我们可以让 OpenClaw 具备网页访问、截图、自动化操作等能力。本文将详细介绍在 ARM64 架构服务器上的完整配置过程。</p>
<hr>
<h2>为什么需要浏览器</h2>
<p>OpenClaw 本身是一个 AI 助手框架，要让它真正&quot;智能&quot;地帮你做事，浏览器能力必不可少：</p>
<ul>
<li><strong>实时信息获取</strong>：天气、新闻、股票等</li>
<li><strong>网页自动化</strong>：自动填表、点击操作、定时签到</li>
<li><strong>内容截图</strong>：把网页截图发给你</li>
<li><strong>爬虫能力</strong>：抓取特定网站内容</li>
</ul>
<hr>
<h2>ARM64 的困境</h2>
<p>市面上的 Chrome 浏览器只有 amd64 架构版本，而很多同学使用 Apple Silicon Mac 或 ARM 服务器（如腾讯云 Lighthouse）。这导致常规的 Chrome 安装方法行不通。</p>
<p>解决方案有两个：</p>
<ol>
<li><strong>使用 Snap Chromium</strong>：Ubuntu 自带，但配置麻烦</li>
<li><strong>使用 Playwright Chromium</strong>：跨平台，兼容 ARM64</li>
</ol>
<p>本文选择方案 2，因为它更可控。</p>
<hr>
<h2>技术栈</h2>
<table>
<thead>
<tr>
<th>组件</th>
<th>作用</th>
<th>备注</th>
</tr>
</thead>
<tbody><tr>
<td>Playwright</td>
<td>浏览器自动化框架</td>
<td>提供 Chromium 二进制</td>
</tr>
<tr>
<td>Chromium</td>
<td>无头浏览器</td>
<td>ARM64 兼容版本</td>
</tr>
<tr>
<td>CDP (Chrome DevTools Protocol)</td>
<td>浏览器调试协议</td>
<td>OpenClaw 通过它控制浏览器</td>
</tr>
</tbody></table>
<hr>
<h2>完整配置步骤</h2>
<h3>1. 安装 Playwright Chromium</h3>
<pre><code class="language-bash">npx playwright install chromium</code></pre><p>Playwright 会自动下载适配当前架构的 Chromium。</p>
<h3>2. 安装中文字体</h3>
<p>无头浏览器截图中文字体需要单独安装：</p>
<pre><code class="language-bash">sudo apt-get install -y fonts-wqy-microhei fonts-wqy-zenhei
fc-cache -fv</code></pre><h3>3. 启动浏览器</h3>
<p>我们需要让 Chromium 以 CDP 模式启动：</p>
<pre><code class="language-bash">nohup ~/.cache/ms-playwright/chromium-1208/chrome-linux/chrome \
  --headless=new \
  --no-sandbox \
  --disable-gpu \
  --remote-debugging-port=18800 \
  --disable-dev-shm-usage \
  --disable-software-rasterizer &gt; /tmp/chrome.log 2&gt;&1 &

# 验证启动成功
curl -s http://127.0.0.1:18800/json/version</code></pre><h3>4. 配置 OpenClaw</h3>
<p>编辑 <code>~/.openclaw/openclaw.json</code>，添加/修改 browser 部分：</p>
<pre><code class="language-json">{
  "browser": {
    "enabled": true,
    "attachOnly": false,
    "headless": true,
    "noSandbox": true,
    "profiles": {
      "openclaw": {
        "cdpPort": 18800,
        "color": "0000FF"
      }
    }
  }
}</code></pre><h3>5. 验证配置</h3>
<pre><code class="language-bash">openclaw browser status
openclaw browser screenshot</code></pre><hr>
<h2>进阶使用</h2>
<h3>自动启动脚本</h3>
<p>为了保证服务器重启后浏览器能自动启动，建议添加 systemd 服务：</p>
<pre><code class="language-ini">[Unit]
Description=OpenClaw Headless Browser
After=network.target

[Service]
Type=simple
User=liuyaowen
ExecStart=/home/liuyaowen/.cache/ms-playwright/chromium-1208/chrome-linux/chrome --headless=new --no-sandbox --disable-gpu --remote-debugging-port=18800 --disable-dev-shm-usage
Restart=on-failure

[Install]
WantedBy=multi-user.target</code></pre><h3>浏览器 Profile 持久化</h3>
<p>如果需要保持登录状态，可以指定用户数据目录：</p>
<pre><code class="language-bash">--user-data-dir=/path/to/profile</code></pre><p>这样浏览器会记住 Cookie 和会话，登录一次即可。</p>
<hr>
<h2>常见问题</h2>
<h3>字体仍然乱码</h3>
<p>检查字体是否正确加载：</p>
<pre><code class="language-bash">fc-list :lang=zh</code></pre><p>如果为空，可能需要手动下载字体到 Chrome 可访问的目录。</p>
<h3>CDP 连接失败</h3>
<ol>
<li>检查端口是否被占用：<code>lsof -i:18800</code></li>
<li>查看浏览器日志：<code>cat /tmp/chrome.log</code></li>
<li>尝试重启浏览器</li>
</ol>
<h3>截图空白</h3>
<p>可能是页面还没加载完成，增加等待时间：</p>
<pre><code class="language-javascript">await page.waitForLoadState("networkidle");</code></pre><hr>
<h2>效果展示</h2>
<p>配置完成后，你可以：</p>
<ol>
<li><strong>让 AI 帮你查天气</strong>：直接截图当前天气发给你</li>
<li><strong>自动签到</strong>：每天定时打开网页点击签到按钮</li>
<li><strong>内容监控</strong>：监控某个网页变化后通知你</li>
<li><strong>生成报表</strong>：自动打开管理后台截图生成日报</li>
</ol>
<hr>
<h2>总结</h2>
<p>本文详细介绍了在 ARM64 服务器上为 OpenClaw 配置无头浏览器的完整过程。虽然过程比直接安装 Chrome 复杂一些，但通过 Playwright + CDP 的组合，我们最终实现了：</p>
<ul>
<li>✅ ARM64 兼容</li>
<li>✅ 中文字体支持</li>
<li>✅ OpenClaw 集成</li>
<li>✅ 自动化能力</li>
</ul>
<p>如果你也有在服务器上运行 AI 助手的需求，不妨试试这个方案。</p>
<hr>
<h2>参考资源</h2>
<ul>
<li><a href="https://playwright.dev/">Playwright 官方文档</a></li>
<li><a href="https://docs.openclaw.ai/">OpenClaw 文档</a></li>
<li><a href="https://github.com/mx-space/core">mx-space 项目</a></li>
</ul>

      <p style='text-align: right'>
      <a href='https://liuyaowen.cn/posts/default/openclaw-arm64-1772639270#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">69a8542771c68afbd9286bf3</guid>
  <category>posts</category>
<category>技术</category>
 </item>
  <item>
    <title>xMemory：超越RAG的智能体记忆</title>
    <link>https://liuyaowen.cn/posts/codenotes/xmemory-intelligent-agent-memory-beyond-rag</link>
    <pubDate>Fri, 27 Feb 2026 12:19:50 GMT</pubDate>
    <description>ICML 2025 最佳论文力作：xMemory 能否彻底解决 RAG 的记忆困境？

在大模型 A</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://liuyaowen.cn/posts/codenotes/xmemory-intelligent-agent-memory-beyond-rag'>https://liuyaowen.cn/posts/codenotes/xmemory-intelligent-agent-memory-beyond-rag</a></blockquote>
      <p>ICML 2025 最佳论文力作：xMemory 能否彻底解决 RAG 的记忆困境？</p>
<p>在大模型 Agent 蓬勃发展的今天，如何让 AI 记住跨越数十次会话的对话历史，已成为决定 Agent 实用性的关键因素。当我们与一个陪伴数月的 AI 助手交流时，我们希望它记得我们之前讨论过的项目偏好、了解我们的工作习惯、甚至记住某次聊天中提到的关键细节。然而，标准的检索增强生成（RAG）方法在面对这种「智能体记忆」场景时，却暴露出惊人的局限性。</p>
<h2>被忽视的根本问题：RAG 与智能体记忆的「基因不兼容」</h2>
<p>在深入 xMemory 之前，我们有必要先理解这场变革的核心背景。传统 RAG 的设计初衷是什么？它是为了在海量的互联网文档、企业的知识库、或者数百篇研究论文中进行检索而生的。在这些场景中，被检索的文本片段来自不同来源，彼此之间往往主题各异、视角多元。正因为如此，传统的「top-k 相似度检索」能够有效地从大量异质候选项中筛选出与当前查询最相关的不同信息。</p>
<p>但智能体记忆的场景与上述情况截然不同。当一个用户与 Agent 进行跨越数周甚至数月的多轮对话时，存储的记忆呈现出一种独特的结构：高度相关、高度冗余、并且在时间维度上紧密相连。举例来说，如果用户在一个月内多次讨论同一个项目，那么关于这个项目的所有对话片段在语义空间中会形成一团密集的「云」—— 它们彼此之间的相似度极高，但与用户当前查询相关的核心信息可能只占这团密云中的几个点。</p>
<p>这带来一个尴尬的局面：当使用标准的 top-5 或 top-10 检索时，返回的结果往往是同一个主题下的近重复片段。换句话说，RAG 系统辛辛苦苦找到的「最相关」上下文，其实只是在反复讲述同一件事，而真正需要的前后时间关联的证据链，反而被算法当作冗余内容「优化」掉了。</p>
<p>xMemory 论文的核心洞察正是这一点：标准 RAG 假设的是「异构文本库」，而智能体记忆本质上是「同构记忆流」。这种根本性的不匹配，不是简单的调参所能解决的。</p>
<h2>xMemory 的核心思路：解耦与聚合</h2>
<p>面对这一挑战，xMemory 提出了一个优雅而深刻的解决方案——「解耦到聚合」（Decoupling-then-Aggregation）。这个理念的核心思想是：与其在原始对话片段的层面挣扎，不如先对记忆进行结构化组织，然后再反转这个结构来驱动检索。</p>
<p>具体而言，xMemory 构建了一个四级层次记忆结构：</p>
<ul>
<li><strong>原始消息（Messages）</strong>：用户与 Agent 之间的实际对话轮次</li>
<li><strong>会话片段（Episodes）</strong>：将连续的多轮对话压缩为概括性的摘要，捕捉一个完整的话题单元</li>
<li><strong>语义单元（Semantics）</strong>：从会话片段中提取的可重用的长期事实，比如用户的姓名、工作单位、偏好习惯等</li>
<li><strong>主题（Themes）</strong>：将相关的语义单元组织在一起，形成更高层次的概念聚合</li>
</ul>
<p>这个层次结构的妙处在于，它将原本混杂在时间流中的信息进行了「解耦」——语义单元被单独提取出来，不再与原始对话轮次绑定；同时通过主题层的「聚合」，建立了语义单元之间的高层关联。这就像是把一滩浑浊的河水先进行沉淀和分层，然后再按需取用。</p>
<h2>两阶段自适应检索：代表选择与不确定性感知</h2>
<p>仅仅构建出层次结构还不够，关键是如何在这个结构上进行高效的检索。xMemory 采用了两阶段的自适应检索策略，正是我认为这篇论文最精彩的部分。</p>
<h3>第一阶段：kNN 图上的查询感知代表选择</h3>
<p>在主题层和语义单元层，xMemory 维护了一个 k 最近邻（kNN）图结构。当用户提出一个查询时，系统不是直接去原始对话中找最相似的片段，而是先在主题层进行「代表选择」——从多个相关主题中挑选出最能覆盖不同知识方向的代表节点。</p>
<p>这里的关键创新在于一个权衡公式：</p>
<pre><code class="language-">i* = argmax [α × 覆盖增益 + (1-α) × 查询相关性]</code></pre><p>系统会平衡两个目标：一方面要选择与当前查询语义相关的内容，另一方面要确保选中的内容能够覆盖记忆中的不同知识面，避免总是扎堆在某个密集区域。这个过程是迭代的——每选择一个节点，就更新其邻居的覆盖状态，直到达到某个覆盖率阈值。</p>
<h3>第二阶段：不确定性自适应的证据包含</h3>
<p>选中了相关的语义单元之后，下一步是决定要包含哪些具体的会话片段来作为「证据」。xMemory 的做法非常巧妙：它不是一股脑地把所有相关片段都塞进上下文窗口，而是基于「不确定性减少」的原则来进行筛选。</p>
<p>具体实现方式是：对于每个候选的会话片段，系统会评估它是否能减少语言模型对答案的预测不确定性。只有当额外包含某个片段能够显著降低答案的不确定性时，这个片段才会被纳入最终的上下文。这种方法天然地实现了「按需获取」的效果——与答案直接相关的细节会被保留，而冗余的近重复内容会被过滤掉。</p>
<h2>实验结果：效率与质量的双赢</h2>
<p>xMemory 在两个基准数据集上进行了充分验证：LoCoMo（多会话对话数据集，包含平均约 300 轮对话）和 PerLTQA（个人长期记忆问答）。</p>
<p>实验结果令人印象深刻：</p>
<ul>
<li>在 Qwen3-8B 模型上，xMemory 将平均 BLEU 从 28.51 提升到 34.48，F1 从 40.45 提升到 43.98</li>
<li>在 GPT-5 nano 上，同样取得了从 36.65 到 38.71（BLEU）和 48.17 到 50.00（F1）的提升</li>
<li>最令人惊叹的是 token 使用效率：在 Qwen3-8B 上，token 消耗从 9103 降至 4711，减少了近一半！</li>
</ul>
<p>这意味着 xMemory 不仅提高了答案质量，还同时降低了成本。对于需要长期运行、成本敏感的 Agent 应用来说，这无疑是一个极具吸引力的特性。</p>
<h2>更深层的启示</h2>
<p>读罢这篇论文，我不禁思考它对整个 Agent 领域的更深层启示。</p>
<p>首先，它揭示了一个重要的设计原则：「记忆的组织方式决定了检索的效率」。当我们设计 Agent 的记忆系统时，不应该仅仅关注如何存储和索引，更应该思考如何将知识「解耦」——把长期有效的通用事实与短期有效的对话上下文分离，把核心概念与细枝末节分离。这种分离本身就是一种智能。</p>
<p>其次，xMemory 的「两阶段检索」也暗示了一个更广泛的设计模式：用粗粒度的代表选择来控制搜索空间，然后再用精细的不确定性评估来筛选内容。这种「先粗后精」的策略，在许多需要高效利用有限资源的场景中都具有普适价值。</p>
<p>最后，这篇论文也提醒我们重新审视 RAG 的边界。RAG 是一个强大的范式，但它并非万能。当应用场景的特性与 RAG 的原始假设不匹配时，我们需要勇敢地进行定制化甚至重构，而不是削足适履地硬套。</p>
<h2>结语</h2>
<p>ICML 2025 的这篇论文，为智能体记忆这个前沿领域注入了一剂强心针。xMemory 用「解耦-聚合」的层次化思路和「代表选择-不确定性感知」的两阶段检索，成功地化解了标准 RAG 在同构记忆流场景下的困境。更难能可贵的是，它在提升答案质量的同时，还显著降低了 token 成本——这对于任何关注实际部署的工程师来说，都是无法忽视的诱惑。</p>
<p>如果你正在构建需要长期记忆的 AI Agent，或者对检索增强技术有更深的兴趣，这篇论文绝对值得一读。它不仅提供了一套可落地的技术方案，更重要的是，它展示了一种思考问题的方式——当你发现现有的方法论与问题本质不匹配时，不妨回到问题的源头，重新设计整个系统。</p>

      <p style='text-align: right'>
      <a href='https://liuyaowen.cn/posts/codenotes/xmemory-intelligent-agent-memory-beyond-rag#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">69a18be6685a5f2c75fab0e1</guid>
  <category>posts</category>
<category>笔记</category>
 </item>
  <item>
    <title>认知红利：工作前几年如何真正拉开差距</title>
    <link>https://liuyaowen.cn/posts/default/20260123</link>
    <pubDate>Mon, 23 Feb 2026 07:29:57 GMT</pubDate>
    <description>在职场早期，很多人习惯用“多加班、多做项目、多报培训”来证明努力。但这些本质上都属于线性努力——当你</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://liuyaowen.cn/posts/default/20260123'>https://liuyaowen.cn/posts/default/20260123</a></blockquote>
      <p>在职场早期，很多人习惯用“多加班、多做项目、多报培训”来证明努力。但这些本质上都属于线性努力——当你与他人处在同一维度竞争时，结果只会出现有限差距。<br>真正能够拉开差距的，往往是认知升级带来的非线性成长。</p>
<p>结合实践经验，可以将关键认知总结为四个方面。</p>
<hr>
<h2>一、决策质量：成长的放大器</h2>
<p>职场中的关键节点——是否跳槽、是否转行、选择城市、选择公司、是否创业——本质上都是决策问题。<br>一旦决策正确，其增益往往远大于日常努力。</p>
<h3>1.1 要什么不重要，不要什么更重要</h3>
<p>很多人误以为“选择越多越好”，希望主业、副业、投资同时推进。但人的注意力与心力是有限资源，分散意味着难以形成突破。<br>在上升期，更有效的策略是：阶段性聚焦，一个阶段只解决一个核心目标。</p>
<h3>1.2 少做决策，做对决策</h3>
<p>决策并非越多越好。频繁决策往往伴随高错误率，并容易陷入连锁反应。例如跳槽失误后急于离开，容易在焦虑中再次做出错误选择，形成循环。<br>因此，更优策略是：把自己放在稳定环境中，用时间换取更高质量的关键决策。</p>
<h3>1.3 决策慢，是为了方向正确</h3>
<p>重大决策前需要刻意设置“冷却期”，系统收集信息并推演后果。<br>典型方法包括：</p>
<ul>
<li>向真实经历者咨询一手经验  </li>
<li>评估成功概率与失败成本  </li>
<li>判断新选择是否真正解决当前问题</li>
</ul>
<p>方向正确时，慢反而是最快的路径；方向错误时，速度只会放大损失。</p>
<hr>
<h2>二、高目标与高底线：避免“及格主义陷阱”</h2>
<p>脱离考试体系后，绝大多数人的工作标准完全由自我设定，容易滑向“完成任务即可”的交差逻辑。<br>这种及格主义会逐渐降低个人底线，最终形成能力天花板。</p>
<h3>2.1 从“交付任务”到“理解目标”</h3>
<p>真正的高标准工作方式是：</p>
<ul>
<li>理解任务背后的业务目标  </li>
<li>主动思考更优方案  </li>
<li>用结果价值而非动作完成度衡量自己</li>
</ul>
<h3>2.2 用标杆替代自我感动</h3>
<p>一个有效策略是：为自己设置行业标杆。<br>持续对齐行业优秀个体的标准，可以显著抬高自己的质量基线。当接近或超越当前标杆时，再引入新的标杆，形成阶梯式成长。</p>
<hr>
<h2>三、风险偏好：差距往往诞生于此</h2>
<p>在同等起点下，不同发展结果往往来自风险选择差异。<br>多数人倾向保守，追求确定性；但竞争优势往往隐藏在他人不敢投入的机会区间。</p>
<h3>3.1 风险的本质是机会筛选器</h3>
<p>组织或大公司通常不会重仓高不确定性但潜在高回报的机会，这为个人提供了竞争空间。<br>适度承担风险，本质是在更低竞争密度的赛道中获取成长。</p>
<h3>3.2 失败并非负收益</h3>
<p>对具备复盘能力的人而言，失败具有长期正向价值：</p>
<ul>
<li>提供认知样本  </li>
<li>修正决策模型  </li>
<li>提升风险评估能力</li>
</ul>
<p>人生本身具备较强纠错能力，一次失败通常难以造成不可逆后果。</p>
<hr>
<h2>四、主体性：从受害者叙事到主角叙事</h2>
<p>主体性决定了一个人如何解释世界，也决定了其行动边界。</p>
<h3>4.1 主体性弱的典型表现</h3>
<ul>
<li>将结果归因于环境、关系或运气  </li>
<li>对外部评价高度依赖  </li>
<li>容易陷入情绪内耗与自我否定</li>
</ul>
<p>这种状态会削弱行动力，并持续降低成长预期。</p>
<h3>4.2 主体性强的核心特征</h3>
<ul>
<li>建立自我评价体系  </li>
<li>将一切经历视为学习样本  </li>
<li>对外部评价保持参考而非依赖</li>
</ul>
<p>换句话说，是以“主角心态”理解世界：发生的事情都是素材，而非束缚。</p>
<hr>
<h2>认知升级，才是早期最大的杠杆</h2>
<p>工作前几年真正拉开差距的，并非投入时长，而是：</p>
<ul>
<li>用高质量决策放大努力  </li>
<li>用高目标抬升能力下限  </li>
<li>用风险选择获取非对称机会  </li>
<li>用主体性构建稳定内核</li>
</ul>
<p>努力决定下限，认知决定上限。当认知结构发生变化时，同样的努力会产生完全不同的结果。</p>

      <p style='text-align: right'>
      <a href='https://liuyaowen.cn/posts/default/20260123#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">699c01f5965c9595cc1fa6d4</guid>
  <category>posts</category>
<category>技术</category>
 </item>
  <item>
    <title>AT 分布式事务使用详解</title>
    <link>https://liuyaowen.cn/posts/default/20251217</link>
    <pubDate>Wed, 17 Dec 2025 12:30:08 GMT</pubDate>
    <description>一、AT 模式概述

AT (Automatic Transaction) 模式是 Seata 中最</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://liuyaowen.cn/posts/default/20251217'>https://liuyaowen.cn/posts/default/20251217</a></blockquote>
      <h2>一、AT 模式概述</h2>
<p>AT (Automatic Transaction) 模式是 Seata 中最常用的分布式事务模式，它通过自动生成反向 SQL 来实现事务的回滚，对业务代码侵入性极小。</p>
<h2>二、核心配置详解</h2>
<h3>1. 全局配置 (registry.conf)</h3>
<pre><code class="language-properties"># 注册中心配置
registry {
  type = "nacos"  # 支持 nacos、eureka、redis、zk、consul、etcd3、sofa
  
  nacos {
    application = "seata-server"
    serverAddr = "127.0.0.1:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
}

# 配置中心
config {
  type = "nacos"
  
  nacos {
    serverAddr = "127.0.0.1:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
    dataId = "seata-server.properties"
  }
}</code></pre><h3>2. 客户端配置 (application.yml)</h3>
<pre><code class="language-yaml">seata:
  enabled: true
  application-id: order-service  # 应用唯一标识
  tx-service-group: my_test_tx_group  # 事务组名称
  
  # 自动数据源代理
  enable-auto-data-source-proxy: true
  data-source-proxy-mode: AT  # AT 模式
  
  # 客户端配置
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace: ""
      data-id: seata-client.properties
  
  # 注册中心配置
  registry:
    type: nacos
    nacos:
      application: seata-server
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      namespace: ""
      cluster: default
  
  # 客户端详细配置
  client:
    rm:
      # 异步提交缓存队列长度
      async-commit-buffer-limit: 10000
      # 一阶段结果上报 TC 重试次数
      report-retry-count: 5
      # 自动刷新缓存中的表结构
      table-meta-check-enable: true
      # 分支事务与其它全局回滚事务冲突时锁策略
      report-success-enable: false
      # 是否上报一阶段成功
      saga-branch-register-enable: false
      # saga json parser
      saga-json-parser: fastjson
      # 一阶段全局提交结果上报 TC 重试次数
      saga-retry-persist-mode-update: false
      # 默认false，ture会提升性能
      saga-compensate-persist-mode-update: false
      # TCC 资源自动清理时间（小时）
      tcc-action-interceptor-order: -2147482648
      
    tm:
      # 一阶段全局提交结果上报 TC 重试次数
      commit-retry-count: 5
      # 一阶段全局回滚结果上报 TC 重试次数
      rollback-retry-count: 5
      # 默认全局事务超时时间（毫秒）
      default-global-transaction-timeout: 60000
      # 降级开关，默认 false
      degrade-check: false
      # 服务自检周期（毫秒）
      degrade-check-period: 2000
      # 允许降级检查的最小业务并发数
      degrade-check-allow-times: 10
      # 自检失败后开启降级的持续时间（毫秒）
      interceptor-order: -2147482648
      
    undo:
      # 是否开启二阶段回滚镜像校验
      data-validation: true
      # 二阶段回滚镜像校验失败的处理方式
      log-serialization: jackson
      # undo 序列化方式：jackson、fastjson、kryo
      log-table: undo_log
      # 自定义 undo 表名
      only-care-update-columns: true
      # 是否只生成被更新字段的镜像
      compress:
        enable: true
        # 是否压缩 undo_log
        type: zip
        # 压缩类型
        threshold: 64k
        # 压缩阈值
    
    # 负载均衡配置
    load-balance:
      type: RandomLoadBalance  # 负载均衡类型
      virtual-nodes: 10  # 虚拟节点数</code></pre><h3>3. 服务端配置 (Seata Server)</h3>
<pre><code class="language-properties"># 存储模式
store.mode=db  # file、db、redis

# 数据库存储配置
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useSSL=false
store.db.user=root
store.db.password=root
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

# 事务、日志存储配置
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000</code></pre><h2>三、使用示例</h2>
<h3>1. 数据库准备</h3>
<p>每个业务库都需要创建 undo_log 表：</p>
<pre><code class="language-sql">CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;</code></pre><h3>2. 业务代码</h3>
<pre><code class="language-java">@Service
public class OrderServiceImpl implements OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private AccountService accountService;
    
    @Autowired
    private StorageService storageService;
    
    /**
     * 全局事务发起者，使用 @GlobalTransactional 注解
     */
    @Override
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(Order order) {
        // 1. 创建订单
        orderMapper.insert(order);
        
        // 2. 扣减库存（远程调用）
        storageService.deduct(order.getProductId(), order.getCount());
        
        // 3. 扣减账户余额（远程调用）
        accountService.debit(order.getUserId(), order.getMoney());
        
        // 4. 更新订单状态
        order.setStatus(1);
        orderMapper.update(order);
    }
}

// 库存服务
@Service
public class StorageServiceImpl implements StorageService {
    
    @Autowired
    private StorageMapper storageMapper;
    
    @Override
    public void deduct(Long productId, Integer count) {
        // 直接执行业务逻辑，无需额外事务注解
        storageMapper.deduct(productId, count);
    }
}</code></pre><h2>四、底层实现原理</h2>
<h3>1. 核心组件架构</h3>
<pre><code class="language-">TC (Transaction Coordinator) - 事务协调器
├── 全局事务管理
├── 分支事务管理
└── 全局锁管理

TM (Transaction Manager) - 事务管理器
├── 全局事务开启
├── 全局事务提交
└── 全局事务回滚

RM (Resource Manager) - 资源管理器
├── 分支事务注册
├── 分支事务上报
└── 分支事务提交/回滚</code></pre><h3>2. AT 模式执行流程</h3>
<h4>第一阶段（执行业务 SQL）</h4>
<pre><code class="language-java">// DataSourceProxy 代理数据源
public class DataSourceProxy extends AbstractDataSourceProxy {
    
    @Override
    public ConnectionProxy getConnection() throws SQLException {
        Connection targetConnection = targetDataSource.getConnection();
        return new ConnectionProxy(this, targetConnection);
    }
}

// ConnectionProxy 核心逻辑
public class ConnectionProxy extends AbstractConnectionProxy {
    
    @Override
    public void commit() throws SQLException {
        try {
            // 注册分支事务
            register();
        } catch (TransactionException e) {
            // 识别并处理异常
            recognizeLockKeyConflictException(e, context.buildLockKeys());
        }
        
        try {
            // 执行本地事务提交
            targetConnection.commit();
        } catch (Throwable ex) {
            // 上报事务执行失败
            report(false);
            throw ex;
        }
        
        // 上报事务执行成功
        report(true);
    }
}

// ExecuteTemplate 执行模板
public class ExecuteTemplate {
    
    public static &lt;T, S extends Statement&gt; T execute(
            SQLRecognizer sqlRecognizer,
            StatementProxy<s> statementProxy,
            StatementCallback&lt;T, S&gt; statementCallback,
            Object... args) throws SQLException {
        
        // 1. 前置镜像：查询修改前的数据
        TableRecords beforeImage = buildBeforeImage(statementProxy, sqlRecognizer);
        
        // 2. 执行业务 SQL
        T result = statementCallback.execute(statementProxy.getTargetStatement(), args);
        
        // 3. 后置镜像：查询修改后的数据
        TableRecords afterImage = buildAfterImage(statementProxy, sqlRecognizer);
        
        // 4. 构造 undo_log
        prepareUndoLog(beforeImage, afterImage);
        
        return result;
    }
}</code></pre><h4>核心数据结构</h4>
<pre><code class="language-java">// 前置镜像和后置镜像数据结构
public class TableRecords {
    private TableMeta tableMeta;
    private List&lt;Row&gt; rows;
    
    // 行数据
    public static class Row {
        private List&lt;Field&gt; fields;
        
        public static class Field {
            private String name;        // 字段名
            private int keyType;        // 主键类型
            private Object value;       // 字段值
        }
    }
}

// Undo Log 结构
public class BranchUndoLog {
    private String xid;                    // 全局事务ID
    private long branchId;                 // 分支事务ID
    private List&lt;SQLUndoLog&gt; sqlUndoLogs;  // SQL回滚日志
    
    public static class SQLUndoLog {
        private String sqlType;            // INSERT/UPDATE/DELETE
        private String tableName;          // 表名
        private TableRecords beforeImage;  // 前置镜像
        private TableRecords afterImage;   // 后置镜像
    }
}</code></pre><h4>第二阶段（提交或回滚）</h4>
<p><strong>提交流程：</strong></p>
<pre><code class="language-java">public class AsyncWorker implements ResourceManagerInbound {
    
    /**
     * 异步提交分支事务
     */
    public BranchStatus branchCommit(String xid, long branchId, 
                                     String resourceId) {
        // AT 模式下，一阶段已经提交，二阶段只需删除 undo_log
        return asyncCommit(xid, branchId, resourceId);
    }
    
    private BranchStatus asyncCommit(String xid, long branchId, 
                                     String resourceId) {
        // 加入异步删除队列
        addToCommitQueue(xid, branchId, resourceId);
        return BranchStatus.PhaseTwo_Committed;
    }
    
    // 异步删除 undo_log
    private void deleteUndoLog(String xid, long branchId) {
        String sql = "DELETE FROM undo_log WHERE xid = ? AND branch_id = ?";
        executeUpdate(sql, xid, branchId);
    }
}</code></pre><p><strong>回滚流程：</strong></p>
<pre><code class="language-java">public class UndoLogManager {
    
    /**
     * 回滚分支事务
     */
    public void undo(DataSourceProxy dataSourceProxy, String xid, 
                     long branchId) throws SQLException {
        
        Connection conn = dataSourceProxy.getConnection();
        
        try {
            // 1. 查询 undo_log
            String selectSQL = "SELECT * FROM undo_log WHERE xid = ? " +
                             "AND branch_id = ? FOR UPDATE";
            BranchUndoLog branchUndoLog = selectUndoLog(conn, xid, branchId);
            
            if (branchUndoLog == null) {
                return;  // 已经回滚或提交
            }
            
            // 2. 数据校验
            if (!dataValidation(conn, branchUndoLog)) {
                throw new SQLException("Data validation failed");
            }
            
            // 3. 生成反向 SQL 并执行
            for (SQLUndoLog sqlUndoLog : branchUndoLog.getSqlUndoLogs()) {
                AbstractUndoExecutor undoExecutor = 
                    UndoExecutorFactory.getUndoExecutor(
                        dataSourceProxy.getDbType(), sqlUndoLog);
                undoExecutor.executeOn(conn);
            }
            
            // 4. 删除 undo_log
            String deleteSQL = "DELETE FROM undo_log WHERE xid = ? " +
                             "AND branch_id = ?";
            executeUpdate(conn, deleteSQL, xid, branchId);
            
            conn.commit();
            
        } catch (Exception e) {
            conn.rollback();
            throw e;
        }
    }
    
    /**
     * 数据校验：比对当前数据和后置镜像
     */
    private boolean dataValidation(Connection conn, 
                                   BranchUndoLog branchUndoLog) {
        for (SQLUndoLog sqlUndoLog : branchUndoLog.getSqlUndoLogs()) {
            TableRecords afterImage = sqlUndoLog.getAfterImage();
            TableRecords currentRecords = queryCurrentRecords(conn, afterImage);
            
            // 比对数据是否一致
            if (!afterImage.equals(currentRecords)) {
                return false;  // 数据被脏写
            }
        }
        return true;
    }
}</code></pre><h4>反向 SQL 生成逻辑</h4>
<pre><code class="language-java">// UPDATE 反向 SQL
public class MySQLUndoUpdateExecutor extends AbstractUndoExecutor {
    
    @Override
    protected String buildUndoSQL() {
        TableRecords beforeImage = sqlUndoLog.getBeforeImage();
        
        // 根据前置镜像生成 UPDATE 语句
        StringBuilder sql = new StringBuilder("UPDATE ");
        sql.append(sqlUndoLog.getTableName()).append(" SET ");
        
        // 设置字段值为前置镜像的值
        List&lt;Field&gt; fields = beforeImage.getRows().get(0).getFields();
        for (int i = 0; i &lt; fields.size(); i++) {
            if (i &gt; 0) sql.append(", ");
            sql.append(fields.get(i).getName()).append(" = ?");
        }
        
        // WHERE 条件（主键）
        sql.append(" WHERE ");
        appendWhereCondition(sql, beforeImage);
        
        return sql.toString();
    }
}

// DELETE 反向 SQL（生成 INSERT）
public class MySQLUndoDeleteExecutor extends AbstractUndoExecutor {
    
    @Override
    protected String buildUndoSQL() {
        TableRecords beforeImage = sqlUndoLog.getBeforeImage();
        
        // 根据前置镜像生成 INSERT 语句
        StringBuilder sql = new StringBuilder("INSERT INTO ");
        sql.append(sqlUndoLog.getTableName()).append(" (");
        
        // 字段列表
        List&lt;Field&gt; fields = beforeImage.getRows().get(0).getFields();
        for (int i = 0; i &lt; fields.size(); i++) {
            if (i &gt; 0) sql.append(", ");
            sql.append(fields.get(i).getName());
        }
        
        sql.append(") VALUES (");
        for (int i = 0; i &lt; fields.size(); i++) {
            if (i &gt; 0) sql.append(", ");
            sql.append("?");
        }
        sql.append(")");
        
        return sql.toString();
    }
}

// INSERT 反向 SQL（生成 DELETE）
public class MySQLUndoInsertExecutor extends AbstractUndoExecutor {
    
    @Override
    protected String buildUndoSQL() {
        TableRecords afterImage = sqlUndoLog.getAfterImage();
        
        // 根据后置镜像生成 DELETE 语句
        StringBuilder sql = new StringBuilder("DELETE FROM ");
        sql.append(sqlUndoLog.getTableName());
        sql.append(" WHERE ");
        
        appendWhereCondition(sql, afterImage);
        
        return sql.toString();
    }
}</code></pre><h3>3. 全局锁机制</h3>
<pre><code class="language-java">public class LockManagerImpl implements LockManager {
    
    /**
     * 获取全局锁
     */
    public boolean acquireLock(List&lt;RowLock&gt; rowLocks) {
        // 锁的粒度：表名 + 主键值
        // 例如：order_table:1,2,3
        String lockKey = buildLockKey(rowLocks);
        
        // 向 TC 申请全局锁
        boolean result = transactionCoordinator.acquireLock(
            xid, branchId, resourceId, lockKey);
        
        if (!result) {
            // 获取锁失败，进入重试逻辑
            return retryAcquireLock(rowLocks);
        }
        
        return true;
    }
    
    private String buildLockKey(List&lt;RowLock&gt; rowLocks) {
        // 格式：table1:pk1,pk2;table2:pk3,pk4
        Map&lt;String, Set&lt;String&gt;&gt; lockMap = new HashMap&lt;&gt;();
        
        for (RowLock rowLock : rowLocks) {
            lockMap.computeIfAbsent(
                rowLock.getTableName(), 
                k -&gt; new HashSet&lt;&gt;()
            ).add(rowLock.getPk());
        }
        
        return lockMap.entrySet().stream()
            .map(e -&gt; e.getKey() + ":" + String.join(",", e.getValue()))
            .collect(Collectors.joining(";"));
    }
}</code></pre><h2>五、性能优化配置</h2>
<pre><code class="language-yaml">seata:
  client:
    rm:
      # 异步提交优化
      async-commit-buffer-limit: 10000
      
    undo:
      # 只记录更新字段
      only-care-update-columns: true
      
      # 压缩 undo_log
      compress:
        enable: true
        type: zip
        threshold: 64k</code></pre><h2>六、常见问题</h2>
<ol>
<li><strong>脏写问题</strong>：通过全局锁和数据校验解决</li>
<li><strong>性能问题</strong>：使用异步提交、undo_log 压缩</li>
<li><strong>undo_log 膨胀</strong>：定期清理历史数据</li>
</ol>
<pre><code class="language-sql">-- 清理 7 天前的 undo_log
DELETE FROM undo_log 
WHERE log_created &lt; DATE_SUB(NOW(), INTERVAL 7 DAY);</code></pre><p>AT 模式通过自动生成前后镜像和反向 SQL，实现了对业务代码零侵入的分布式事务解决方案，是 Seata 最推荐使用的模式。​​​​​​​​​​​​​​​​</p>

      <p style='text-align: right'>
      <a href='https://liuyaowen.cn/posts/default/20251217#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">6942a25026b72f4e551588c5</guid>
  <category>posts</category>
<category>技术</category>
 </item>
  <item>
    <title>Koupleless 合并部署完整教程</title>
    <link>https://liuyaowen.cn/posts/default/20251216</link>
    <pubDate>Tue, 16 Dec 2025 13:10:56 GMT</pubDate>
    <description>目录

Koupleless 简介
合并部署概述
环境准备
基座改造
模块改造
模块瘦身
部署与验证</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://liuyaowen.cn/posts/default/20251216'>https://liuyaowen.cn/posts/default/20251216</a></blockquote>
      <h2>目录</h2>
<ol>
<li><a href="#koupleless-%E7%AE%80%E4%BB%8B">Koupleless 简介</a></li>
<li><a href="#%E5%90%88%E5%B9%B6%E9%83%A8%E7%BD%B2%E6%A6%82%E8%BF%B0">合并部署概述</a></li>
<li><a href="#%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87">环境准备</a></li>
<li><a href="#%E5%9F%BA%E5%BA%A7%E6%94%B9%E9%80%A0">基座改造</a></li>
<li><a href="#%E6%A8%A1%E5%9D%97%E6%94%B9%E9%80%A0">模块改造</a></li>
<li><a href="#%E6%A8%A1%E5%9D%97%E7%98%A6%E8%BA%AB">模块瘦身</a></li>
<li><a href="#%E9%83%A8%E7%BD%B2%E4%B8%8E%E9%AA%8C%E8%AF%81">部署与验证</a></li>
<li><a href="#%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98%E4%B8%8E%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88">常见问题与解决方案</a></li>
<li><a href="#%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5">最佳实践</a></li>
<li><a href="#%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E9%83%A8%E7%BD%B2">生产环境部署</a></li>
</ol>
<hr>
<h2>Koupleless 简介</h2>
<p>Koupleless 是一种模块化的 Serverless 技术解决方案，它能让普通应用以较低的代价演进为 Serverless 研发模式，让代码与资源解耦，轻松独立维护，同时支持秒级构建部署、合并部署、动态伸缩等能力，为用户提供极致的研发运维体验。</p>
<h3>适用场景</h3>
<ol>
<li><strong>应用构建发布慢或 SDK 升级繁琐</strong>：传统应用构建发布需要 6-10 分钟，使用 Koupless 可降至 10 秒级</li>
<li><strong>长尾应用资源浪费</strong>：企业 80% 的应用 CPU 使用率低于 10%，合并部署可显著降低资源成本</li>
<li><strong>研发协作效率低</strong>：多人开发一个应用时，需要统一时间窗口发布， Koupless 支持模块独立迭代</li>
<li><strong>中台应用难以沉淀业务资产</strong>：通过基座沉淀公共能力，模块实现具体业务逻辑</li>
<li><strong>微服务演进成本高</strong>：支持应用在单体、模块化、微服务架构间平滑过渡</li>
</ol>
<hr>
<h2>合并部署概述</h2>
<h3>什么是合并部署</h3>
<p>合并部署是指将多个独立的应用（在 Koupless 中称为&quot;模块&quot;）部署到同一个 JVM 进程中，共享基座的资源和依赖，但保持代码和运行时的隔离。</p>
<h3>合并部署的优势</h3>
<ul>
<li><strong>节省资源</strong>：多个模块共享基座的内存（Metaspace 和 Heap），CPU 使用率有效提升</li>
<li><strong>快速启动</strong>：模块构建产物从数百 MB 瘦身到几十 MB，启动时间大幅缩短</li>
<li><strong>简化运维</strong>：统一管理多个模块，降低维护成本</li>
<li><strong>灵活扩展</strong>：支持动态添加、移除模块，无需重启基座</li>
</ul>
<h3>架构原理</h3>
<p>Koupleless 基于 SOFAArk 框架实现类隔离和合并部署：</p>
<ul>
<li><strong>基座（Base）</strong>：提供公共依赖和基础能力，如 SpringBoot 框架、中间件 SDK 等</li>
<li><strong>模块（Biz）</strong>：业务功能模块，依赖基座提供的公共能力</li>
<li><strong>类加载机制</strong>：模块优先从自己的 ClassLoader 查找类，找不到再委托给基座 ClassLoader</li>
</ul>
<hr>
<h2>环境准备</h2>
<h3>前置条件</h3>
<ul>
<li>JDK 8 或更高版本</li>
<li>Maven 3.6+</li>
<li>SpringBoot 2.x 或 3.x</li>
<li>可用的 IDE（IntelliJ IDEA、Eclipse 等）</li>
</ul>
<h3>版本选择</h3>
<p>当前 Koupleless 主要版本：</p>
<pre><code class="language-xml">&lt;koupleless.runtime.version&gt;0.5.6&lt;/koupleless.runtime.version&gt;
&lt;sofa.ark.version&gt;2.2.14&lt;/sofa.ark.version&gt;</code></pre><h3>工具准备</h3>
<p>下载 Arkctl 工具（用于模块部署）：</p>
<pre><code class="language-bash"># Mac/Linux
wget https://github.com/koupleless/koupleless/releases/download/arkctl-release-0.2.0/arkctl-darwin-amd64
mv arkctl-darwin-amd64 /usr/local/bin/arkctl
chmod +x /usr/local/bin/arkctl

# Linux
wget https://github.com/koupleless/koupleless/releases/download/arkctl-release-0.2.0/arkctl-linux-amd64
mv arkctl-linux-amd64 /usr/local/bin/arkctl
chmod +x /usr/local/bin/arkctl</code></pre><hr>
<h2>基座改造</h2>
<h3>1. 添加依赖</h3>
<p>在基座的 <code>pom.xml</code> 中添加 Koupleless 基座依赖：</p>
<pre><code class="language-xml">&lt;properties&gt;
    &lt;koupleless.runtime.version&gt;0.5.6&lt;/koupleless.runtime.version&gt;
    &lt;sofa.ark.version&gt;2.2.14&lt;/sofa.ark.version&gt;
&lt;/properties&gt;

&lt;dependencies&gt;
    
    &lt;dependency&gt;
        &lt;groupId&gt;com.alipay.sofa.koupleless&lt;/groupId&gt;
        &lt;artifactId&gt;koupleless-base-starter&lt;/artifactId&gt;
        &lt;version&gt;${koupleless.runtime.version}&lt;/version&gt;
    &lt;/dependency&gt;
    
    
    &lt;dependency&gt;
        &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
        &lt;artifactId&gt;web-ark-plugin&lt;/artifactId&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;</code></pre><h3>2. 配置应用名</h3>
<p>在 <code>application.properties</code> 或 <code>application.yml</code> 中配置应用名：</p>
<pre><code class="language-properties"># application.properties
spring.application.name=base-application</code></pre><h3>3. 配置基座构建插件</h3>
<p>在 <code>pom.xml</code> 的 <code>build</code> 部分添加基座构建插件：</p>
<pre><code class="language-xml">&lt;build&gt;
    &lt;plugins&gt;
        
        &lt;plugin&gt;
            &lt;groupId&gt;com.alipay.sofa.koupleless&lt;/groupId&gt;
            &lt;artifactId&gt;koupleless-base-build-plugin&lt;/artifactId&gt;
            &lt;version&gt;${koupleless.runtime.version}&lt;/version&gt;
            &lt;executions&gt;
                &lt;execution&gt;
                    &lt;goals&gt;
                        &lt;goal&gt;add-patch&lt;/goal&gt;
                    &lt;/goals&gt;
                &lt;/execution&gt;
            &lt;/executions&gt;
            &lt;configuration&gt;
                
                &lt;dependencyArtifactId&gt;${baseAppName}-dependencies-starter&lt;/dependencyArtifactId&gt;
                
                &lt;dependencyVersion&gt;0.0.1-SNAPSHOT&lt;/dependencyVersion&gt;
            &lt;/configuration&gt;
        &lt;/plugin&gt;
        
        
        &lt;plugin&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
            &lt;executions&gt;
                &lt;execution&gt;
                    &lt;goals&gt;
                        &lt;goal&gt;repackage&lt;/goal&gt;
                    &lt;/goals&gt;
                &lt;/execution&gt;
            &lt;/executions&gt;
        &lt;/plugin&gt;
    &lt;/plugins&gt;
&lt;/build&gt;</code></pre><h3>4. 生成依赖 Starter</h3>
<p>执行以下命令生成基座的依赖 starter：</p>
<pre><code class="language-bash">mvn com.alipay.sofa.koupleless:koupleless-base-build-plugin::packageDependency -f pom.xml</code></pre><p>这个命令会生成一个 <code>${baseAppName}-dependencies-starter</code> 的依赖包，模块项目可以将其作为 parent 来继承基座的所有依赖。</p>
<h3>5. 启动基座</h3>
<p>完成上述配置后，可以直接通过 IDE 或命令行启动基座：</p>
<pre><code class="language-bash">mvn spring-boot:run</code></pre><hr>
<h2>模块改造</h2>
<h3>1. 创建模块项目</h3>
<p>模块可以是现有的 SpringBoot 应用，也可以是新创建的项目。建议使用 Maven 多模块结构管理多个模块。</p>
<h3>2. 配置模块依赖</h3>
<p>在模块的 <code>pom.xml</code> 中添加模块依赖和打包插件：</p>
<pre><code class="language-xml">&lt;properties&gt;
    &lt;koupleless.runtime.version&gt;0.5.6&lt;/koupleless.runtime.version&gt;
    &lt;sofa.ark.version&gt;2.2.14&lt;/sofa.ark.version&gt;
&lt;/properties&gt;

&lt;dependencies&gt;
    
    &lt;dependency&gt;
        &lt;groupId&gt;com.alipay.sofa.koupleless&lt;/groupId&gt;
        &lt;artifactId&gt;koupleless-app-starter&lt;/artifactId&gt;
        &lt;version&gt;${koupleless.runtime.version}&lt;/version&gt;
        &lt;scope&gt;provided&lt;/scope&gt;
    &lt;/dependency&gt;
    
    
    &lt;parent&gt;
        &lt;groupId&gt;com.example&lt;/groupId&gt;
        &lt;artifactId&gt;base-application-dependencies-starter&lt;/artifactId&gt;
        &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;
    &lt;/parent&gt;
&lt;/dependencies&gt;</code></pre><h3>3. 配置打包插件</h3>
<p>在模块的 <code>pom.xml</code> 中添加 SOFAArk 打包插件：</p>
<pre><code class="language-xml">&lt;build&gt;
    &lt;finalName&gt;${project.artifactId}&lt;/finalName&gt;
    &lt;plugins&gt;
        
        &lt;plugin&gt;
            &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
            &lt;artifactId&gt;sofa-ark-maven-plugin&lt;/artifactId&gt;
            &lt;version&gt;${sofa.ark.version}&lt;/version&gt;
            &lt;executions&gt;
                &lt;execution&gt;
                    &lt;id&gt;default-cli&lt;/id&gt;
                    &lt;goals&gt;
                        &lt;goal&gt;repackage&lt;/goal&gt;
                    &lt;/goals&gt;
                &lt;/execution&gt;
            &lt;/executions&gt;
            &lt;configuration&gt;
                
                &lt;skipArkExecutable&gt;true&lt;/skipArkExecutable&gt;
                
                &lt;outputDirectory&gt;./target&lt;/outputDirectory&gt;
                
                &lt;bizName&gt;module1&lt;/bizName&gt;
                
                &lt;webContextPath&gt;/module1&lt;/webContextPath&gt;
                
                &lt;declaredMode&gt;true&lt;/declaredMode&gt;
                
                &lt;packExcludesConfig&gt;rules.txt&lt;/packExcludesConfig&gt;
            &lt;/configuration&gt;
        &lt;/plugin&gt;
        
        
        &lt;plugin&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
            &lt;executions&gt;
                &lt;execution&gt;
                    &lt;goals&gt;
                        &lt;goal&gt;repackage&lt;/goal&gt;
                    &lt;/goals&gt;
                &lt;/execution&gt;
            &lt;/executions&gt;
        &lt;/plugin&gt;
    &lt;/plugins&gt;
&lt;/build&gt;</code></pre><h3>4. 配置模块瘦身</h3>
<p>下载自动排包配置文件 <code>rules.txt</code>，放在模块的 <code>conf/ark/</code> 目录下：</p>
<pre><code class="language-bash"># 创建目录
mkdir -p conf/ark

# 下载配置文件
wget https://github.com/koupleless/samples/raw/master/springboot-samples/slimming/log4j2/biz1/conf/ark/rules.txt -O conf/ark/rules.txt</code></pre><p><code>rules.txt</code> 内容示例：</p>
<pre><code class="language-"># 排除的依赖（groupId:artifactId:version）
excludes=org.springframework.boot:spring-boot-starter-logging,commons-logging:commons-logging

# 排除的 groupId
excludeGroupIds=org.slf4j

# 排除的 artifactId
excludeArtifactIds=logback-classic</code></pre><h3>5. 开发模块代码</h3>
<p>创建模块的业务代码，例如 REST Controller：</p>
<pre><code class="language-java">package com.example.module1.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.context.ApplicationContext;

@RestController
@RequestMapping("/")
public class SampleController {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(SampleController.class);
    
    @Autowired
    private ApplicationContext applicationContext;
    
    @GetMapping("/")
    public String hello() {
        String appName = applicationContext.getApplicationName();
        LOGGER.info("{} web test: into sample controller", appName);
        return String.format("hello to %s deploy", appName);
    }
    
    @GetMapping("/health")
    public String health() {
        return "OK";
    }
}</code></pre><h3>6. 配置应用名</h3>
<p>在模块的 <code>application.properties</code> 中配置应用名：</p>
<pre><code class="language-properties">spring.application.name=module1</code></pre><hr>
<h2>模块瘦身</h2>
<h3>为什么要模块瘦身</h3>
<p>模块瘦身可以：</p>
<ul>
<li><strong>减小模块包大小</strong>：从数百 MB 降低到几十 MB</li>
<li><strong>减少内存占用</strong>：降低 Metaspace 占用，基座可以安装更多模块</li>
<li><strong>加快启动速度</strong>：模块启动时间从分钟级降低到秒级</li>
<li><strong>提高部署效率</strong>：小体积包更容易传输和部署</li>
</ul>
<h3>瘦身原理</h3>
<p>模块瘦身的核心思想是：<strong>移除模块中与基座重复的依赖，让模块复用基座的类</strong></p>
<p>通过以下方式实现：</p>
<ol>
<li><strong>继承基座依赖 starter</strong>：模块以 <code>${baseAppName}-dependencies-starter</code> 作为 parent</li>
<li><strong>自动排包</strong>：使用 <code>rules.txt</code> 配置文件排除不需要的依赖</li>
<li><strong>依赖委托</strong>：模块依赖设置为 <code>provided</code> 作用域，委托给基座加载</li>
</ol>
<h3>配置方法</h3>
<h4>方法一：继承基座依赖 starter（推荐）</h4>
<p>在模块的 <code>pom.xml</code> 中配置：</p>
<pre><code class="language-xml">&lt;parent&gt;
    &lt;groupId&gt;com.example&lt;/groupId&gt;
    &lt;artifactId&gt;base-application-dependencies-starter&lt;/artifactId&gt;
    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;
&lt;/parent&gt;</code></pre><p>这样模块会自动继承基座的所有依赖，无需手动配置版本。</p>
<h4>方法二：手动配置依赖作用域</h4>
<p>在模块的 <code>pom.xml</code> 中，将基座已有的依赖设置为 <code>provided</code> 作用域：</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
    &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
    &lt;scope&gt;provided&lt;/scope&gt;
&lt;/dependency&gt;</code></pre><h4>方法三：使用排除配置</h4>
<p>在 <code>sofa-ark-maven-plugin</code> 中配置排除项：</p>
<pre><code class="language-xml">&lt;configuration&gt;
    &lt;excludeGroupIds&gt;org.springframework,org.slf4j&lt;/excludeGroupIds&gt;
    &lt;excludeArtifactIds&gt;spring-boot-starter-logging&lt;/excludeArtifactIds&gt;
&lt;/configuration&gt;</code></pre><h3>验证瘦身效果</h3>
<p>构建模块并查看包大小：</p>
<pre><code class="language-bash"># 构建模块
mvn clean package

# 查看构建产物
ls -lh target/*.jar

# 瘦身前
-rw-r--r--  1 user  staff   250M  1 15 10:00 module1-1.0.0.jar
-rw-r--r--  1 user  staff   180M  1 15 10:00 module1-1.0.0-ark-biz.jar

# 瘦身后
-rw-r--r--  1 user  staff   250M  1 15 10:30 module1-1.0.0.jar
-rw-r--r--  1 user  staff    15M  1 15 10:30 module1-1.0.0-ark-biz.jar</code></pre><p>可以看到，瘦身后的 ark-biz.jar 从 180M 降低到 15M，效果显著。</p>
<hr>
<h2>部署与验证</h2>
<h3>1. 启动基座</h3>
<p>首先启动基座应用：</p>
<pre><code class="language-bash">cd base-application
mvn spring-boot:run</code></pre><p>或使用 IDE 启动主类。</p>
<h3>2. 构建模块</h3>
<p>在模块项目中执行构建：</p>
<pre><code class="language-bash">cd module1
mvn clean package</code></pre><p>构建成功后会生成两个 jar 包：</p>
<ul>
<li><code>module1-1.0.0.jar</code>：普通 SpringBoot fat jar，可独立运行</li>
<li><code>module1-1.0.0-ark-biz.jar</code>：Koupleless 模块包，用于合并部署</li>
</ul>
<h3>3. 使用 Arkctl 部署模块</h3>
<p>使用 Arkctl 工具将模块部署到基座：</p>
<pre><code class="language-bash"># 部署模块
arkctl deploy target/module1-1.0.0-ark-biz.jar

# 或者使用 curl 命令
curl -X POST http://localhost:1238/installBiz \
  -H "Content-Type: application/json" \
  -d '{"bizName":"module1","bizVersion":"1.0.0","bizUrl":"file:///path/to/module1-1.0.0-ark-biz.jar"}'</code></pre><h3>4. 验证部署</h3>
<p>部署成功后，可以通过以下方式验证：</p>
<h4>查看模块状态</h4>
<pre><code class="language-bash">arkctl status</code></pre><p>输出示例：</p>
<pre><code class="language-">Biz count: 1
bizName: module1
bizVersion: 1.0.0
bizState: ACTIVATED
mainClass: com.example.module1.Module1Application
webContextPath: /module1</code></pre><h4>访问模块接口</h4>
<pre><code class="language-bash"># 访问模块1的接口
curl http://localhost:8080/module1/
# 输出: hello to module1 deploy

curl http://localhost:8080/module1/health
# 输出: OK</code></pre><h4>查看日志</h4>
<p>在基座控制台可以看到模块启动日志：</p>
<pre><code class="language-">[INFO] Install biz: module1, version: 1.0.0, state: ACTIVATED
[INFO] Module module1 started successfully</code></pre><h3>5. 部署多个模块</h3>
<p>重复上述步骤部署 module2：</p>
<pre><code class="language-bash">cd module2
mvn clean package
arkctl deploy target/module2-1.0.0-ark-biz.jar</code></pre><p>验证多模块部署：</p>
<pre><code class="language-bash"># 查看所有模块
arkctl status

# 访问不同模块
curl http://localhost:8080/module1/
curl http://localhost:8080/module2/</code></pre><h3>6. 卸载模块</h3>
<pre><code class="language-bash"># 卸载指定模块
arkctl uninstall --bizName=module1 --bizVersion=1.0.0

# 或者使用 curl
curl -X POST http://localhost:1238/uninstallBiz \
  -H "Content-Type: application/json" \
  -d '{"bizName":"module1","bizVersion":"1.0.0"}'</code></pre><hr>
<h2>常见问题与解决方案</h2>
<h3>1. 类冲突问题</h3>
<p><strong>问题描述</strong>：模块和基座存在相同类名的类，导致类冲突。</p>
<p><strong>解决方案</strong>：</p>
<ul>
<li>使用类隔离机制，模块优先加载自己的类</li>
<li>公共类下沉到基座，模块通过依赖委托复用</li>
<li>避免在模块和基座中定义相同包名和类名的类</li>
</ul>
<h3>2. 静态字段共享问题</h3>
<p><strong>问题描述</strong>：多个模块共享静态字段，导致数据混乱。</p>
<p><strong>解决方案</strong>：</p>
<p>使用 <code>StaticFieldMapWrapper</code> 适配静态字段：</p>
<pre><code class="language-java">import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;

public class StaticFieldMapWrapper&lt;T&gt; {
    private final ConcurrentHashMap&lt;ClassLoader, T&gt; classLoaderTMap = new ConcurrentHashMap&lt;&gt;();
    private Supplier&lt;T&gt; getDefaultMethod;
    
    public StaticFieldMapWrapper(Supplier&lt;T&gt; getDefaultMethod) {
        this.getDefaultMethod = getDefaultMethod;
    }
    
    public T getOrPutDefault() {
        T t = classLoaderTMap.get(Thread.currentThread().getContextClassLoader());
        if (t == null && getDefaultMethod != null) {
            t = getDefaultMethod.get();
            classLoaderTMap.put(Thread.currentThread().getContextClassLoader(), t);
        }
        return t;
    }
}</code></pre><h3>3. Dubbo 兼容性问题</h3>
<p><strong>问题描述</strong>：模块中使用 Dubbo 泛化调用时出现问题。</p>
<p><strong>解决方案</strong>：</p>
<ul>
<li>确保 Dubbo 相关依赖下沉到基座</li>
<li>为 Dubbo 配置正确的 ClassLoader 切换</li>
<li>参考官方 adapter 仓库的解决方案</li>
</ul>
<h3>4. Nacos 线程过多</h3>
<p><strong>问题描述</strong>：每个模块启动时创建 Nacos 客户端，导致线程数过多。</p>
<p><strong>解决方案</strong>：</p>
<p>在基座中缓存 Nacos 客户端：</p>
<pre><code class="language-java">private static final Map&lt;Properties, ConfigService&gt; CONFIG_SERVICE_MAP = new ConcurrentHashMap&lt;&gt;();

public static ConfigService getConfigService(Properties properties) {
    return CONFIG_SERVICE_MAP.computeIfAbsent(properties, k -&gt; {
        try {
            return NacosFactory.createConfigService(k);
        } catch (NacosException e) {
            throw new RuntimeException(e);
        }
    });
}</code></pre><h3>5. 模块启动顺序问题</h3>
<p><strong>问题描述</strong>：模块之间有依赖关系，需要按顺序启动。</p>
<p><strong>解决方案</strong>：</p>
<ul>
<li>在基座中配置模块启动顺序</li>
<li>使用依赖关系控制启动顺序</li>
<li>避免循环依赖</li>
</ul>
<h3>6. 内存泄漏</h3>
<p><strong>问题描述</strong>：模块卸载后，内存没有释放，导致 Metaspace 持续增长。</p>
<p><strong>解决方案</strong>：</p>
<ul>
<li>确保模块卸载时清理所有资源（线程池、连接等）</li>
<li>使用弱引用管理缓存</li>
<li>定期重启基座或进行内存整理</li>
</ul>
<h3>7. Spring 上下文冲突</h3>
<p><strong>问题描述</strong>：模块和基座的 Spring Bean 冲突。</p>
<p><strong>解决方案</strong>：</p>
<ul>
<li>使用不同的 Bean 名称</li>
<li>模块中使用 <code>@Primary</code> 注解明确优先级</li>
<li>避免模块和基座定义相同的 Bean</li>
</ul>
<hr>
<h2>生产环境部署</h2>
<h3>1. 部署架构</h3>
<p>生产环境推荐部署架构：</p>
<pre><code class="language-">用户流量 -&gt; Nginx/SLB -&gt; 基座应用（K8s Pod）
                                    |
                                    +-&gt; 模块1
                                    +-&gt; 模块2
                                    +-&gt; 模块3</code></pre><h3>2. K8s 部署配置</h3>
<h4>基座 Deployment</h4>
<pre><code class="language-yaml">apiVersion: apps/v1
kind: Deployment
metadata:
  name: base-application
spec:
  replicas: 3
  selector:
    matchLabels:
      app: base-application
  template:
    metadata:
      labels:
        app: base-application
    spec:
      containers:
      - name: base-application
        image: registry.example.com/base-application:1.0.0
        ports:
        - containerPort: 8080
        env:
        - name: MODULE_AUTO_INSTALL
          value: "true"
        - name: MODULE_OSS_ENDPOINT
          value: "oss-cn-region.aliyuncs.com"
        - name: MODULE_OSS_BUCKET
          value: "koupleless-modules"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 5
        volumeMounts:
        - name: module-storage
          mountPath: /opt/modules
      volumes:
      - name: module-storage
        persistentVolumeClaim:
          claimName: module-pvc</code></pre><h4>模块发布流水线</h4>
<pre><code class="language-yaml"># 模块 CI/CD Pipeline
stages:
  - build
  - test
  - package
  - deploy

build_module:
  stage: build
  script:
    - mvn clean compile

test_module:
  stage: test
  script:
    - mvn test

package_module:
  stage: package
  script:
    - mvn clean package
    - cp target/*.ark-biz.jar module.ark-biz.jar
  artifacts:
    paths:
      - module.ark-biz.jar

deploy_module:
  stage: deploy
  script:
    # 上传模块到 OSS
    - ossutil cp module.ark-biz.jar oss://koupleless-modules/module1/${CI_COMMIT_SHA}.ark-biz.jar
    
    # 通知基座更新模块
    - curl -X POST http://base-service:8080/install \
        -H "Content-Type: application/json" \
        -d '{"module":"module1","version":"'${CI_COMMIT_SHA}'"}'</code></pre><h3>3. 模块动态部署</h3>
<h4>方案一：OSS 监听模式</h4>
<p>基座监听 OSS 目录变化，自动部署新模块：</p>
<pre><code class="language-java">@Service
public class ModuleDeploymentService {
    
    @Autowired
    private OssClient ossClient;
    
    @Scheduled(fixedDelay = 5000)
    public void checkModuleUpdates() {
        // 检查 OSS 上的模块版本
        List&lt;String&gt; modules = getModuleList();
        
        for (String module : modules) {
            String latestVersion = getLatestVersion(module);
            String currentVersion = getCurrentVersion(module);
            
            if (!latestVersion.equals(currentVersion)) {
                // 下载新模块
                downloadModule(module, latestVersion);
                
                // 卸载旧模块
                uninstallModule(module, currentVersion);
                
                // 安装新模块
                installModule(module, latestVersion);
            }
        }
    }
}</code></pre><h4>方案二：配置中心模式</h4>
<p>通过配置中心控制模块部署：</p>
<pre><code class="language-yaml"># 配置中心配置
modules:
  module1:
    version: 1.0.0
    enabled: true
    healthCheck: /health
  module2:
    version: 2.0.0
    enabled: true
    healthCheck: /health</code></pre><p>基座监听配置变化：</p>
<pre><code class="language-java">@NacosConfigListener(dataId = "modules.json")
public void onModuleConfigChange(String config) {
    ModulesConfig modulesConfig = JSON.parseObject(config, ModulesConfig.class);
    
    // 对比配置差异
    List&lt;ModuleChange&gt; changes = compareModuleConfig(modulesConfig);
    
    // 应用变更
    for (ModuleChange change : changes) {
        switch (change.getType()) {
            case INSTALL:
                installModule(change.getModule());
                break;
            case UNINSTALL:
                uninstallModule(change.getModule());
                break;
            case UPDATE:
                updateModule(change.getModule());
                break;
        }
    }
}</code></pre><p>下面把教程里没展开的两件事一次讲透：  </p>
<ol>
<li>静态合并部署（Static Packaged Deployment）——一次性把基座 + N 个模块打成一个可执行 Fat Jar，启动即“全员就位”；  </li>
<li>端口隔离——让每个模块真的监听自己的端口，实现“进程内多 WebServer”。</li>
</ol>
<hr>
<h4>方案三、静态合并部署（Static Packaged Deployment）</h4>
<p><strong>适用场景</strong>  </p>
<ul>
<li>上线流程极简，不允许运维侧再敲 arkctl deploy；  </li>
<li>容器镜像只启一个进程，K8s liveness/readiness 探针直接探测基座即可；  </li>
<li>模块版本固定，不需要热卸载/热加载。</li>
</ul>
<ol>
<li><p><strong>打包思路</strong><br>基座 pom 里把各模块的 ark-biz.jar 当资源一起打进 BOOT-INF/lib 目录；<br>基座启动时借助 SOFAArk 的 “static-biz” 机制，让这些 ark-biz.jar 随基座生命周期一起 install + start。</p>
</li>
<li><p><strong>实操步骤</strong><br>step-1 模块端正常 mvn package 得到 moduleX-1.0.0-ark-biz.jar<br>step-2 基座 pom 增加 profile（只在打静态包时激活）</p>
</li>
</ol>
<pre><code class="language-xml">
&lt;profiles&gt;
  &lt;profile&gt;
    &lt;id&gt;static-pack&lt;/id&gt;
    &lt;dependencies&gt;
      
      &lt;dependency&gt;
        &lt;groupId&gt;com.example&lt;/groupId&gt;
        &lt;artifactId&gt;module1&lt;/artifactId&gt;
        &lt;version&gt;1.0.0&lt;/version&gt;
        &lt;classifier&gt;ark-biz&lt;/classifier&gt;
        &lt;type&gt;jar&lt;/type&gt;
      &lt;/dependency&gt;
      &lt;dependency&gt;
        &lt;groupId&gt;com.example&lt;/groupId&gt;
        &lt;artifactId&gt;module2&lt;/artifactId&gt;
        &lt;version&gt;1.0.0&lt;/version&gt;
        &lt;classifier&gt;ark-biz&lt;/classifier&gt;
        &lt;type&gt;jar&lt;/type&gt;
      &lt;/dependency&gt;
    &lt;/dependencies&gt;
  &lt;/profile&gt;
&lt;/profiles&gt;</code></pre><p>step-3 基座启动类加 <code>@StaticBiz</code> 注解（0.5.6+ 支持），告诉 SOFAArk 把 classpath 下所有 ark-biz.jar 静态安装：</p>
<pre><code class="language-java">@SpringBootApplication
@StaticBiz   // 关键
public class BaseApplication {
    public static void main(String[] args) {
        SpringApplication.run(BaseApplication.class, args);
    }
}</code></pre><p>step-4 打静态包</p>
<pre><code class="language-bash">mvn clean package -Pstatic-pack</code></pre><p>生成的 base-application-1.0.0-exec.jar 里同时包含 module1-ark-biz.jar &amp; module2-ark-biz.jar；<br>java -jar base-application-1.0.0-exec.jar 启动后，arkctl status 能看到两个模块已经是 ACTIVATED，无需再 deploy。</p>
<p><strong>让模块监听自己的端口</strong></p>
<p>默认行为  </p>
<ul>
<li>所有模块与基座共用同一个嵌入式 Tomcat（端口 8080），仅靠 webContextPath 区分；  </li>
<li>好处：节省线程、节省内存；  </li>
<li>坏处：无法“端口级”隔离，也做不到“模块单独暴露管理口”。</li>
</ul>
<p>Koupleless 0.5.6 开始支持 “多 WebServer 模式”，即每个模块可以起独立的 Netty/Tomcat，真正 bind 自己的端口。</p>
<ol>
<li>模块端增加依赖</li>
</ol>
<pre><code class="language-xml">
&lt;dependency&gt;
    &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
    &lt;artifactId&gt;web-ark-plugin&lt;/artifactId&gt;
    &lt;classifier&gt;multi-web&lt;/classifier&gt;   
    &lt;scope&gt;provided&lt;/scope&gt;
&lt;/dependency&gt;</code></pre><ol start="2">
<li>配置端口 &amp; 上下文</li>
</ol>
<pre><code class="language-yaml"># module1/application.yml
server:
  port: 8081          # 模块独占
  servlet:
    context-path: /   # 可以为 /
spring:
  application:
    name: module1</code></pre><ol start="3">
<li>打包插件声明</li>
</ol>
<pre><code class="language-xml">&lt;plugin&gt;
  &lt;groupId&gt;com.alipay.sofa&lt;/groupId&gt;
  &lt;artifactId&gt;sofa-ark-maven-plugin&lt;/artifactId&gt;
  &lt;configuration&gt;
    &lt;bizName&gt;module1&lt;/bizName&gt;
    &lt;webContextPath&gt;/&lt;/webContextPath&gt;
    
    &lt;webMultiFlag&gt;true&lt;/webMultiFlag&gt;
  &lt;/configuration&gt;
&lt;/plugin&gt;</code></pre><ol start="4">
<li>部署验证<br>基座仍跑 8080，module1 会额外监听 8081，module2 可再配 8082，互不影响；<br>curl <a href="http://localhost:8081/">http://localhost:8081/</a> 直接返回模块 1 的响应，无需再加前缀。</li>
</ol>
<p>注意  </p>
<ul>
<li>端口多占 n 个，内存/线程也会相应增加，按需权衡；  </li>
<li>生产环境需在 K8s Service 里把 8081、8082… 也声明出来，或走 Istio 多端口即可。</li>
</ul>
<h2>参考资源</h2>
<ul>
<li><strong>官方文档</strong>：<a href="https://koupleless.io/docs/">https://koupleless.io/docs/</a></li>
<li><strong>GitHub 仓库</strong>：<a href="https://github.com/koupleless/koupleless">https://github.com/koupleless/koupleless</a></li>
<li><strong>示例代码</strong>：<a href="https://github.com/koupleless/samples">https://github.com/koupleless/samples</a></li>
<li><strong>社区支持</strong>：<a href="https://github.com/koupleless/koupleless/issues">https://github.com/koupleless/koupleless/issues</a></li>
</ul>

      <p style='text-align: right'>
      <a href='https://liuyaowen.cn/posts/default/20251216#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">69415a6026b72f4e55156fc3</guid>
  <category>posts</category>
<category>技术</category>
 </item>
  <item>
    <title>java 虚拟线程好文推荐</title>
    <link>https://liuyaowen.cn/posts/default/20251023</link>
    <pubDate>Wed, 22 Oct 2025 12:30:25 GMT</pubDate>
    <description>https://www.cnblogs.com/throwable/p/16758997.html</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://liuyaowen.cn/posts/default/20251023'>https://liuyaowen.cn/posts/default/20251023</a></blockquote>
      <p><a href="https://www.cnblogs.com/throwable/p/16758997.html">https://www.cnblogs.com/throwable/p/16758997.html</a></p>

      <p style='text-align: right'>
      <a href='https://liuyaowen.cn/posts/default/20251023#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">68f8ce6129f528c76ad3fe68</guid>
  <category>posts</category>
<category>技术</category>
 </item>
  <item>
    <title>源码阅读之旅</title>
    <link>https://liuyaowen.cn/notes/6</link>
    <pubDate>Fri, 12 Sep 2025 15:59:47 GMT</pubDate>
    <description>源码之旅

Java
spring security 99%
Tomcat  10%
openfei</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://liuyaowen.cn/notes/6'>https://liuyaowen.cn/notes/6</a></blockquote>
      <h1>源码之旅</h1>
<h2>Java</h2>
<h3>spring security 99%</h3>
<h3>Tomcat  10%</h3>
<h3>openfeign 20%</h3>
<h3>spring boot  50%</h3>
<h3>spring  70%</h3>
<h3>mybatis 99%</h3>
<h3>springmvc 80%</h3>
<h3>netty</h3>
<h3>spring  cloud</h3>
<h3>spring cloud alibaba sentinel 40%</h3>
<h3>nacos</h3>
<h3>spring loadbalance</h3>
<h3>opentrace</h3>

      <p style='text-align: right'>
      <a href='https://liuyaowen.cn/notes/6#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">68c4437329f528c76ad26190</guid>
  <category>notes</category>
false
 </item>
  <item>
    <title>MyBatis 源码手记</title>
    <link>https://liuyaowen.cn/posts/default/20250906</link>
    <pubDate>Sat, 06 Sep 2025 05:53:11 GMT</pubDate>
    <description>一、SqlSession 与执行器 Executor

SqlSession 是 MyBatis 的</description>
    <content:encoded><![CDATA[
      <blockquote>该渲染由 marked 生成，可能存在排版问题，最佳体验请前往：<a href='https://liuyaowen.cn/posts/default/20250906'>https://liuyaowen.cn/posts/default/20250906</a></blockquote>
      <h2>一、SqlSession 与执行器 Executor</h2>
<p>SqlSession 是 MyBatis 的门面接口，我们在业务代码里最常打交道的家伙。它不直接执行 SQL，而是委托给内部的 Executor 来干活。Executor 负责 SQL 的实际执行、事务控制和缓存管理。</p>
<p>看看 DefaultSqlSession 的实现：</p>
<pre><code class="language-java">public class DefaultSqlSession implements SqlSession {
    private final Configuration configuration;
    private final Executor executor;
    private final boolean autoCommit;
    // ... 其他字段

    public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
        this.configuration = configuration;
        this.executor = executor;
        this.autoCommit = autoCommit;
        // ... 初始化
    }

    @Override
    public &lt;T&gt; T selectOne(String statement, Object parameter) {
        try {
            MappedStatement ms = configuration.getMappedStatement(statement);
            List&lt;T&gt; list = executor.query(ms, wrapCollection(parameter), RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
            if (list.size() == 1) {
                return list.get(0);
            } else if (list.size() &gt; 1) {
                throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
            } else {
                return null;
            }
        } catch (Exception e) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        }
    }
    // ... 其他方法如 update、insert 等类似
}</code></pre><p>这里的关键是 <code>executor.query()</code>，它会根据 MappedStatement（封装了 SQL 配置）来执行查询。SqlSession 还管理事务：<code>commit()</code> 会调用 <code>executor.commit()</code>，<code>rollback()</code> 同理。如果 <code>autoCommit</code> 为 true，就不会自动提交事务——这在批量操作时特别有用，避免了频繁的 JDBC commit 开销。</p>
<h3>Executor 类型详解</h3>
<p>MyBatis 提供了几种 Executor 实现，每种针对不同场景优化：</p>
<h4>SimpleExecutor</h4>
<p>最基础的，每次执行 SQL 都会新建 Statement 对象。简单粗暴，适合单次查询，不用担心资源复用。但在循环执行时性能差，因为反复创建 Statement 会增加 JDBC 开销。源码里 <code>doQuery()</code> 方法每次都调用 <code>statementHandler.prepare()</code> 来创建新 Statement。</p>
<h4>ReuseExecutor</h4>
<p>聪明点，它会复用 PreparedStatement。对于相同的 SQL，它用一个 Map 来缓存 Statement 对象，避免重复 prepare。看代码：</p>
<pre><code class="language-java">private final Map&lt;String, Statement&gt; statementMap = new HashMap&lt;&gt;();

@Override
protected int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    // ... 获取 BoundSql
    String sql = boundSql.getSql();
    Statement stmt = statementMap.get(sql);
    if (stmt == null) {
        stmt = handler.prepare(connection, transaction.getTimeout());
        statementMap.put(sql, stmt);
    }
    // ... 设置参数并执行
}</code></pre><p>这在高频相同 SQL 的场景下（如循环插入相同结构的数据）能省不少时间。我在项目里用过，确实能感觉到性能提升，但要注意事务结束时要清空 <code>statementMap</code>，否则内存泄漏。</p>
<h4>BatchExecutor</h4>
<p>专为批量操作设计。它会缓冲多个 SQL 操作，直到 <code>flushStatements()</code> 或 <code>commit()</code> 时一次性发送到数据库。内部用一个 List 来收集结果。适合大批量 insert/update，比如导入 Excel 数据时用这个能把性能从 O(n) 的 JDBC 调用降到接近 O(1)。</p>
<h4>CachingExecutor</h4>
<p>这是一个装饰器（Decorator），包裹其他 Executor 来添加二级缓存逻辑。如果配置了二级缓存，它会在 query 前先查缓存，miss 后再执行底层 Executor。</p>
<p>Executor 基类 BaseExecutor 还内置了一级缓存（PerpetualCache，默认 HashMap 实现），查询时用 queryStack 来防止循环查询（比如嵌套查询）。事务管理也在 BaseExecutor 里：localCache 在 commit/rollback 时清空。</p>
<p><strong>感悟</strong>：MyBatis 的 Executor 体系体现了策略模式，不同执行策略无缝切换，业务代码零感知。</p>
<h2>二、SqlSource 与动态 SQL</h2>
<p>MyBatis 的 SQL 构建是其亮点之一，分静态和动态两种。静态 SQL 简单高效，动态 SQL 则通过树状结构处理复杂逻辑。</p>
<h3>静态 SQL</h3>
<p>直接的 SQL 字符串，比如：</p>
<pre><code class="language-java">String sql = "SELECT * FROM user WHERE id = ?";
StaticSqlSource sqlSource = new StaticSqlSource(configuration, sql, parameterMappings);
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);  // 直接返回固定 SQL 和参数</code></pre><p>参数映射在解析 XML 时就固定了，运行时零开销。适合不变的查询。</p>
<h3>动态 SQL</h3>
<p>动态 SQL 用 SqlNode 树表示，DynamicSqlSource 在 <code>getBoundSql()</code> 时遍历树生成最终 SQL。SqlNode 是接口，有各种实现：</p>
<ul>
<li><strong>TextSqlNode</strong>：纯文本 SQL 片段</li>
<li><strong>IfSqlNode</strong>：条件判断，内部持有一个 SqlNode 和表达式（如 “id != null”）</li>
<li><strong>ChooseSqlNode</strong>：类似 switch，包含 When 和 Otherwise</li>
<li><strong>ForEachSqlNode</strong>：处理集合迭代，生成 IN 子句或批量插入</li>
<li><strong>TrimSqlNode</strong>：去除多余的 AND/OR 前缀</li>
<li><strong>MixedSqlNode</strong>：容器，持有子节点列表</li>
</ul>
<p>示例构建树：</p>
<pre><code class="language-java">List&lt;SqlNode&gt; contents = new ArrayList&lt;&gt;();
contents.add(new StaticTextSqlNode("SELECT * FROM user WHERE 1=1 "));
if (condition) {
    contents.add(new IfSqlNode(new StaticTextSqlNode("AND id = #{id}"), "id != null"));
}
MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);
DynamicSqlSource dynamicSqlSource = new DynamicSqlSource(configuration, mixedSqlNode);</code></pre><p><code>getBoundSql(parameterObject)</code> 时，会用 DynamicContext 上下文递归 <code>apply()</code> 每个节点，收集 SQL 和 ParameterMapping。BoundSql 最终封装 SQL、参数列表和映射。</p>
<p>动态 SQL 的灵活性来自于 OGNL 的表达式求值（详见下节），但性能稍差，因为每次执行都要解析树。<strong>优化点</strong>：如果参数固定，可以预编译，但 MyBatis 默认运行时解析。</p>
<p><strong>个人吐槽</strong>：第一次看动态 SQL 源码时，被 SqlNode 树的递归搞晕了，但用习惯后觉得这设计太优雅了——XML 配置直接转成树，运行时动态组装，避免了字符串拼接的 SQL 注入风险。</p>
<h2>三、ParameterMapping 与 OGNL</h2>
<p>参数处理是 MyBatis 安全的核心。SQL 中的 <code>#{}</code> 被解析成 ParameterMapping 对象，包含属性名、Java 类型、JDBC 类型等。</p>
<pre><code class="language-java">ParameterMapping mapping = ParameterMapping.builder(configuration, "id", Integer.class)
    .javaType(Integer.class)
    .jdbcType(JdbcType.INTEGER)
    .build();</code></pre><p>在 <code>StatementHandler.setParameters()</code> 时，用 TypeHandler 设置参数：</p>
<pre><code class="language-java">TypeHandler&lt;Object&gt; typeHandler = parameterMapping.getTypeHandler();
typeHandler.setParameter(ps, i + 1, value, parameterMapping.getJdbcType());</code></pre><p>值从 parameterObject 取，用 OGNL：</p>
<h3>OGNL (Object Graph Navigation Language)</h3>
<p>OGNL 是 MyBatis 的表达式引擎，<code>OgnlCache.getValue(expression, parameterObject)</code> 可以解析 “user.address.street” 这样的嵌套属性。内部用 OgnlRuntime 缓存解析结果，避免重复解析。OGNL 支持方法调用、集合访问等，比反射灵活。</p>
<p>TypeHandler 负责类型转换，TypeHandlerRegistry 注册了常见类型（如 StringTypeHandler、IntegerTypeHandler），自定义类型可扩展。参数映射确保了 preparedStatement 的安全，防 SQL 注入。</p>
<p><strong>感悟</strong>：OGNL 的强大让我在业务中也用类似表达式引擎来处理配置，超级方便。</p>
<h2>四、ResultMap 与结果映射</h2>
<p>ResultMap 是结果集到对象的桥梁。XML 转成 ResultMap 对象，包含 ResultMapping 列表（列名到属性名的映射）。</p>
<p>在 <code>ResultSetHandler.handleResultSets()</code> 时，用 DefaultResultSetHandler 处理：</p>
<pre><code class="language-java">while (rs.next()) {
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    // ... 通过 MetaObject 设置属性
    metaObject.setValue(resultMapping.getProperty(), value);
}</code></pre><p>支持：</p>
<ul>
<li><strong>自动映射</strong>：列名匹配属性名（下划线转驼峰）</li>
<li><strong>嵌套结果</strong>：association/collection 处理一对一/一对多</li>
<li><strong>构造函数注入</strong></li>
<li><strong>鉴别器（discriminator）</strong>：基于列值选择映射</li>
<li><strong>自定义 TypeHandler</strong></li>
</ul>
<p>ResultMap 解耦了 SQL 和 POJO，改表结构只需调映射，不动代码。懒加载（lazyLoadingEnabled）在访问时再查嵌套对象。</p>
<p><strong>吐槽</strong>：复杂嵌套映射时，调试 ResultSetWrapper 的列类型匹配真是个坑，但一旦搞懂，处理复杂查询如鱼得水。</p>
<h2>五、缓存机制</h2>
<p>MyBatis 缓存分两级，设计精妙。</p>
<h3>一级缓存</h3>
<p>SqlSession 级，默认开启。在 <code>BaseExecutor.query()</code> 中，先查 localCache（PerpetualCache，HashMap）。key 是 [MappedStatement id + offset + limit + SQL + params]。命中直接返回，miss 执行 SQL 后 put。update/commit 清空。防止脏读，在事务内有效。</p>
<h3>二级缓存</h3>
<p>Namespace 级（Mapper.xml），需配置 <code>&lt;cache/&gt;</code> 或 <code>cacheEnabled=true</code>。CachingExecutor 装饰底层 Executor，先查二级缓存（可配置如 FifoCache、LruCache），miss 后执行并 put。支持序列化（Serializable），集群时可集成 Redis 等。flushCache/selective 配置控制刷新。</p>
<p>缓存 key 生成用 CacheKey，包含 hashCode、multiplier 等，确保唯一。事务提交时才同步到二级缓存。</p>
<p><strong>优化</strong>：读写锁（SynchronizedCache）防止并发问题。</p>
<p><strong>个人经验</strong>：在高并发读场景，用二级缓存能把数据库压力降 80%，但要注意失效策略。</p>
<h3>缓存装饰器模式实现</h3>
<pre><code class="language-java">public class Cache {
    // 基础缓存实现
    PerpetualCache delegate;
    
    // 可选的装饰器链
    LruCache lru;
    FifoCache fifo;
    SynchronizedCache sync;
    
    // 装饰器包装
    cache = new SynchronizedCache(new LruCache(new PerpetualCache(id)));
}</code></pre><h2>六、插件与拦截器</h2>
<p>插件用责任链模式扩展核心组件。Interceptor 接口：</p>
<pre><code class="language-java">@Intercepts({@Signature(type = Executor.class, method = "query", 
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
public class MyPlugin implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 前置逻辑
        Object result = invocation.proceed();  // 调用下一个
        // 后置逻辑
        return result;
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);  // 动态代理
    }
}</code></pre><p><code>Plugin.wrap()</code> 用 JDK 动态代理包装目标（如 Executor）。多个插件链式执行。</p>
<h3>常见用例</h3>
<ul>
<li><strong>分页插件</strong>：拦截 query，修改 SQL 添加 LIMIT</li>
<li><strong>分库分表插件</strong>：修改 SQL，路由到不同库表</li>
<li><strong>性能监控插件</strong>：记录执行时间，慢查询报警</li>
<li><strong>数据脱敏插件</strong>：结果集处理，敏感数据打码</li>
</ul>
<p><strong>启发</strong>：这让我在设计业务框架时也用拦截器来加日志、权限检查，核心代码干净。</p>
<h2>七、事务管理</h2>
<p>事务在 Transaction 接口，JdbcTransaction 实现 JDBC commit/rollback。Executor 持 Transaction：</p>
<pre><code class="language-java">public void commit(boolean required) throws SQLException {
    if (transaction != null) {
        transaction.commit();
    }
}</code></pre><p><code>SqlSessionFactory.openSession(autoCommit)</code> 决定是否自动提交。BatchExecutor 在 flush 时批量 commit。支持 Spring 集成，通过 TransactionManager。</p>
<p><strong>底层</strong>：<code>getConnection()</code> 从 DataSource 取连接，<code>setAutoCommit(false)</code> 开启手动事务。回滚时 close 连接释放资源。</p>
<h3>事务隔离级别</h3>
<pre><code class="language-java">public enum TransactionIsolationLevel {
    NONE(Connection.TRANSACTION_NONE),
    READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED),
    READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED),
    REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ),
    SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE);
}</code></pre><p><strong>坑点</strong>：嵌套事务不支持，需小心多 SqlSession 场景。</p>
<h2>八、反射与工具类</h2>
<p>MyBatis 重度依赖反射简化对象操作。</p>
<h3>核心反射组件</h3>
<ul>
<li><strong>Reflector/MetaClass</strong>：Reflector 缓存类元数据（getter/setter），MetaClass 找属性路径</li>
<li><strong>MetaObject</strong>：统一读写对象，支持嵌套：“user.address.street”。内部用 ObjectWrapper（BeanWrapper for POJO, MapWrapper for Map, CollectionWrapper for List）</li>
<li><strong>Invoker</strong>：抽象 get/set，如 MethodInvoker、GetFieldInvoker</li>
</ul>
<pre><code class="language-java">// 使用示例
MetaObject metaObject = SystemMetaObject.forObject(user);
metaObject.setValue("name", "grok");
String name = (String) metaObject.getValue("address.street");</code></pre><h3>PropertyTokenizer</h3>
<p>解析属性表达式 “user.address[0].street”：</p>
<pre><code class="language-java">public class PropertyTokenizer implements Iterator&lt;PropertyTokenizer&gt; {
    private String name;      // user
    private String indexedName; // address[0] 
    private String index;     // 0
    private String children;  // street
}</code></pre><p>这些工具让参数/结果映射通用化，避免硬编码反射调用。</p>
<p><strong>启发</strong>：业务中用类似 MetaObject 处理动态表单，省时省力。</p>
<h2>九、类型处理</h2>
<h3>TypeHandlerRegistry</h3>
<p>管理类型转换：</p>
<pre><code class="language-java">registry.register(Integer.class, new IntegerTypeHandler());
registry.register(String.class, new StringTypeHandler());
// 自定义枚举处理
registry.register(MyEnum.class, new EnumTypeHandler&lt;&gt;(MyEnum.class));</code></pre><p><code>getTypeHandler()</code> 根据 JavaType/JdbcType 找。内置 40+ handler，支持自定义如 EnumTypeHandler。JDBC null 处理用 NullTypeHandler。</p>
<h3>常见 TypeHandler</h3>
<pre><code class="language-java">// 基础类型
IntegerTypeHandler, LongTypeHandler, StringTypeHandler
// 日期时间  
DateTypeHandler, LocalDateTimeTypeHandler, InstantTypeHandler
// 大对象
BlobTypeHandler, ClobTypeHandler
// 数组
ArrayTypeHandler</code></pre><h3>自定义 TypeHandler 示例</h3>
<pre><code class="language-java">public class JsonTypeHandler implements TypeHandler&lt;Object&gt; {
    @Override
    public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) {
        ps.setString(i, JSON.toJSONString(parameter));
    }

    @Override
    public Object getResult(ResultSet rs, String columnName) {
        return JSON.parseObject(rs.getString(columnName));
    }
}</code></pre><p><strong>类型别名 TypeAliasRegistry</strong> 简化配置，如 “int” alias Integer。</p>
<h2>十、日志与调试</h2>
<h3>日志抽象</h3>
<p>日志用 Log 接口，适配多种框架（SLF4J、Log4J 等）。配置 <code>logImpl=SLF4J</code>。</p>
<pre><code class="language-java">public interface Log {
    boolean isDebugEnabled();
    void debug(String s);
    void debug(String s, Throwable e);
    // ... 其他级别
}</code></pre><h3>具体实现</h3>
<ul>
<li>Slf4jImpl</li>
<li>Log4jImpl</li>
<li>Log4j2Impl</li>
<li>JdkLoggingImpl</li>
<li>CommonsLoggingImpl</li>
<li>StdOutImpl（标准输出）</li>
<li>NoLoggingImpl（无日志）</li>
</ul>
<h3>SQL 日志</h3>
<p>StatementLog 在 prepare/execute 前后 log SQL 和 params。调试时，开启 trace 级别看绑定参数：</p>
<pre><code class="language-">2023-01-01 10:00:00.001 [main] DEBUG c.example.mapper.UserMapper.selectById - ==&gt;  Preparing: SELECT * FROM user WHERE id = ?
2023-01-01 10:00:00.002 [main] DEBUG c.example.mapper.UserMapper.selectById - ==&gt; Parameters: 1(Integer)
2023-01-01 10:00:00.005 [main] DEBUG c.example.mapper.UserMapper.selectById - &lt;==      Total: 1</code></pre><p><strong>感悟</strong>：日志设计早，帮我排查过无数 SQL 问题。框架日志抽象让我学到：日志别硬编码，适配器模式永不过时。</p>
<h2>十一、SQL 重用与优化</h2>
<h3>性能优化策略</h3>
<ul>
<li><strong>ReuseExecutor</strong> 缓存 Statement，减少 prepare 调用</li>
<li><strong>BatchExecutor</strong> 批量 <code>addBatch()</code>，<code>executeBatch()</code> 一次性发</li>
<li><strong>预编译 SQL</strong> 用 <code>#{}</code>，参数复用安全</li>
<li><strong>一级缓存</strong> 避重复查询，二级缓存跨 Session</li>
<li><strong>RowBounds</strong> 内存分页（不推荐大结果集，用物理分页）</li>
<li><strong>懒加载</strong> 延迟嵌套查询</li>
</ul>
<h3>SQL 执行流程优化</h3>
<pre><code class="language-java">// 1. SQL 解析缓存
MappedStatement ms = configuration.getMappedStatement(statement);

// 2. 参数处理优化
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey cacheKey = createCacheKey(ms, parameter, rowBounds, boundSql);

// 3. 缓存命中检查  
List&lt;E&gt; list = resultHandler == null ? (List&lt;E&gt;) localCache.getObject(cacheKey) : null;
if (list != null) {
    // 缓存命中，直接返回
    return list;
}

// 4. 数据库查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
localCache.putObject(cacheKey, list);</code></pre><h3>优化建议</h3>
<ol>
<li><strong>监控 slow SQL</strong>：用插件加执行时间 log</li>
<li><strong>高负载调优</strong>：调 cache size，避免 OOM</li>
<li><strong>连接池配置</strong>：合理设置 maxActive、maxIdle</li>
<li><strong>批量操作</strong>：使用 BatchExecutor，避免 N+1 查询</li>
<li><strong>结果集优化</strong>：只查需要的字段，避免 SELECT *</li>
</ol>
<h2>十二、配置与初始化</h2>
<h3>Configuration 核心配置</h3>
<p>Configuration 是 MyBatis 的配置中心，包含所有配置信息：</p>
<pre><code class="language-java">public class Configuration {
    // 核心组件
    protected Environment environment;
    protected boolean safeRowBoundsEnabled;
    protected boolean safeResultHandlerEnabled = true;
    protected boolean mapUnderscoreToCamelCase;
    protected boolean aggressiveLazyLoading;
    protected boolean multipleResultSetsEnabled = true;
    protected boolean useGeneratedKeys;
    protected boolean useColumnLabel = true;
    protected boolean cacheEnabled = true;
    protected boolean callSettersOnNulls;
    protected boolean useActualParamName = true;
    
    // 注册中心
    protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
    protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
    protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();
    
    // 映射语句
    protected final Map&lt;String, MappedStatement&gt; mappedStatements = new StrictMap&lt;&gt;();
    protected final Map&lt;String, Cache&gt; caches = new StrictMap&lt;&gt;();
    protected final Map&lt;String, ResultMap&gt; resultMaps = new StrictMap&lt;&gt;();
    protected final Map&lt;String, ParameterMap&gt; parameterMaps = new StrictMap&lt;&gt;();
}</code></pre><h3>XML 解析流程</h3>
<pre><code class="language-java">// 1. 解析 mybatis-config.xml
XMLConfigBuilder configBuilder = new XMLConfigBuilder(inputStream);
Configuration config = configBuilder.parse();

// 2. 解析 Mapper XML
XMLMapperBuilder mapperBuilder = new XMLMapperBuilder(inputStream, config, resource);
mapperBuilder.parse();

// 3. 构建 SqlSessionFactory
SqlSessionFactory factory = new DefaultSqlSessionFactoryBuilder().build(config);</code></pre><h3>注解解析</h3>
<pre><code class="language-java">@Select("SELECT * FROM user WHERE id = #{id}")
@Results({
    @Result(property = "id", column = "id"),
    @Result(property = "name", column = "user_name")
})
User selectById(@Param("id") Integer id);</code></pre><p>MapperAnnotationBuilder 解析注解，转换成 MappedStatement。</p>
<h2>十三、对日常开发的启发</h2>
<h3>设计模式应用</h3>
<ol>
<li><strong>接口优先，解耦模块</strong>：SqlSession 接口隐藏实现，换 Executor 零成本。业务中多用接口，易测试/扩展</li>
<li><strong>关注点分离</strong>：SQL 构建、参数、结果、事务、缓存各模块独立。避免 monolithic 代码</li>
<li><strong>可插拔设计</strong>：插件让扩展 non-invasive。业务用 AOP 类似</li>
<li><strong>装饰器模式</strong>：CachingExecutor、各种 Cache 装饰器</li>
<li><strong>策略模式</strong>：多种 Executor 实现，运行时选择</li>
<li><strong>模板方法</strong>：BaseExecutor 定义执行模板，子类实现具体逻辑</li>
</ol>
<h3>架构思想</h3>
<ol>
<li><strong>动态静态平衡</strong>：静态高性能，动态灵活。项目中混用</li>
<li><strong>抽象复杂性</strong>：反射/OGNL/TypeHandler 藏细节。业务抽象工具类</li>
<li><strong>透明执行</strong>：业务无感知 JDBC，专注逻辑</li>
<li><strong>配置驱动</strong>：XML/注解配置，代码零侵入</li>
<li><strong>缓存分层</strong>：一级/二级缓存，不同粒度不同策略</li>
</ol>
<h3>实战经验总结</h3>
<pre><code class="language-java">// 1. 善用批量操作
SqlSession batchSession = factory.openSession(ExecutorType.BATCH);
for (User user : users) {
    batchSession.insert("insertUser", user);
}
batchSession.commit();

// 2. 合理使用缓存
@CacheNamespace(
    size = 512,
    flushInterval = 60000,
    eviction = LRU.class,
    readWrite = false
)

// 3. 自定义类型处理
@MappedTypes({MyEnum.class})
@MappedJdbcTypes({JdbcType.VARCHAR})
public class MyEnumTypeHandler implements TypeHandler&lt;MyEnum&gt; {
    // 实现类型转换逻辑
}

// 4. 插件扩展
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare"))  
public class SqlPrintPlugin implements Interceptor {
    // 打印执行的 SQL
}</code></pre><h2>十四、进阶主题</h2>
<h3>MyBatis-Spring 集成</h3>
<pre><code class="language-java">@Configuration
@MapperScan("com.example.mapper")
public class MyBatisConfig {
    
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        return factory.getObject();
    }
    
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory factory) {
        return new SqlSessionTemplate(factory);
    }
}</code></pre><h3>分页插件实现原理</h3>
<pre><code class="language-java">@Intercepts(@Signature(type = Executor.class, method = "query"))
public class PageInterceptor implements Interceptor {
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameter = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        
        if (rowBounds != RowBounds.DEFAULT) {
            // 构造 COUNT SQL
            String countSql = "SELECT COUNT(*) FROM (" + originalSql + ") tmp_count";
            // 构造分页 SQL  
            String pageSql = originalSql + " LIMIT " + rowBounds.getOffset() + "," + rowBounds.getLimit();
            
            // 先查总数，再查分页数据
            // ...
        }
        
        return invocation.proceed();
    }
}</code></pre>
      <p style='text-align: right'>
      <a href='https://liuyaowen.cn/posts/default/20250906#comments'>看完了？说点什么呢</a>
      </p>
    ]]>
    </content:encoded>
  <guid isPermaLink="false">68bbcc4729f528c76ad1ec79</guid>
  <category>posts</category>
<category>技术</category>
 </item>
  
</channel>
</rss>