<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Yibin’s Blog</title>
        <link>https://yibin.dev/</link>
        <description>记录与分享🧀</description>
        <lastBuildDate>Tue, 16 Jun 2026 16:00:41 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>zh-CN</language>
        <copyright>All rights reserved 2026, Yibin</copyright>
        <item>
            <title><![CDATA[一条配置查询，搞瘫了整个服务]]></title>
            <link>https://yibin.dev/article/37960b50-99a4-80a3-b385-deff53e32068</link>
            <guid>https://yibin.dev/article/37960b50-99a4-80a3-b385-deff53e32068</guid>
            <pubDate>Mon, 08 Jun 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div id="notion-article" class="mx-auto overflow-hidden "><main class="notion light-mode notion-page notion-block-37960b5099a480a3b385deff53e32068"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><div class="notion-text notion-block-37960b5099a48035b726ef038b04fd91">那天下午，监控突然炸了。</div><div class="notion-text notion-block-37960b5099a480c89dc7dec00cb48b5b">核心服务在短时间内批量报 <code class="notion-inline-code">QueryTimeoutException</code>。我打开日志一看，满屏都是同一类错误：</div><div class="notion-text notion-block-37960b5099a480fdbca4c6534174e6fd">三个数字一模一样：active 16，maxActive 16，runningSqlCount 16。连接池被打满了，一个不剩。</div><div class="notion-text notion-block-37960b5099a480ab8693edde9db15dce">更诡异的是，报错的 SQL 五花八门：查用户订阅的 COUNT、查红点状态的 SELECT、查用户标签的查询……看起来毫无关联的业务全部超时。</div><div class="notion-text notion-block-37960b5099a480d39ed6e2f515c04c99">出问题了。但问题不在这些报错的 SQL。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37960b5099a4801a9c20dbb29208dbee" data-id="37960b5099a4801a9c20dbb29208dbee"><span><div id="37960b5099a4801a9c20dbb29208dbee" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37960b5099a4801a9c20dbb29208dbee" title="1. 先找谁占住了连接"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>1. 先找谁占住了连接</b></span></span></h3><div class="notion-text notion-block-37960b5099a4800489f7e09335812f82">连接池只有 16 个连接。既然全部被占，那一定有人在占着不放。</div><div class="notion-text notion-block-37960b5099a48030b8b5e8a21325dd44">我把每条超时异常的 Cause 链追到底，发现真正占住连接的，始终是同一条 SQL：</div><div class="notion-text notion-block-37960b5099a480fa8534c2834a8a2cc5">一条配置查询，把 16 个连接全部霸占。其余所有业务查询排队等 5 秒后超时，形成连锁故障。</div><div class="notion-text notion-block-37960b5099a48066976fd57b333ca981">这就怪了。配置查询按理说应该是毫秒级返回的东西，怎么会把连接占住不还？</div><div class="notion-text notion-block-37960b5099a480aab46decb1754c243b">先还原一下调用链路。故障涉及两个服务：</div><div class="notion-text notion-block-37960b5099a480fbb237d208fe56bdd7">正常流程不复杂：上游消费 MQ 消息，先查本地缓存，命中直接返回；没命中就 RPC 调下游查库，再写缓存。</div><div class="notion-text notion-block-37960b5099a480e381c4f20c701b312f">看起来挺合理。但就是这个看起来合理的链路，藏了三个问题。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-37960b5099a480d5abb0d15996f8204f"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A543daf25-b391-44cf-a80e-d65dd3a433a4%3Aimage.png?table=block&amp;id=37960b50-99a4-80d5-abb0-d15996f8204f&amp;t=37960b50-99a4-80d5-abb0-d15996f8204f" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-37960b5099a480e2bfc6c04b7fd5f53b"><em>图 1：调用链路与三个隐藏问题。</em></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37960b5099a480919642ea88439ede28" data-id="37960b5099a480919642ea88439ede28"><span><div id="37960b5099a480919642ea88439ede28" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37960b5099a480919642ea88439ede28" title="2. 第一个问题：查询根本没走索引"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>2. 第一个问题：查询根本没走索引</b></span></span></h3><div class="notion-text notion-block-37960b5099a4808a9808d2df418a78ed">我先去看了 <code class="notion-inline-code">config_table</code> 的索引结构：</div><table class="notion-simple-table notion-block-37960b5099a48006947ef97c6b1fbe0c"><tbody><tr class="notion-simple-table-row notion-simple-table-header-row notion-block-37960b5099a480c1a9f4d424a2830610"><td class="" style="width:177px"><div class="notion-simple-table-cell">索引</div></td><td class="" style="width:462.875px"><div class="notion-simple-table-cell">列</div></td></tr><tr class="notion-simple-table-row notion-block-37960b5099a4804fb872cb374af8f325"><td class="" style="width:177px"><div class="notion-simple-table-cell">PRIMARY KEY</div></td><td class="" style="width:462.875px"><div class="notion-simple-table-cell">id</div></td></tr><tr class="notion-simple-table-row notion-block-37960b5099a4806091d2dd2f2142a12a"><td class="" style="width:177px"><div class="notion-simple-table-cell">UNIQUE KEY</div></td><td class="" style="width:462.875px"><div class="notion-simple-table-cell">(code, category_id)</div></td></tr></tbody></table><div class="notion-text notion-block-37960b5099a480acaa24e3999f98fd54">表上有个联合唯一索引 <code class="notion-inline-code">(code, category_id)</code>。但查询条件是 <code class="notion-inline-code">WHERE category_id = ?</code>，没有带 <code class="notion-inline-code">code</code>。</div><div class="notion-text notion-block-37960b5099a4800ab32bf4dc2159fbfe">根据 MySQL 最左前缀原则，这个联合索引的最左列是 <code class="notion-inline-code">code</code>。只查 <code class="notion-inline-code">category_id</code> 不走索引，每次都是全表扫描。</div><div class="notion-text notion-block-37960b5099a48067b52dcacea1cb68c7">表数据量小的时候，全表扫描也无所谓，几毫秒就过了。但数据量涨上去以后，一条本应毫秒级返回的查询变成了秒级操作。而这期间，数据库连接一直被占用。</div><div class="notion-text notion-block-37960b5099a480f39064e15a32dff7d0">不过到这里我还有一个疑问：就算全表扫描要几秒钟，那也就占一个连接吧？为什么 16 个连接全部被同一类 SQL 占满？</div><div class="notion-blank notion-block-37960b5099a48080b611cd505fd75e1c"> </div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-37960b5099a4803c9565f6ab21cf3948"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A9178f4f6-b487-418f-9d07-6abf693cae16%3Aimage.png?table=block&amp;id=37960b50-99a4-803c-9565-f6ab21cf3948&amp;t=37960b50-99a4-803c-9565-f6ab21cf3948" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-37960b5099a480f98682d46d76f9c434"><em>图 2：联合索引 (code, category_id)，跳过 code 直接查 category_id 不走索引。</em></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37960b5099a480ee97a2d00d1b978de9" data-id="37960b5099a480ee97a2d00d1b978de9"><span><div id="37960b5099a480ee97a2d00d1b978de9" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37960b5099a480ee97a2d00d1b978de9" title="3. 第二个问题：SELECT * 拉了大字段"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>3. 第二个问题：SELECT * 拉了大字段</b></span></span></h3><div class="notion-text notion-block-37960b5099a480a7878febbfa42ee60c">我接着看了下表结构。<code class="notion-inline-code">config_table</code> 里有一个 TEXT 类型的 <code class="notion-inline-code">detail</code> 字段，存的是大段 JSON 配置，动辄几十 KB。</div><div class="notion-text notion-block-37960b5099a480369252ecd5db2fe299">而查询代码是这样的：</div><div class="notion-text notion-block-37960b5099a480ddbee8f53525705a64">没有用 <code class="notion-inline-code">.select()</code> 限定列，默认查全部字段。全表扫描，加上每次拖几十 KB 的 TEXT 大字段，单条查询比正常慢了不止一倍。</div><div class="notion-text notion-block-37960b5099a480ad838cddaa1655440d">慢上加慢。</div><div class="notion-text notion-block-37960b5099a480a29772cff98affc644">但这也说不通。慢 SQL 再慢，如果并发量不高，也不至于把整个连接池打满。一定还有东西在放大并发。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37960b5099a480119211f2e939555d09" data-id="37960b5099a480119211f2e939555d09"><span><div id="37960b5099a480119211f2e939555d09" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37960b5099a480119211f2e939555d09" title="4. 第三个问题：缓存击穿"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>4. 第三个问题：缓存击穿</b></span></span></h3><div class="notion-text notion-block-37960b5099a480f4bd30d222db842dd7">继续往上追，我看到了上游服务的缓存代码：</div><div class="notion-text notion-block-37960b5099a4809cbdeefefdc2fff005">看到 <code class="notion-inline-code">getIfPresent()</code> + 手动 <code class="notion-inline-code">put()</code> 这个组合，我心里咯噔了一下。</div><div class="notion-text notion-block-37960b5099a480d19a6fd93963171ab0">这个写法不是原子操作。15 分钟 TTL 到期那一瞬间，50 个线程同时走到 <code class="notion-inline-code">getIfPresent()</code>，全部拿到 null。然后全部穿透到 RPC，全部打到数据库。</div><div class="notion-text notion-block-37960b5099a480a58544fcf829b11e39">而且上游有多个实例，每个实例的 Caffeine 缓存独立，过期时间接近。也就是说多个实例几乎同时发生缓存失效，压力被进一步放大。</div><div class="notion-text notion-block-37960b5099a480ebbc0fcb37acb4fc42">到这里，故障的全貌就清楚了。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-37960b5099a4805e995be70ac6bde368"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3Ab7bb131a-b3e1-45c1-860d-320eb1b83a0b%3Aimage.png?table=block&amp;id=37960b50-99a4-805e-995b-e70ac6bde368&amp;t=37960b50-99a4-805e-995b-e70ac6bde368" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-37960b5099a480d6805ff8a7aba5c1a3"><em>图 3：缓存击穿——TTL 到期瞬间，所有线程同时穿透到数据库。</em></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37960b5099a480d5b277f3397d251923" data-id="37960b5099a480d5b277f3397d251923"><span><div id="37960b5099a480d5b277f3397d251923" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37960b5099a480d5b277f3397d251923" title="5. 把时间线拼起来"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>5. 把时间线拼起来</b></span></span></h3><div class="notion-text notion-block-37960b5099a480ce8594ef3bad850a3c">三个问题串在一起，时间线是这样的：</div><div class="notion-text notion-block-37960b5099a48025a026faa0c5c890e8">不到 5 秒，一条配置查询搞瘫了所有业务。</div><div class="notion-text notion-block-37960b5099a480ac8da9f90a67082627">这三个问题，单独拿出哪一个都不算致命。索引缺失让查询变慢，SELECT * 让查询更慢，缓存击穿让慢查询并发暴增。三者叠加，一次普通的缓存过期就变成了全站雪崩。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-37960b5099a48061ac87e2d90bf57ce4"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A1aa91371-f754-42d0-97fb-77f0e1b7adc7%3Aimage.png?table=block&amp;id=37960b50-99a4-8061-ac87-e2d90bf57ce4&amp;t=37960b50-99a4-8061-ac87-e2d90bf57ce4" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-37960b5099a480e69170d95d65cbacd7"><em>图 4：从缓存到期到全站雪崩，不到 5 秒。</em></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37960b5099a48054af6ac223eb780e43" data-id="37960b5099a48054af6ac223eb780e43"><span><div id="37960b5099a48054af6ac223eb780e43" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37960b5099a48054af6ac223eb780e43" title="6. 怎么修"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>6. 怎么修</b></span></span></h3><div class="notion-text notion-block-37960b5099a480e78983f09136e2959e">P0 止血三件事：</div><div class="notion-text notion-block-37960b5099a480dd9ca7fd77019b46bc"><b>第一，加索引。</b> 这是最根本的一刀。</div><div class="notion-text notion-block-37960b5099a4808f9e30ce5342e4f64c">全表扫描变索引查找，查询时间从秒级降到毫秒级。单条查询不慢了，后面的问题就算还在，影响面也大幅缩小。</div><div class="notion-text notion-block-37960b5099a480c4b537d3b0a4cd7d09"><b>第二，修缓存击穿。</b> <code class="notion-inline-code">Caffeine.get(key, loader)</code> 对同一个 key 内部加锁，只有一个线程执行加载逻辑，其余线程等结果。彻底消除惊群效应。</div><div class="notion-text notion-block-37960b5099a480f3a872f55e0108a014"><b>第三，限定查询列。</b> 不查不需要的 TEXT 大字段。</div><div class="notion-text notion-block-37960b5099a480619423f4f4a0fa031c">P1 加固两件事。一是连接池 <code class="notion-inline-code">maxActive</code> 从 16 调到 50，提高容错水位。二是下游服务也加一层缓存——配置数据变化极低频，不应该每次 RPC 都查库。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37960b5099a480e69677e8bc05e74425" data-id="37960b5099a480e69677e8bc05e74425"><span><div id="37960b5099a480e69677e8bc05e74425" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37960b5099a480e69677e8bc05e74425" title="7. 回过头看"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>7. 回过头看</b></span></span></h3><div class="notion-text notion-block-37960b5099a480438572fc7ca4240466">这次复盘让我重新理解了&quot;防御性编程&quot;这五个字。</div><div class="notion-text notion-block-37960b5099a480129a3ef0a7ed9e5c1f">以前我觉得防御性编程就是处理好异常、写好单元测试、加好降级逻辑。但这次的问题，没有一个环节是&quot;异常&quot;的——索引没报错，SQL 没报错，缓存没报错，连接池也没报错。每个组件都在按设计工作，但合在一起，炸了。</div><div class="notion-text notion-block-37960b5099a4804296c7c6088806fb8b">防御性编程真正的含义，不是处理已知的异常，而是假设每个环节都可能出问题，然后确保问题不会扩散。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-37960b5099a48063be20c9cac0f52490"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A950573b2-f4c1-435f-99ec-b6a4a9eba5d5%3Aimage.png?table=block&amp;id=37960b50-99a4-8063-be20-c9cac0f52490&amp;t=37960b50-99a4-8063-be20-c9cac0f52490" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-37960b5099a48020a119d46e15356b8e"><em>图 5：三个单独看都不致命的问题，叠加在一起就是全站雪崩。</em></div><div class="notion-text notion-block-37960b5099a4808b9724f4b530896a9d">具体到这次，我觉得有几条值得记住：</div><ul class="notion-list notion-list-disc notion-block-37960b5099a48089af5ce963c58230d7"><li><b>写查询前先看索引。</b> 不是写完再补，是写之前就确认能走什么索引。联合索引的列顺序，决定了你的查询能不能用上。</li></ul><ul class="notion-list notion-list-disc notion-block-37960b5099a4801fac43d34c51ccb5d8"><li><b>SELECT * 在有 TEXT/BLOB 字段的表上是定时炸弹。</b> 尤其是通过 RPC 传输的场景，大字段的代价每一跳都在放大。</li></ul><ul class="notion-list notion-list-disc notion-block-37960b5099a4807e8597fd5263e18d01"><li><code class="notion-inline-code"><b>getIfPresent</b></code><b> + </b><code class="notion-inline-code"><b>put</b></code><b> 在高并发下等于没缓存。</b> 用 <code class="notion-inline-code">get(key, loader)</code> 或者分布式锁，别自己手动作缓存加载。</li></ul><ul class="notion-list notion-list-disc notion-block-37960b5099a4808b9b2dfffa868069cc"><li><b>连接池是所有业务共享的资源。</b> 一条慢 SQL 可以拖垮整个服务。多实例本地缓存不共享，N 个实例就是 N 倍穿透压力。</li></ul><div class="notion-text notion-block-37960b5099a480bc8811e9c5f7a583b2">故障的本质很少是单一原因。往往是几个&quot;不大&quot;的问题，在某个时间点刚好凑到一起，一个引爆另一个。你能做的不是消灭所有问题——那不可能。你能做的是，别让一个小问题有扩散成全站雪崩的机会。</div></main></div>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[AI 生成的代码有 bug，修还是重来？我的三层判断法]]></title>
            <link>https://yibin.dev/article/37860b50-99a4-801d-aba4-c846cacf36bf</link>
            <guid>https://yibin.dev/article/37860b50-99a4-801d-aba4-c846cacf36bf</guid>
            <pubDate>Sun, 07 Jun 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div id="notion-article" class="mx-auto overflow-hidden "><main class="notion light-mode notion-page notion-block-37860b5099a4801daba4c846cacf36bf"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><div class="notion-text notion-block-37860b5099a480f59137d5daf0d264f3">最近和团队其他深度使用AI的同事聊到一个很有意思的问题。</div><div class="notion-text notion-block-37860b5099a48091b1dfd7ba3ccb9746">AI 生了一段代码，大部分能跑，但有几个 bug。这时候你怎么选——手工修掉？继续写 prompt 让 AI 改？还是直接把这段代码回滚，重新生成？</div><div class="notion-text notion-block-37860b5099a480a5bbfee7bc731fa63d">每个人反应都不太一样。有的人习惯手工修，有的人第一反应是追问 AI，有的人直接回滚重来。</div><div class="notion-text notion-block-37860b5099a48019bdbed812f5ed7ef1">用了大半年 AI Coding 之后，我慢慢摸索出一套自己的判断标准。核心就一件事：<b>先判问题大小，再决定怎么动手。</b></div><div class="notion-text notion-block-37860b5099a48047aabaf8de40ce91c6">我把问题分成三层。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-37860b5099a48052aacec6a4fbba2809"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3Adae01332-dfdb-43cc-b298-2ca6900b16e8%3Aimage.png?table=block&amp;id=37860b50-99a4-8052-aace-c6a4fbba2809&amp;t=37860b50-99a4-8052-aace-c6a4fbba2809" alt="notion image" loading="lazy" decoding="async"/></div></figure><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37860b5099a48068b9f1fea175e80009" data-id="37860b5099a48068b9f1fea175e80009"><span><div id="37860b5099a48068b9f1fea175e80009" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37860b5099a48068b9f1fea175e80009" title="第一层：小问题，手工修"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>第一层：小问题，手工修</b></span></span></h3><div class="notion-text notion-block-37860b5099a48070a7acc5ea6f14cea0">什么叫小问题？空指针没判、标签没闭合、变量名写错了——这种一眼能看穿的。</div><div class="notion-text notion-block-37860b5099a480bea618fd309367aeea">这类 bug 我从不写 prompt。为什么？因为<b>写 prompt 描述问题的功夫，你已经修完了。</b></div><div class="notion-text notion-block-37860b5099a480ce9be2dc1a5e50ac94">你想想这个过程：你得把 bug 现象描述清楚 → 等 AI 生成 → 看改没改对 → 可能还得再调一轮。一个空指针，手动改 5 秒。走一轮 prompt，至少两分钟。这笔账太好算了。</div><div class="notion-text notion-block-37860b5099a480d1a822f5e3ea32de87">小问题的判断标准很简单：<b>你一眼就知道哪里错了，而且你知道怎么改。</b></div><div class="notion-text notion-block-37860b5099a48045a4c5f4845bf90081">这种情况还去写 prompt，不是用好 AI，是被 AI 绑架。时间浪费了，token 也白烧。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37860b5099a4804fa2caf317cd43582d" data-id="37860b5099a4804fa2caf317cd43582d"><span><div id="37860b5099a4804fa2caf317cd43582d" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37860b5099a4804fa2caf317cd43582d" title="第二层：中等问题，看熟悉度"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>第二层：中等问题，看熟悉度</b></span></span></h3><div class="notion-text notion-block-37860b5099a4802c9830eb18a2ec9ef8">这是最纠结的一层。</div><div class="notion-text notion-block-37860b5099a48041b688d6fa911d3fc1">比如边界条件没处理、并发逻辑有问题、异常路径没覆盖——不是一眼能看穿的 bug，但也不是架构级的硬伤。</div><div class="notion-text notion-block-37860b5099a4803c82b2f18d789a1027">遇到这种，我的判断标准是两条：</div><div class="notion-text notion-block-37860b5099a4808cb066fbeed44cf428"><b>第一，这块代码我熟不熟？</b> 熟的话，大概率手工修。因为我知道改哪里、怎么改、改完会影响什么。不熟的话，可以尝试一两轮 prompt。</div><div class="notion-text notion-block-37860b5099a480bcb4e2c8453e8a2e33"><b>第二，是不是核心链路？</b> 如果是支付、鉴权这种高危模块，我更倾向于手工修——出了问题你没法跟领导说&quot;这是 AI 写的&quot;。如果是边缘功能，让 AI 改一两轮问题不大。</div><div class="notion-text notion-block-37860b5099a480a4a474f15998fd31a4">但这里有个坑，我重点说一下。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37860b5099a48014acb7e9d4d2be6142" data-id="37860b5099a48014acb7e9d4d2be6142"><span><div id="37860b5099a48014acb7e9d4d2be6142" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37860b5099a48014acb7e9d4d2be6142" title="最大的坑：追问死循环"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>最大的坑：追问死循环</b></span></span></h3><div class="notion-text notion-block-37860b5099a480b99d36eb3c0817f832">中等问题上，最容易犯的错是<b>无底线追问</b>。</div><div class="notion-text notion-block-37860b5099a4800ea55dccaaa60df76c">修了 A，AI 给你冒出 B。修了 B，冒出 C。修完 C 回头一看，A 又坏了。</div><div class="notion-text notion-block-37860b5099a4800ba382e761dcbcc70d">为什么？因为 AI 在迭代修改时会继承错误的上下文。你每追问一轮，prompt 里就多一层历史包袱。改到第三四轮的时候，AI 已经在垃圾堆上盖房子了。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-37860b5099a480cfb00bc27a1795c25e"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3Abd409205-11f0-41bd-a669-2b44c642bfac%3Aimage.png?table=block&amp;id=37860b50-99a4-80cf-b00b-c27a1795c25e&amp;t=37860b50-99a4-80cf-b00b-c27a1795c25e" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-37860b5099a480029457f095dc1b55ba">这时候有一个办法可以试：<b>换模型。</b></div><div class="notion-text notion-block-37860b5099a4805b853fcbbc6d46c8bb">不同模型擅长的领域真的不一样。我自己的体感，DeepSeek V4 Pro 在处理一些逻辑推理和数据处理类的代码上很灵光，但遇到复杂架构设计或者需要深度理解业务上下文的任务，就容易跑偏。反过来，MiniMax-M3 在某些领域很稳，但换到 DeepSeek 擅长的那类问题，表现又不太够看。</div><div class="notion-text notion-block-37860b5099a480f3a242c7de428a0351">这不是玄学。每个模型训练时喂的数据不一样，推理的路数也不一样，舒适区天然不同。你在这个模型上追了 3 轮还转不出来，不一定是你 prompt 的问题，可能就是这个模型不擅长这类任务。换个模型，同样的 prompt，一遍就过了。</div><div class="notion-text notion-block-37860b5099a480869d06f6397f3a24fc">所以遇到追问两三轮还没搞定的时候，别死磕。切到另一个模型试试，有时候比改 prompt 更管用。</div><div class="notion-text notion-block-37860b5099a4800d8ff0d8270603c27b">如果换了模型还不对，那问题大概率不在模型上。我的止损线很明确：<b>追 3 轮还不对，立刻回滚，重新写 prompt 生成整个功能。</b></div><div class="notion-text notion-block-37860b5099a4803eba52dd7e798e34db">这话说起来简单，做起来难。因为人都有沉没成本心理——&quot;我已经花了半小时修了，现在回滚不是白费了？&quot;</div><div class="notion-text notion-block-37860b5099a48063ad0ccfb4cb94a51a">但实际情况是，你继续追下去，可能再花一小时，最后还是得回滚。</div><div class="notion-text notion-block-37860b5099a480e588e3fcad6cf49937">我见过最典型的案例：一个同事修了一下午的 AI 代码，来回追了七八轮，最后烦了，回滚重写 prompt，10 分钟搞定。前面那三小时，全是&quot;不舍得&quot;的代价。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37860b5099a480169500c7aed5756830" data-id="37860b5099a480169500c7aed5756830"><span><div id="37860b5099a480169500c7aed5756830" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37860b5099a480169500c7aed5756830" title="第三层：大问题，果断回滚"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>第三层：大问题，果断回滚</b></span></span></h3><div class="notion-text notion-block-37860b5099a480c6af1ad7def3252c62">代码分层不对、整体设计方向跑偏、或者 AI 理解错了需求——这种叫大问题。</div><div class="notion-text notion-block-37860b5099a480aead02d8a0b03b1a5c">大问题不要修，不要追，直接回滚。因为<b>根是歪的，修再多枝叶都没用。</b></div><div class="notion-text notion-block-37860b5099a480218a98d3b3d813a36d">重新生成之前，把 prompt 里的歧义清掉。很多时候 AI 写偏了，不是它不行，是你没说清楚。把需求拆细、把约束写明、给一两个正反例——第二次出来的东西通常好很多。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37860b5099a48033b086c0b415f7d9fb" data-id="37860b5099a48033b086c0b415f7d9fb"><span><div id="37860b5099a48033b086c0b415f7d9fb" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37860b5099a48033b086c0b415f7d9fb" title="判断问题大小，本身就是能力"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>判断问题大小，本身就是能力</b></span></span></h3><div class="notion-text notion-block-37860b5099a48055a2d1df6126d5b639">这套三层判断法说起来不复杂，但执行起来有个前提：<b>你得能快速判断一个 bug 属于哪一层。</b></div><div class="notion-text notion-block-37860b5099a4807990ffe5beb5ce211b">这个判断力不是天生的。你得对你用的语言、框架、业务逻辑有一定熟悉度，才能在一两分钟内做出判断。</div><div class="notion-text notion-block-37860b5099a48006aff7e7f2ef820ed5">有些同学一看到 bug 就开始追着 AI 改，改到一半才发现是个大问题。等反应过来，时间已经烧掉了。有这个时间，自己修早完事了。</div><div class="notion-text notion-block-37860b5099a4805db876e7c2c37efc94">这就是 AI Coding 里很微妙的一点：<b>AI 省的是&quot;写&quot;的时间，不是&quot;判&quot;的时间。</b> 判断还得靠人。</div><div class="notion-text notion-block-37860b5099a480e3ad60d86cb7579c2e">练多了之后，你会形成一种直觉——看到 bug 的第一反应不是&quot;让 AI 修&quot;，而是&quot;这是哪类问题&quot;。到这个状态，人机协同的效率才算真正上来了。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-37860b5099a4802cb690f1a00d122a49" data-id="37860b5099a4802cb690f1a00d122a49"><span><div id="37860b5099a4802cb690f1a00d122a49" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37860b5099a4802cb690f1a00d122a49" title="最后"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>最后</b></span></span></h3><div class="notion-text notion-block-37860b5099a480b69e21e83f17486307">我们团队近期达成了个共识：AI 能不能搞定一个项目，不看项目复杂度，不看代码量，看<b>验证成本</b>。</div><div class="notion-text notion-block-37860b5099a480f4bac6f046d0f01ef3">什么叫验证成本？就是 AI 生成完代码之后，你验证它对不对要花多少时间。验证成本低的（比如跑个测试就完事），AI 可以放开用。验证成本高的（比如核心支付逻辑，错了就是事故），人必须盯紧。</div><div class="notion-text notion-block-37860b5099a4805dbb86eede35f30069">所以回到最开始的问题：AI 生成的代码有 bug，修还是重来？</div><div class="notion-text notion-block-37860b5099a4801abaafdd4d3424ba69">答案不是绝对的。但它取决于你的判断，不取决于 bug 本身。<b>人机协同，人判机写。判在前，写在后面。</b></div></main></div>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[四个装机脚本，新机到手十分钟搞定]]></title>
            <link>https://yibin.dev/article/20560b50-99a4-8067-8119-cc2a5ce8455a</link>
            <guid>https://yibin.dev/article/20560b50-99a4-8067-8119-cc2a5ce8455a</guid>
            <pubDate>Sat, 06 Jun 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div id="notion-article" class="mx-auto overflow-hidden "><main class="notion light-mode notion-page notion-block-20560b5099a480678119cc2a5ce8455a"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-37760b5099a480178fe5dee0ba1b0190" data-id="37760b5099a480178fe5dee0ba1b0190"><span><div id="37760b5099a480178fe5dee0ba1b0190" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37760b5099a480178fe5dee0ba1b0190" title="四个装机脚本，新机到手十分钟搞定"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>四个装机脚本，新机到手十分钟搞定</b></span></span></h2><div class="notion-text notion-block-37760b5099a48090a9e6e39719c30f10">每次拿到一台新 VPS 或者重装系统之后，有四件事几乎必做——换源、装环境、跑测评、开虚拟机。这四个脚本一直在用，一条命令搞定，不折腾。</div><div class="notion-text notion-block-37760b5099a480a383b0e16e1238a2e2">我们一个一个说。</div><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-37760b5099a480e38f4ffb6e23090bbc" data-id="37760b5099a480e38f4ffb6e23090bbc"><span><div id="37760b5099a480e38f4ffb6e23090bbc" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37760b5099a480e38f4ffb6e23090bbc" title="换源：LinuxMirrors"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>换源：LinuxMirrors</b></span></span></h3><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-37760b5099a4806d9463cb7406cb3008"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A30dc00fc-60a8-4653-9bbe-3e757dbc4617%3Aimage.png?table=block&amp;id=37760b50-99a4-806d-9463-cb7406cb3008&amp;t=37760b50-99a4-806d-9463-cb7406cb3008" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-37760b5099a480509b04dd22386f2425">新机到手第一件事是什么？换源。</div><div class="notion-text notion-block-37760b5099a4806f84b1fa518ace7acc">不换源的后果很直接：apt 或 yum 下载速度几十 KB/s，装个 Docker 能等十分钟。换了阿里或清华源，同样的操作十秒完事。</div><div class="notion-text notion-block-37760b5099a4805aa6dbd0d080bc6c1b">LinuxMirrors 是目前覆盖最广的换源脚本。支持 26 个发行版，从 Debian 8 到 Ubuntu 26，CentOS 7 到 Fedora 44。甚至 Arch、Gentoo、NixOS、openKylin 这些偏门发行版都覆盖了。</div><div class="notion-text notion-block-37760b5099a480a68003d3ce4be0f5c7">一条命令：</div><div class="notion-text notion-block-37760b5099a48039a4d8e6157ab23c0f">脚本会自动检测你的系统，然后列出可用的国内镜像源让你选。不用记任何参数。</div><div class="notion-text notion-block-37760b5099a480888117d8f317298127">还有两个加分项：</div><div class="notion-text notion-block-37760b5099a4802cad13f28f9904a649">第一，它自带 Docker 安装和换源。装 Docker 的同时帮你把镜像加速器也配好，不用再去 <code class="notion-inline-code">/etc/docker/daemon.json</code> 里手写。如果 Docker 已经装好了，加 <code class="notion-inline-code">--only-registry</code> 参数只换源：</div><div class="notion-text notion-block-37760b5099a480f4b2fed06692f31a99">第二，提供轻量版脚本。如果你的机器内存紧张或者网络很差，用 Lite 版更稳。</div><div class="notion-text notion-block-37760b5099a480e2946bfe31d07a5eb8">GitHub 7.6k stars，不算多，但对这个品类来说已经是最高的了。</div><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-37760b5099a4804cb631cf0cdf9933de" data-id="37760b5099a4804cb631cf0cdf9933de"><span><div id="37760b5099a4804cb631cf0cdf9933de" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37760b5099a4804cb631cf0cdf9933de" title="系统修复与环境安装：one-click-installation-script"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>系统修复与环境安装：one-click-installation-script</b></span></span></h3><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-37760b5099a480649fe2fb5029cdabc9"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A99f64a7e-9d76-4283-bfe1-9efdc81fbe66%3Aimage.png?table=block&amp;id=37760b50-99a4-8064-9fe2-fb5029cdabc9&amp;t=37760b50-99a4-8064-9fe2-fb5029cdabc9" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-37760b5099a480228b80db5ecf35d08e">换完源之后，通常还有一堆零碎事要做：修时间、配网络、装开发环境。</div><div class="notion-text notion-block-37760b5099a48030ae58f6a64b4cbb1b">这套脚本的作者 SpiritLHL 把常见的修复和安装操作打包成了一个工具箱。每个脚本独立，按需跑。</div><div class="notion-text notion-block-37760b5099a4801eb587d1561523a3f6">修复类的几个高频场景：</div><ul class="notion-list notion-list-disc notion-block-37760b5099a48071824ec33bf12c7793"><li>apt 源锁死或公钥缺失 → 一条命令修好</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a48021adc3fb63cb2f8500"><li>系统时间不准 → 自动装 chronyd 同步</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a480f496c4eae343852e0f"><li>纯 IPv6 机器没网络 → 加 nat64 通道</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a480d9a628c3a8d5c13055"><li>journal 日志撑爆磁盘 → 自动调大小</li></ul><div class="notion-text notion-block-37760b5099a48016967ee69ddd9c6c15">环境安装类的：</div><ul class="notion-list notion-list-disc notion-block-37760b5099a480b49bd5f3091e365594"><li>Jupyter 环境（Miniconda3 + JupyterLab，适合按小时计费的 GPU 机器）</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a480b2a1fcf33272d3bf52"><li>Go、Rust、C++ 开发环境</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a48022af18fac820d50cee"><li>vnstat 流量监控</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a48020b170fbe2eb58f8aa"><li>FileBrowser（Web 文件管理器，端口 3030）</li></ul><div class="notion-text notion-block-37760b5099a480288e8af2c813aea633">还有一个挺实用的：一键卸载云服务器自带的监控 agent。阿里云、腾讯云、华为云、甲骨文、UCloud、京东云都支持。买过国内 VPS 的都知道，这些监控 agent 默认挂着，吃资源不说，看着就烦。</div><div class="notion-text notion-block-37760b5099a480d7a810ff1cece5f9d4">用法很直接，需要哪个跑哪个。比如修 apt 源：</div><div class="notion-text notion-block-37760b5099a4808299ebe1a8b74fae4b">卸云监控：</div><div class="notion-text notion-block-37760b5099a4807db087c0d1f4b27ace">国内机器把 <code class="notion-inline-code">raw.githubusercontent.com</code> 换成 <code class="notion-inline-code">cdn.spiritlhl.net/https://raw.githubusercontent.com</code> 走 CDN 加速。</div><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-37760b5099a4809aa537d7a7575d2f47" data-id="37760b5099a4809aa537d7a7575d2f47"><span><div id="37760b5099a4809aa537d7a7575d2f47" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37760b5099a4809aa537d7a7575d2f47" title="跑分测评：融合怪"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>跑分测评：融合怪</b></span></span></h3><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-37760b5099a480fdad70e4ca45fe33dc"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A3c9a23b8-3c6c-4bb1-bd69-034aa565e320%3Aimage.png?table=block&amp;id=37760b50-99a4-80fd-ad70-e4ca45fe33dc&amp;t=37760b50-99a4-80fd-ad70-e4ca45fe33dc" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-37760b5099a4800cb7e2da3113ef7fe6">机器到手，环境装好，下一步该干嘛？跑个分看看这机器到底几斤几两。</div><div class="notion-text notion-block-37760b5099a480d5b62ce4f15d6aa1d1">融合怪（ecs）是目前 VPS 圈最常用的综合测评脚本。每次买新机都会跑一遍，看看商家有没有虚标。</div><div class="notion-text notion-block-37760b5099a48091844ffd85df17d4f3">它把 bench.sh、superbench、yabs、LemonBench 这几个主流工具的测试项整合到了一起，自己优化了不少细节。你可以只跑单项，也可以一键跑全套。不用挨个找脚本。</div><div class="notion-text notion-block-37760b5099a4802da4d3dc9fbf3a58a5">测什么：</div><ul class="notion-list notion-list-disc notion-block-37760b5099a48096932ec69dbfc3cbed"><li>系统信息：CPU 型号、内存、磁盘、虚拟化类型、NAT 类型</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a48030bf46c094f768115a"><li>CPU 跑分：默认 sysbench，可选 Geekbench 4/5/6</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a48056b0c7cc77cc665ac7"><li>内存和磁盘 I/O：DD 快速测 + FIO 精准测</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a4803aa713c90a89582a52"><li>流媒体解锁：Netflix、Disney+、TikTok（两个版本交叉验证）</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a480e5bf73e9718ebac0f5"><li>三网回程路由：电信、联通、移动各线路延迟</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a480d6a390c3f6e4bd114a"><li>IP 质量检测：15 家黑名单数据库查询，含 DNS 黑名单</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a480828dc3dbad3e17d9ab"><li>邮件端口 25 是否开放（能不能搭邮局）</li></ul><ul class="notion-list notion-list-disc notion-block-37760b5099a480328f00fb178319d285"><li>Speedtest 三网测速</li></ul><div class="notion-text notion-block-37760b5099a480568c34d5460c67c426">跑完之后自动上传结果到 pastebin，给你一个分享链接。同时本地也会保存一份 <code class="notion-inline-code">test_result.txt</code>。</div><div class="notion-text notion-block-37760b5099a48001a3f6f0957bdc0aaa">一条命令：</div><div class="notion-text notion-block-37760b5099a48041aa93f91479d7eafb">跑完全套大概 10-15 分钟。想省时间的话，跑交互模式，只勾自己关心的项目。</div><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-37760b5099a4802ea969dcd4784d19a2" data-id="37760b5099a4802ea969dcd4784d19a2"><span><div id="37760b5099a4802ea969dcd4784d19a2" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37760b5099a4802ea969dcd4784d19a2" title="开虚拟机：PVE 一键部署"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>开虚拟机：PVE 一键部署</b></span></span></h3><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-37760b5099a4803e9b66dfdb3c1934fe"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A8e1c22c4-1b44-4fa7-add2-519168d25684%3Aimage.png?table=block&amp;id=37760b50-99a4-803e-9b66-dfdb3c1934fe&amp;t=37760b50-99a4-803e-9b66-dfdb3c1934fe" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-37760b5099a48075a92ccc599d9a55ae">如果你用的是 Proxmox VE，这个脚本能省大把时间。</div><div class="notion-text notion-block-37760b5099a480f09bafd3a7fe6c971f">oneclickvirt/pve 做的事情很简单：在 PVE 上一键创建虚拟机或容器，支持 KVM 和 LXC 两种虚拟化。可以单独开一台，也可以批量开。CPU 核数、内存大小、磁盘容量、网络配置，全都通过环境变量控制。</div><div class="notion-text notion-block-37760b5099a48030a8cbd0569673dae0">比如批量开 3 台：</div><div class="notion-text notion-block-37760b5099a480adad16e97ab94bf555">不需要交互，全程自动跑完。</div><div class="notion-text notion-block-37760b5099a4808998e3e8f6aaf9ccb6">支持的系统也很全：Linux、Windows、macOS、Android 都能装。ARM64 和 AMD64 都兼容，包括 riscv 也在陆续适配。</div><div class="notion-text notion-block-37760b5099a48054955df95953358a3e">内外网端口转发和 NAT 也是自动配好的。开完就能用，不用再去防火墙里手动加规则。</div><div class="notion-text notion-block-37760b5099a4801dbeaec2639d8b0f4c">这个脚本跟上面三个同属 SpiritLHL 的生态。跟融合怪测评脚本配着用刚好——新机器开出来，跑一遍融合怪，看看性能有没有问题。</div><div class="notion-text notion-block-37760b5099a480fb9893c5b3906d7180">具体用法和命令在配套文档站 virt.spiritlhl.net 上，PVE 分区的说明很详细，照着跑就行。</div><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-37760b5099a4809eb301fe1ad47a512f" data-id="37760b5099a4809eb301fe1ad47a512f"><span><div id="37760b5099a4809eb301fe1ad47a512f" class="notion-header-anchor"></div><a class="notion-hash-link" href="#37760b5099a4809eb301fe1ad47a512f" title="总结"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>总结</b></span></span></h3><div class="notion-text notion-block-37760b5099a480fb9136d17e050d6125">四个脚本，覆盖了新机到手到正式用起来的主要环节：</div><table class="notion-simple-table notion-block-37760b5099a48023b95fe30bafe02f20"><tbody><tr class="notion-simple-table-row notion-simple-table-header-row notion-block-37760b5099a480759ffdd41cf2735887"><td class="" style="width:286px"><div class="notion-simple-table-cell">脚本</div></td><td class="" style="width:186.2890625px"><div class="notion-simple-table-cell">做什么</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">一句话</div></td></tr><tr class="notion-simple-table-row notion-block-37760b5099a4808d9c01e5f8ac170b8f"><td class="" style="width:286px"><div class="notion-simple-table-cell">LinuxMirrors</div></td><td class="" style="width:186.2890625px"><div class="notion-simple-table-cell">换源 + Docker</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">新机第一件事</div></td></tr><tr class="notion-simple-table-row notion-block-37760b5099a48075a871ed5a60895dee"><td class="" style="width:286px"><div class="notion-simple-table-cell">one-click-installation-script</div></td><td class="" style="width:186.2890625px"><div class="notion-simple-table-cell">系统修复 + 环境安装</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">零碎事按需跑</div></td></tr><tr class="notion-simple-table-row notion-block-37760b5099a480cba453f5aa06a04164"><td class="" style="width:286px"><div class="notion-simple-table-cell">融合怪 ecs</div></td><td class="" style="width:186.2890625px"><div class="notion-simple-table-cell">跑分测评</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">看看机器几斤几两</div></td></tr><tr class="notion-simple-table-row notion-block-37760b5099a480f193cfeff424e44a3f"><td class="" style="width:286px"><div class="notion-simple-table-cell">PVE 一键部署</div></td><td class="" style="width:186.2890625px"><div class="notion-simple-table-cell">开虚拟机</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">PVE 用户必备</div></td></tr></tbody></table><div class="notion-text notion-block-37760b5099a48019a0fade9d44728f4e">建议收藏，下次重装系统的时候翻出来直接复制粘贴。</div><div class="notion-text notion-block-37760b5099a4802e96e0dbfd372926a1">如果只装一台机器，手动敲一遍也无所谓。但如果经常折腾 VPS，或者一次要搞好几台，这四个脚本能帮你从半小时压到五分钟。</div></main></div>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[fastjson2 丢字段：不是 reader 吞了字段，是 writer 写错了路]]></title>
            <link>https://yibin.dev/article/36960b50-99a4-808b-a32b-dbe53793c66a</link>
            <guid>https://yibin.dev/article/36960b50-99a4-808b-a32b-dbe53793c66a</guid>
            <pubDate>Sat, 23 May 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div id="notion-article" class="mx-auto overflow-hidden "><main class="notion light-mode notion-page notion-block-36960b5099a4808ba32bdbe53793c66a"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><div class="notion-text notion-block-36960b5099a480b78eaff1dde8084b64">前面的排查文章【<b><a class="notion-link" href="https://yibin.dev/article/36960b50-99a4-804a-9ea2-e0a8c6112c14" target="_blank" rel="noopener noreferrer">线上 4 个 RPC 参数丢了 3 个字段，到底谁干的</a></b>】已经把问题收敛到了 fastjson2。</div><div class="notion-text notion-block-36960b5099a480a78130ef819e1c52a8">当时我用一个最小的 Dubbo <code class="notion-inline-code">provider + consumer</code> 工程，把线上路径复现出来。结论也很明确：锅不在业务代码，也不在 Dubbo 业务层本身，而是在 Dubbo 最终选中的 fastjson2 序列化路径。</div><div class="notion-text notion-block-36960b5099a4804c9378ee3aa5bfe8cc">但只说“问题落在 fastjson2”还不够。</div><div class="notion-text notion-block-36960b5099a48071b8bde2370ea0db24">下一步不能继续盯着 Dubbo 看了。Dubbo 在这条链路里更像一个把请求送到 fastjson2 的壳。真正要确认的是：把 Dubbo 完全拿掉，只保留它调用 fastjson2 时那组 writer / reader features，纯 JSONB 还能不能独立复现。</div><div class="notion-text notion-block-36960b5099a480769da3eb921b4d0e23">这篇我想把 4 件事讲清楚：</div><ol start="1" class="notion-list notion-list-numbered notion-block-36960b5099a480ab8fbfe14464b63854" style="list-style-type:decimal"><li>纯 JSONB 最小实验怎么搭。</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-36960b5099a480d3b8f3d3a33ba2aa12" style="list-style-type:decimal"><li>fastjson2 的写端和读端，分别在哪一步把问题坐实。</li></ol><ol start="3" class="notion-list notion-list-numbered notion-block-36960b5099a48064a62ff5a04c2c6482" style="list-style-type:decimal"><li>为什么最后应该修写端，不修读端。</li></ol><ol start="4" class="notion-list notion-list-numbered notion-block-36960b5099a480c882b2da86b6fd4c67" style="list-style-type:decimal"><li>我提给 fastjson2 的 PR，到底改了什么。</li></ol><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a4805591c7c442b020dab4" data-id="36960b5099a4805591c7c442b020dab4"><span><div id="36960b5099a4805591c7c442b020dab4" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4805591c7c442b020dab4" title="1. 先把 Dubbo 拿掉，只保留它那组 features"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>1. 先把 Dubbo 拿掉，只保留它那组 features</b></span></span></h3><div class="notion-text notion-block-36960b5099a480d490a5c54c17511c73">前一轮排查已经说明：Dubbo 在这条路径里更像调度员。它决定选哪个序列化实现，也决定调用 fastjson2 时带哪组 features。</div><div class="notion-text notion-block-36960b5099a480edbafbe662892cbeb5">那现在最干净的验证方式，就是把 Dubbo 拿掉，只保留这组 features。</div><div class="notion-text notion-block-36960b5099a480ecaf23d2f3a4171e7d">最小实验核心代码是这样的：</div><div class="notion-text notion-block-36960b5099a48075bc23d848b7f7f9ea">再准备两组数据：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a480cab2d5c356ae10e14b"><li>一组让 4 个 <code class="notion-inline-code">StatusQuery</code> 共享同一个 <code class="notion-inline-code">statusCodeSet</code></li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a480aeb763ca44d258798d"><li>一组让每个 <code class="notion-inline-code">StatusQuery</code> 各自 <code class="notion-inline-code">new HashSet&lt;&gt;(codeSet)</code></li></ul><div class="notion-text notion-block-36960b5099a4809c9c92d21790083e74">最后直接跑：</div><div class="notion-text notion-block-36960b5099a48030aa08dce8ce38a555">这里有两个点值得先说。</div><div class="notion-text notion-block-36960b5099a480de8eb7ebaa2c9bcca7">第一，我故意传了完整泛型 <code class="notion-inline-code">TypeReference&lt;Set&lt;StatusQuery&gt;&gt;()</code>，不是裸 <code class="notion-inline-code">Set.class</code>。因为我想先把“泛型擦除”这个嫌疑人摘掉。既然前面已经怀疑它不是根因，这里干脆先把条件给足。</div><div class="notion-text notion-block-36960b5099a4805bb0b5eb92167efdb0">第二，这里直接复用了 Dubbo 3.2.0 在 <code class="notion-inline-code">FastJson2ObjectOutput</code> / <code class="notion-inline-code">FastJson2ObjectInput</code> 里实际打开的 features。这样跑出来的结果，才有资格和 Dubbo 线上路径放在一起比较。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-36960b5099a48060b345ff6302c2849e"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A0ce9445e-18c4-46ae-a679-403ebb072bb3%3Aimage.png?table=block&amp;id=36960b50-99a4-8060-b345-ff6302c2849e&amp;t=36960b50-99a4-8060-b345-ff6302c2849e" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-36960b5099a4807ba5d7c860c99c36cf"><em>图 1：Dubbo 被拿掉后，只保留 fastjson2 features，问题仍然复现。</em></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a4808ea3cdf641957421db" data-id="36960b5099a4808ea3cdf641957421db"><span><div id="36960b5099a4808ea3cdf641957421db" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4808ea3cdf641957421db" title="2. 先看运行结果，再谈源码"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>2. 先看运行结果，再谈源码</b></span></span></h3><div class="notion-text notion-block-36960b5099a4809e950fd274e8936e02">最关键的 baseline 输出是这样：</div><div class="notion-text notion-block-36960b5099a48044b09ef496c2ee8f6e">Dubbo 已经不在场了，但结果和线上一模一样：还是“1 个完整，3 个 null”。</div><div class="notion-text notion-block-36960b5099a480caab7fcee0df83afb8">再把几组对照一起放出来，问题就更清楚：</div><table class="notion-simple-table notion-block-36960b5099a48053a18ac6544af21134"><tbody><tr class="notion-simple-table-row notion-simple-table-header-row notion-block-36960b5099a4804a87a8f00bcdd94ef7"><td class="" style="width:120px"><div class="notion-simple-table-cell">场景</div></td><td class="" style="width:142.0390625px"><div class="notion-simple-table-cell">外层容器</div></td><td class="" style="width:143.0390625px"><div class="notion-simple-table-cell"><code class="notion-inline-code">statusCodeSet</code></div></td><td class="" style="width:281.53125px"><div class="notion-simple-table-cell">结果</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480fd8340f561e3235bd1"><td class="" style="width:120px"><div class="notion-simple-table-cell">A</div></td><td class="" style="width:142.0390625px"><div class="notion-simple-table-cell"><code class="notion-inline-code">HashSet</code></div></td><td class="" style="width:143.0390625px"><div class="notion-simple-table-cell">共享</div></td><td class="" style="width:281.53125px"><div class="notion-simple-table-cell">1 完整 + 3 null</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a48095a1a0e2d630364dc6"><td class="" style="width:120px"><div class="notion-simple-table-cell">B</div></td><td class="" style="width:142.0390625px"><div class="notion-simple-table-cell"><code class="notion-inline-code">HashSet</code></div></td><td class="" style="width:143.0390625px"><div class="notion-simple-table-cell">独立</div></td><td class="" style="width:281.53125px"><div class="notion-simple-table-cell">4 完整</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480308933d55f184fcb3c"><td class="" style="width:120px"><div class="notion-simple-table-cell">C</div></td><td class="" style="width:142.0390625px"><div class="notion-simple-table-cell"><code class="notion-inline-code">LinkedHashSet</code></div></td><td class="" style="width:143.0390625px"><div class="notion-simple-table-cell">共享</div></td><td class="" style="width:281.53125px"><div class="notion-simple-table-cell">1 完整 + 3 null</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480989f0ccc83e8334886"><td class="" style="width:120px"><div class="notion-simple-table-cell">D</div></td><td class="" style="width:142.0390625px"><div class="notion-simple-table-cell"><code class="notion-inline-code">LinkedHashSet</code></div></td><td class="" style="width:143.0390625px"><div class="notion-simple-table-cell">独立</div></td><td class="" style="width:281.53125px"><div class="notion-simple-table-cell">4 完整</div></td></tr></tbody></table><div class="notion-text notion-block-36960b5099a480caaca3f91ce1fd7e3e">这一步已经足够把第一层结论坐实：</div><blockquote class="notion-quote notion-block-36960b5099a480debdaed143164abc25"><div>只要保留 Dubbo 那组 fastjson2 features，这个问题不需要 Dubbo 也能独立复现。</div></blockquote><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a480d08f67c5e0c37df08a" data-id="36960b5099a480d08f67c5e0c37df08a"><span><div id="36960b5099a480d08f67c5e0c37df08a" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480d08f67c5e0c37df08a" title="2.1 先排除一个很容易误判的点"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>2.1 先排除一个很容易误判的点</b></span></span></h4><div class="notion-text notion-block-36960b5099a4801b9d82cef2d52a08fe">如果你直接看 <code class="notion-inline-code">JSONB.toJSONString(bytes)</code>，会看到一种很别扭的输出：</div><div class="notion-text notion-block-36960b5099a48036947ae2f42dbdf858">这个输出很容易把人带偏，以为“写端把同一个字段写了两次”。</div><div class="notion-text notion-block-36960b5099a4801f92b9dd12b7c0325f">但这其实不是根因，而是 <code class="notion-inline-code">JSONBDump</code> 的打印问题。</div><div class="notion-text notion-block-36960b5099a480cbbdbdea66438f8b32"><code class="notion-inline-code">JSONB.toJSONString(bytes)</code> 底层会把 JSONB 字节流 dump 成一段方便人看的 JSON。它在 <code class="notion-inline-code">dumpObject</code> 里遇到 <code class="notion-inline-code">BC_REFERENCE</code> 时，先把引用打印成了 <code class="notion-inline-code">{&quot;$ref&quot;:&quot;...&quot;}</code>；但这个分支结束后，又继续落到外层通用逻辑里，补了一次“写冒号 + 再读下一个值”。</div><div class="notion-text notion-block-36960b5099a480da8dc7f7cf3cfe93e3">所以你看到的像是：</div><div class="notion-text notion-block-36960b5099a480e48419ea009972ec5b">真实字节流不是这样。</div><div class="notion-text notion-block-36960b5099a48038bfa2cef667b9c5e7">真实的 JSONB 字节流里，field name 没有被写两遍。这个误判先摘掉，后面的源码才不会看歪。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a480bca8f0c770bd76a9a2" data-id="36960b5099a480bca8f0c770bd76a9a2"><span><div id="36960b5099a480bca8f0c770bd76a9a2" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480bca8f0c770bd76a9a2" title="3. 先看写端：$ref 是怎么写出来的"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>3. 先看写端：</b><code class="notion-inline-code"><b>$ref</b></code><b> 是怎么写出来的</b></span></span></h3><div class="notion-text notion-block-36960b5099a480debc5adbfd6be64e27">这次问题的第一刀，要先落在 writer。</div><div class="notion-text notion-block-36960b5099a48014b1b1ed0934b64e7f">因为只要你看到“共享引用 + <code class="notion-inline-code">ReferenceDetection</code>”，就该问一句：writer 到底把第二次、第三次出现的对象写成了什么。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a480c4bac3fcdc6770e9db" data-id="36960b5099a480c4bac3fcdc6770e9db"><span><div id="36960b5099a480c4bac3fcdc6770e9db" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480c4bac3fcdc6770e9db" title="3.1 第一次出现时，writer 会先登记路径"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>3.1 第一次出现时，writer 会先登记路径</b></span></span></h4><div class="notion-text notion-block-36960b5099a480dfaf93c3ced5c44adf">fastjson2 开了 <code class="notion-inline-code">ReferenceDetection</code> 以后，第一次遇到对象，不是简单写完就算了。它还会把这个对象和当前路径登记起来。</div><div class="notion-text notion-block-36960b5099a48070a848c4c07112023b">在这个场景里，第 1 个 <code class="notion-inline-code">StatusQuery.statusCodeSet</code> 首次出现时，大概会走到类似这样的逻辑：</div><div class="notion-text notion-block-36960b5099a48030a06de6a0ff792b5b">这一步的意思是：把“这个对象现在在什么路径下”记住。后面再碰到同一个对象实例，就可以不重复写值，而是直接写引用路径。</div><div class="notion-text notion-block-36960b5099a480e0a09ecaf75ed8d4c2">问题也从这里开始露头。</div><div class="notion-text notion-block-36960b5099a480e68ae8f264a782bdd0"><code class="notion-inline-code">setPath(fieldWriter, value)</code> 记录的是字段路径。放在普通 Bean 里，它很好理解；放在 <code class="notion-inline-code">List</code> 的第 0 个元素里，也还能拼出 <code class="notion-inline-code">$[0].statusCodeSet</code> 这种可求值路径。</div><div class="notion-text notion-block-36960b5099a480188480eb1d43d3e3ac">但外层如果是 <code class="notion-inline-code">HashSet</code>，writer 此时没有一个稳定的“第几个元素”可以用。于是第一次登记出来的路径就变成了 <code class="notion-inline-code">$.statusCodeSet</code>。</div><div class="notion-text notion-block-36960b5099a4801db7fde9d10f22ce14">这条路径看起来短，实际上危险。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a480a5846fed09cdc3def8" data-id="36960b5099a480a5846fed09cdc3def8"><span><div id="36960b5099a480a5846fed09cdc3def8" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480a5846fed09cdc3def8" title="3.2 第二次开始，不再内联写值，而是写 $ref"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>3.2 第二次开始，不再内联写值，而是写 </b><code class="notion-inline-code"><b>$ref</b></code></span></span></h4><div class="notion-text notion-block-36960b5099a480029a10d6a5163b62b2">后面第 2、3、4 个 <code class="notion-inline-code">StatusQuery</code> 再遇到同一个 <code class="notion-inline-code">HashSet&lt;String&gt;</code> 时，<code class="notion-inline-code">FieldWriterObject.writeInternal(...)</code> 就不会再完整写内容了，而是走 <code class="notion-inline-code">$ref</code> 分支。</div><div class="notion-text notion-block-36960b5099a480539d1dc48b0ad74223">源码里的判断大致可以理解成这样：</div><div class="notion-text notion-block-36960b5099a480639990d17fc8bf295c">也就是说，只要 writer 判断“这个对象之前见过”，就直接把之前登记的路径写出去。</div><div class="notion-text notion-block-36960b5099a4805f8dd1c38a14f5659b">在这个场景下，最终写出来的几种路径是：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a4807aaf50f4f80f4e6c88"><li>外层是 <code class="notion-inline-code">HashSet</code> 时：<code class="notion-inline-code">$.statusCodeSet</code></li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a4803d8124e180acc1743d"><li>外层是 <code class="notion-inline-code">LinkedHashSet</code> 时：<code class="notion-inline-code">$[0].statusCodeSet</code></li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a480c39f77d2f9154ed2e8"><li>后续相对引用：<code class="notion-inline-code">#-1</code></li></ul><div class="notion-text notion-block-36960b5099a480a4b56ed0da9bb1cc7a">这几个路径里，最危险的是第一个。</div><div class="notion-text notion-block-36960b5099a480cc9087fcaf8d9ae9fd"><code class="notion-inline-code">$.statusCodeSet</code> 看上去像在说：“从根节点拿到 <code class="notion-inline-code">statusCodeSet</code> 这个字段。”</div><div class="notion-text notion-block-36960b5099a4803da2f3d5f8ce48b980">但根节点是谁？不是 <code class="notion-inline-code">StatusQuery</code>，而是外层那个 <code class="notion-inline-code">HashSet&lt;StatusQuery&gt;</code>。</div><div class="notion-text notion-block-36960b5099a480fdb421da55810f5702"><code class="notion-inline-code">HashSet</code> 根本没有 <code class="notion-inline-code">statusCodeSet</code> 这个属性。</div><div class="notion-text notion-block-36960b5099a480b7934bccef3d971b0e">换句话说，writer 这时候已经埋下雷了：它写出了一个以 <code class="notion-inline-code">Set</code> 为根、但根本不适合在 <code class="notion-inline-code">Set</code> 上求值的路径。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-36960b5099a480a488e6e3e33e96413c"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3Ab775fb9d-8b1d-4706-8078-99898e331131%3Aimage.png?table=block&amp;id=36960b50-99a4-80a4-88e6-e3e33e96413c&amp;t=36960b50-99a4-80a4-88e6-e3e33e96413c" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-36960b5099a480d78e40e05b419642bb"><em>图 2：第一次出现时 writer 登记路径，后续共享引用直接写 </em><em><code class="notion-inline-code">$ref</code></em><em>。问题在于这个路径对 </em><em><code class="notion-inline-code">HashSet</code></em><em> root 不成立。</em></div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a480d68171cf066c4f89f5" data-id="36960b5099a480d68171cf066c4f89f5"><span><div id="36960b5099a480d68171cf066c4f89f5" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480d68171cf066c4f89f5" title="3.3 为什么 LinkedHashSet 也没活下来"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>3.3 为什么 </b><code class="notion-inline-code"><b>LinkedHashSet</b></code><b> 也没活下来</b></span></span></h4><div class="notion-text notion-block-36960b5099a480acab11f4fbe8971f0b">有人看到这里会说：那 <code class="notion-inline-code">LinkedHashSet</code> 写成 <code class="notion-inline-code">$[0].statusCodeSet</code>，不是合理多了吗？</div><div class="notion-text notion-block-36960b5099a4800e8b01d3a6d61f4ed7">表面上是。</div><div class="notion-text notion-block-36960b5099a480499712ce510b0437fc">但这只是“看起来更合理”，不等于 reader 一定能按原意解回来。因为这件事还要看读端把 root 建成了什么。这个坑要放到下一节一起看。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a480b091add44f1e84c87f" data-id="36960b5099a480b091add44f1e84c87f"><span><div id="36960b5099a480b091add44f1e84c87f" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480b091add44f1e84c87f" title="4. 再看读端：为什么这些路径解不回来"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>4. 再看读端：为什么这些路径解不回来</b></span></span></h3><div class="notion-text notion-block-36960b5099a480848224df42a43fb9bf">如果 writer 的问题是“写出了坏路径”，那 reader 的问题就是“它到底是在哪一步解不回来”。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a480bf9050f71db8e69031" data-id="36960b5099a480bf9050f71db8e69031"><span><div id="36960b5099a480bf9050f71db8e69031" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480bf9050f71db8e69031" title="4.1 读端最后会把 $ref 任务攒起来，再统一 resolve"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>4.1 读端最后会把 </b><code class="notion-inline-code"><b>$ref</b></code><b> 任务攒起来，再统一 resolve</b></span></span></h4><div class="notion-text notion-block-36960b5099a480f0a00fe4e4eb0a3c0b">fastjson2 反序列化时，不会在第一次读到 <code class="notion-inline-code">$ref</code> 的那一刻立刻把所有字段补齐。它会先记一笔待回填任务，等对象树大致建好以后，再在 <code class="notion-inline-code">JSONReader.handleResolveTasks()</code> 里统一处理。</div><div class="notion-text notion-block-36960b5099a480f19209d69266e59fb3">核心逻辑可以粗暴理解成：</div><div class="notion-text notion-block-36960b5099a480ff8b68d031c969fb94">问题就出在这里的 <code class="notion-inline-code">root</code>。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a480068c4dca58d463e073" data-id="36960b5099a480068c4dca58d463e073"><span><div id="36960b5099a480068c4dca58d463e073" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480068c4dca58d463e073" title="4.2 $.statusCodeSet 在 HashSet root 上根本求不出值"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>4.2 </b><code class="notion-inline-code"><b>$.statusCodeSet</b></code><b> 在 </b><code class="notion-inline-code"><b>HashSet</b></code><b> root 上根本求不出值</b></span></span></h4><div class="notion-text notion-block-36960b5099a480c19d87c57f6a2fc9a9">当 root 是外层的 <code class="notion-inline-code">HashSet&lt;StatusQuery&gt;</code> 时：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a48021bf0ef7599b1beb1c"><li><code class="notion-inline-code">$.statusCodeSet</code> 的含义是“去根节点上找 <code class="notion-inline-code">statusCodeSet</code> 字段”</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a48013b345c56f6e082d32"><li>但 <code class="notion-inline-code">HashSet</code> 根本没有这个字段</li></ul><div class="notion-text notion-block-36960b5099a48021ab5ffa1229e93406">于是这条路径天然求值失败。reader 不是把一个正确路径解错了，而是根本拿到了一条不可能在当前 root 上成立的路径。</div><div class="notion-text notion-block-36960b5099a480fa958ac6df692a3732">这也是为什么我说这次问题的本质更偏向 writer：<b>reader 处理的是一条它自己也无法成立的路径。</b></div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-36960b5099a480c68c7bdafb06f84ca1"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A8b8a574a-97ab-49d5-afa6-535d43f8b702%3Aimage.png?table=block&amp;id=36960b50-99a4-80c6-8c7b-dafb06f84ca1&amp;t=36960b50-99a4-80c6-8c7b-dafb06f84ca1" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-36960b5099a48016ad8fdd0bac5ce47f"><em>图 3：reader 最后会对 root 执行 </em><em><code class="notion-inline-code">path.eval(root)</code></em><em>。当 root 是 </em><em><code class="notion-inline-code">HashSet</code></em><em> 时，</em><em><code class="notion-inline-code">$.statusCodeSet</code></em><em> 没有求值对象。</em></div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a4807aab8ac7474b4f18c8" data-id="36960b5099a4807aab8ac7474b4f18c8"><span><div id="36960b5099a4807aab8ac7474b4f18c8" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4807aab8ac7474b4f18c8" title="4.3 LinkedHashSet 之所以也失败，是因为读回来时 root 还是 HashSet"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>4.3 </b><code class="notion-inline-code"><b>LinkedHashSet</b></code><b> 之所以也失败，是因为读回来时 root 还是 </b><code class="notion-inline-code"><b>HashSet</b></code></span></span></h4><div class="notion-text notion-block-36960b5099a480598951e66a49e1be30">外层换成 <code class="notion-inline-code">LinkedHashSet</code> 后，writer 写的是 <code class="notion-inline-code">$[0].statusCodeSet</code>。这个路径只有在 root 是有稳定索引语义的容器时，才真正有意义。</div><div class="notion-text notion-block-36960b5099a48065becafa5b08496e4a">但实际实验里，reader 侧拿到的外层容器是 <code class="notion-inline-code">HashSet</code>，不是 <code class="notion-inline-code">LinkedHashSet</code>。</div><div class="notion-text notion-block-36960b5099a4801f88bffa6d1285353a">也就是说：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a480299d2be8d4e0bf98ec"><li>writer 以为自己在给一个“有顺序”的容器写路径</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a48002b278dd0e721711ee"><li>reader 最后却在一个没有稳定索引语义的 <code class="notion-inline-code">HashSet</code> 上做 <code class="notion-inline-code">path.eval(root)</code></li></ul><div class="notion-text notion-block-36960b5099a48067931bd3f2ae1c32fa">那 <code class="notion-inline-code">$[0]</code> 还能不能稳定落到 writer 以为的第一个元素上？答案是不行。</div><div class="notion-text notion-block-36960b5099a4800aa463f63f6a6a589b">这也是为什么 <code class="notion-inline-code">LinkedHashSet</code> 这组实验看起来更像是“差一点就好了”，但最后结果还是“1 完整 + 3 null”。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a480c2ad83e391ca174d8c" data-id="36960b5099a480c2ad83e391ca174d8c"><span><div id="36960b5099a480c2ad83e391ca174d8c" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480c2ad83e391ca174d8c" title="4.4 为什么 List 没事，Set 不行"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>4.4 为什么 </b><code class="notion-inline-code"><b>List</b></code><b> 没事，</b><code class="notion-inline-code"><b>Set</b></code><b> 不行</b></span></span></h4><div class="notion-text notion-block-36960b5099a48079bc8ee0b14bc7d5d3">这也是我最后没有全局禁用 <code class="notion-inline-code">ReferenceDetection</code> 的原因。</div><div class="notion-text notion-block-36960b5099a480759306c85a937faad2">如果外层是 <code class="notion-inline-code">List&lt;StatusQuery&gt;</code>，writer 写出来的路径通常是 <code class="notion-inline-code">$[0].statusCodeSet</code>。读端拿到的 root 也是 <code class="notion-inline-code">List</code>，<code class="notion-inline-code">$[0]</code> 能先定位到第 1 个元素，再继续取 <code class="notion-inline-code">.statusCodeSet</code>。</div><div class="notion-text notion-block-36960b5099a480518a29f223e693444d">这条路径能成立，所以 <code class="notion-inline-code">List</code> 场景本来就是健康的。</div><div class="notion-text notion-block-36960b5099a480818c20f54705a28b74"><code class="notion-inline-code">Set</code> 不一样。</div><div class="notion-text notion-block-36960b5099a480b9bfc1dd2cd1d7833e"><code class="notion-inline-code">HashSet</code> 没有稳定下标。<code class="notion-inline-code">$.statusCodeSet</code> 在 <code class="notion-inline-code">HashSet</code> root 上也没有意义。<code class="notion-inline-code">LinkedHashSet</code> 虽然有迭代顺序，但反序列化后 root 未必还能按 writer 想象的方式提供 <code class="notion-inline-code">$[0]</code> 语义。</div><div class="notion-text notion-block-36960b5099a480229e20efd21f2a07a5">所以这次问题的边界不是“JSONB 共享引用都坏了”，而是更窄：</div><blockquote class="notion-quote notion-block-36960b5099a4804086d4da78a3368b59"><div>外层是非 <code class="notion-inline-code">List</code> 集合，元素内部又共享了同一个对象，writer 还打开了 <code class="notion-inline-code">ReferenceDetection</code>。</div></blockquote><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-36960b5099a480faa2eef484a4db4592"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A32ba038b-c0ff-4ab7-a8e1-1fa4d1eebd00%3Aimage.png?table=block&amp;id=36960b50-99a4-80fa-a2ee-f484a4db4592&amp;t=36960b50-99a4-80fa-a2ee-f484a4db4592" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-36960b5099a480c4a9f5ef3353bf543c"><em>图 4：</em><em><code class="notion-inline-code">List</code></em><em> 有稳定下标，</em><em><code class="notion-inline-code">$[0].statusCodeSet</code></em><em> 能成立；</em><em><code class="notion-inline-code">Set</code></em><em> 没有稳定下标，</em><em><code class="notion-inline-code">$.statusCodeSet</code></em><em> 在 root 上也不成立。</em></div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a48071a0afda90ea45e924" data-id="36960b5099a48071a0afda90ea45e924"><span><div id="36960b5099a48071a0afda90ea45e924" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a48071a0afda90ea45e924" title="4.5 到这里，又能顺手排掉两个误判"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>4.5 到这里，又能顺手排掉两个误判</b></span></span></h4><div class="notion-text notion-block-36960b5099a48065bdc1fa028498be56">第一，完整泛型 Type 也救不了。</div><div class="notion-text notion-block-36960b5099a4802183a3f23a3c74d61c">这次纯 JSONB 实验里，我已经传了完整的 <code class="notion-inline-code">TypeReference&lt;Set&lt;StatusQuery&gt;&gt;()</code>。问题照样复现，所以“泛型擦除”不是必要条件。</div><div class="notion-text notion-block-36960b5099a480e59f3eedb0ea2e517c">第二，<code class="notion-inline-code">UseNativeObject</code> 也不是必要条件。</div><div class="notion-text notion-block-36960b5099a480bf9e8cfa0310d05d0c">我单独做过一组实验，把 reader 的 <code class="notion-inline-code">UseNativeObject</code> 去掉，结果还是“1 完整 + 3 null”。这说明它确实会放大 <code class="notion-inline-code">LinkedHashSet</code> 那条路径的问题，但不是这次 bug 的根开关。</div><div class="notion-text notion-block-36960b5099a480649563d4bc53215c4a">到这里，触发这个问题的最小条件其实已经很清楚了：</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a4801ebfbbf22b5ca0d399" data-id="36960b5099a4801ebfbbf22b5ca0d399"><span><div id="36960b5099a4801ebfbbf22b5ca0d399" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4801ebfbbf22b5ca0d399" title="5. 为什么最后修写端，不修读端"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>5. 为什么最后修写端，不修读端</b></span></span></h3><div class="notion-text notion-block-36960b5099a4805082b7f0b79ffad3f2">把根因钉到这里以后，摆在我面前其实有 3 条路。</div><table class="notion-simple-table notion-block-36960b5099a480b1b541f16d0606766f"><tbody><tr class="notion-simple-table-row notion-simple-table-header-row notion-block-36960b5099a480b29756c837f25af791"><td class="" style="width:120px"><div class="notion-simple-table-cell">方案</div></td><td class="" style="width:308px"><div class="notion-simple-table-cell">思路</div></td><td class="" style="width:267px"><div class="notion-simple-table-cell">主要问题</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480aab960e5590930a3c2"><td class="" style="width:120px"><div class="notion-simple-table-cell">改读端</div></td><td class="" style="width:308px"><div class="notion-simple-table-cell">让 reader 对 <code class="notion-inline-code">Set</code> 这类容器做更复杂的 resolve</div></td><td class="" style="width:267px"><div class="notion-simple-table-cell">改动面大，要碰 resolve 生命周期和 hash 容器行为</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480bd8063e5735c4b0405"><td class="" style="width:120px"><div class="notion-simple-table-cell">改写端</div></td><td class="" style="width:308px"><div class="notion-simple-table-cell">非 <code class="notion-inline-code">List</code> 集合场景下，不再为元素内部共享引用生成 <code class="notion-inline-code">$ref</code> 路径</div></td><td class="" style="width:267px"><div class="notion-simple-table-cell">共享对象会内联，字节流变大</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480e5a28cdbc33142ee5a"><td class="" style="width:120px"><div class="notion-simple-table-cell">全局禁用</div></td><td class="" style="width:308px"><div class="notion-simple-table-cell">一刀切关掉 ref detection</div></td><td class="" style="width:267px"><div class="notion-simple-table-cell">太粗暴，会伤到本来没问题的 <code class="notion-inline-code">List</code> 路径</div></td></tr></tbody></table><div class="notion-text notion-block-36960b5099a48050aa8ceb437d827e29">我最后选的是第二条：<b>修写端，不修读端。</b></div><div class="notion-text notion-block-36960b5099a480619d5bc2281501b4ea">原因有两个。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a480f08f13c9aab09dd935" data-id="36960b5099a480f08f13c9aab09dd935"><span><div id="36960b5099a480f08f13c9aab09dd935" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480f08f13c9aab09dd935" title="5.1 直接问题在写端"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>5.1 直接问题在写端</b></span></span></h4><div class="notion-text notion-block-36960b5099a48092a51ddc8d876bbfd8">这次最直接的问题，不是 reader 无缘无故把一个好好的字段吞掉了。</div><div class="notion-text notion-block-36960b5099a48012acc2cc28441a9189">而是 writer 先写出了一条在当前 root 上根本不成立的路径。</div><div class="notion-text notion-block-36960b5099a4800fbe4def62622a720f">既然坏路径从这里开始，那最小修法就应该优先瞄准这里。先别让 writer 再写出这种路径。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a48060a0c1f0c694dc49c4" data-id="36960b5099a48060a0c1f0c694dc49c4"><span><div id="36960b5099a48060a0c1f0c694dc49c4" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a48060a0c1f0c694dc49c4" title="5.2 读端真要修，会牵出更大的坑"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>5.2 读端真要修，会牵出更大的坑</b></span></span></h4><div class="notion-text notion-block-36960b5099a480d79c3cff5170901a31">读端不是不能修，而是会修得很重。</div><div class="notion-text notion-block-36960b5099a480109a85f0ad02631993">因为一旦你想在 reader 侧“帮忙把 <code class="notion-inline-code">Set</code> 上的引用回填好”，马上就会碰到另一个问题：<code class="notion-inline-code">HashSet</code> 这类容器依赖元素的 <code class="notion-inline-code">hashCode()</code>。</div><div class="notion-text notion-block-36960b5099a480558708e7d0b144ba0e">如果对象在“字段还没回填完成”的状态下就已经被 <code class="notion-inline-code">add</code> 进 <code class="notion-inline-code">HashSet</code>，后面再去补字段，理论上就可能破坏 hash 容器的不变量。</div><div class="notion-text notion-block-36960b5099a480da9686c091aeaffc12">这不是假想出来的复杂度。</div><div class="notion-text notion-block-36960b5099a480d49471d40fb7714849">fastjson2 读 <code class="notion-inline-code">$ref</code> 时，会先把回填任务记下来。</div><div class="notion-text notion-block-36960b5099a4801c8611dda3364bbd58">等元素已经进了集合，再统一跑 <code class="notion-inline-code">handleResolveTasks()</code>。</div><div class="notion-text notion-block-36960b5099a480bbbb4bc9fef2ea4dcb">如果 <code class="notion-inline-code">StatusQuery.hashCode()</code> 又依赖 <code class="notion-inline-code">statusCodeSet</code>，那就会出现一个尴尬局面：</div><ol start="1" class="notion-list notion-list-numbered notion-block-36960b5099a480f3a831c49098fa7844" style="list-style-type:decimal"><li>先按 <code class="notion-inline-code">statusCodeSet = null</code> 的状态算 hash，把对象放进 <code class="notion-inline-code">HashSet</code>。</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-36960b5099a480f4aecaf6a8ed641ec3" style="list-style-type:decimal"><li>后面 resolve 成功，又把 <code class="notion-inline-code">statusCodeSet</code> 回填成一个非空集合。</li></ol><ol start="3" class="notion-list notion-list-numbered notion-block-36960b5099a480b8aba1f77b0f7727da" style="list-style-type:decimal"><li>对象的 hash 变了，但它在 <code class="notion-inline-code">HashSet</code> 里的桶位没跟着重排。</li></ol><div class="notion-text notion-block-36960b5099a4806ba46ae4d0089a9428">这已经不是“补一条 resolve 规则”那么简单了。它会把你拖进更大的读端改造里：先缓冲元素、等引用全部回填完再 <code class="notion-inline-code">addAll</code>、必要时重建容器、还要考虑对象稳定性。</div><div class="notion-text notion-block-36960b5099a480e2854cdb2547db66c6">所以我的判断很明确：<b>这次应该先用写端的小修，堵住一个写端先制造坏路径的问题，而不是为了补读端，把动刀范围扩大一圈。</b></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a480f78a97c1d17caf2e17" data-id="36960b5099a480f78a97c1d17caf2e17"><span><div id="36960b5099a480f78a97c1d17caf2e17" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480f78a97c1d17caf2e17" title="6. PR 到底改了什么"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>6. PR 到底改了什么</b></span></span></h3><div class="notion-text notion-block-36960b5099a480cabb66e58526ba5242">最后补丁落在：</div><div class="notion-text notion-block-36960b5099a480c88238fe4b505979bf">核心思路很简单：</div><ol start="1" class="notion-list notion-list-numbered notion-block-36960b5099a48004bdd4ea84ab20da01" style="list-style-type:decimal"><li>进入集合写出逻辑时，先判断外层是不是 <code class="notion-inline-code">List</code></li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-36960b5099a4803ba088ec12b611904a" style="list-style-type:decimal"><li>如果不是 <code class="notion-inline-code">List</code>，并且当前 context 开着 <code class="notion-inline-code">ReferenceDetection</code></li></ol><ol start="3" class="notion-list notion-list-numbered notion-block-36960b5099a48077904ecc8e5d655784" style="list-style-type:decimal"><li>那就在写元素期间临时把这个 feature 关掉</li></ol><ol start="4" class="notion-list notion-list-numbered notion-block-36960b5099a4803b8a84f26fcfa77b02" style="list-style-type:decimal"><li>写完再用 <code class="notion-inline-code">try/finally</code> 恢复原来的 context</li></ol><div class="notion-text notion-block-36960b5099a480f587bbe4e5363c35a4">关键代码大致是这样：</div><div class="notion-text notion-block-36960b5099a48074b56bc3e81591d0b8">然后把原来的元素遍历包进 <code class="notion-inline-code">try/finally</code> 里，结束后再恢复。</div><div class="notion-text notion-block-36960b5099a480cdb4dce60db379ff0d">这个补丁的意思其实非常朴素：</div><blockquote class="notion-quote notion-block-36960b5099a480b6a560eb290269ea6f"><div>对外层是非 <code class="notion-inline-code">List</code> 的集合，不要再为元素内部共享引用生成 <code class="notion-inline-code">$ref</code> 路径；直接内联写值。</div></blockquote><div class="notion-text notion-block-36960b5099a480d7b545f4a2b06635ec">我没有只对 <code class="notion-inline-code">HashSet</code> 写特判，而是把边界放在“非 <code class="notion-inline-code">List</code> 集合”。因为这次问题的本质，不是某个类名，而是“这类容器没有稳定的数组索引语义”。</div><div class="notion-text notion-block-36960b5099a48050a1bfc3171b1a228b">这里还有一个实现细节。</div><div class="notion-text notion-block-36960b5099a480dc9a64f9d2e4e3b9df">我关的不是调用方传进来的 features 数组，也不是永久改掉全局配置，而是临时改当前 <code class="notion-inline-code">JSONWriter.Context</code> 里的 feature bit。写元素前关掉，元素写完后在 <code class="notion-inline-code">finally</code> 里恢复。</div><div class="notion-text notion-block-36960b5099a480668718e9ccd4b1a3eb">这样做有两个好处：</div><ol start="1" class="notion-list notion-list-numbered notion-block-36960b5099a4807db2ace35f6d65f0c9" style="list-style-type:decimal"><li>影响范围只包住这一次非 <code class="notion-inline-code">List</code> 集合的元素写出。</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-36960b5099a480da9b32e12224fa8561" style="list-style-type:decimal"><li>如果中间抛异常，<code class="notion-inline-code">finally</code> 也会把原来的 context 还回去。</li></ol><div class="notion-text notion-block-36960b5099a48010b961d0c71235ddd2">我本地验证时用的是“源文件覆盖”的方式：从 fastjson2 sources jar 里取出 <code class="notion-inline-code">ObjectWriterImplCollection.java</code>，放到测试工程的 <code class="notion-inline-code">src/main/java</code> 同路径下。Maven 编译后，这个 class 会比依赖 jar 里的原版更早出现在 classpath 里，所以不用重打 fastjson2 jar，也能快速验证补丁。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-36960b5099a480e48baadf8d313c7b14"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A3c9f4500-b4fc-4541-aacd-204c579eb355%3Aimage.png?table=block&amp;id=36960b50-99a4-80e4-8baa-df8d313c7b14&amp;t=36960b50-99a4-80e4-8baa-df8d313c7b14" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-36960b5099a48045b190fa7abc7f981f"><em>图 5：外层是非 </em><em><code class="notion-inline-code">List</code></em><em> 集合时，写元素期间临时关闭 </em><em><code class="notion-inline-code">ReferenceDetection</code></em><em>，避免生成不可 resolve 的 </em><em><code class="notion-inline-code">$ref</code></em><em> 路径。</em></div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a4800ab818e04aec5fdbfa" data-id="36960b5099a4800ab818e04aec5fdbfa"><span><div id="36960b5099a4800ab818e04aec5fdbfa" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4800ab818e04aec5fdbfa" title="6.1 为什么这个改法能自圆其说"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>6.1 为什么这个改法能自圆其说</b></span></span></h4><div class="notion-text notion-block-36960b5099a4807e997dc9c5dad00bac">补丁不是只要让一个 case 通过就行。它至少要同时满足两件事：</div><ol start="1" class="notion-list notion-list-numbered notion-block-36960b5099a48045989ff94370c4dabd" style="list-style-type:decimal"><li>原来会炸的 case 现在好起来。</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-36960b5099a480378ca6dd2732746e5a" style="list-style-type:decimal"><li>原来没问题的 case 别被顺手改坏。</li></ol><div class="notion-text notion-block-36960b5099a48068ae5dee073a4db877">所以我本地验证时盯的重点是这 4 组：</div><table class="notion-simple-table notion-block-36960b5099a48086a3b0f1b92c554d0b"><tbody><tr class="notion-simple-table-row notion-simple-table-header-row notion-block-36960b5099a480949653e8834c276ec5"><td class="" style="width:120px"><div class="notion-simple-table-cell">用例</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">修复前</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">修复后</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a4808f96a9d41c662674de"><td class="" style="width:120px"><div class="notion-simple-table-cell">共享 <code class="notion-inline-code">HashSet</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">null count = 3/4</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">null count = 0/4</code></div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480249779c228f8d8e9f4"><td class="" style="width:120px"><div class="notion-simple-table-cell">共享 <code class="notion-inline-code">LinkedHashSet</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">null count = 3/4</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">null count = 0/4</code></div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480588593c9fec59b2791"><td class="" style="width:120px"><div class="notion-simple-table-cell">关闭 <code class="notion-inline-code">ReferenceDetection</code> 的健康基线</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">null count = 0/4</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">null count = 0/4</code></div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480c2a47debbb36d421d0"><td class="" style="width:120px"><div class="notion-simple-table-cell">外层 <code class="notion-inline-code">List</code> 的健康基线</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">null count = 0/4</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">null count = 0/4</code></div></td></tr></tbody></table><div class="notion-text notion-block-36960b5099a48039a8e7f10db5031544">最后一行尤其关键。</div><div class="notion-text notion-block-36960b5099a480e890a6ed7e6cc6916d">因为它说明这次改动没有把 <code class="notion-inline-code">List</code> 这种本来能正确处理 <code class="notion-inline-code">$ref</code> 的路径一起打坏。补丁的影响面，确实被控制在“非 <code class="notion-inline-code">List</code> 集合元素内部的共享引用”上。</div><div class="notion-text notion-block-36960b5099a480899d07d1dbbc695235">代价也有。</div><div class="notion-text notion-block-36960b5099a4804eb97efcfcaf4d9515">非 <code class="notion-inline-code">List</code> 集合里被共享的对象，会从 <code class="notion-inline-code">$ref</code> 变成内联写出。我的最小用例里，wire bytes 从 <code class="notion-inline-code">217</code> 变成了 <code class="notion-inline-code">373</code>。这不是零成本，但我觉得这个取舍能接受：字节流变大一些，换反序列化结果正确。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-36960b5099a480658f8fdad05d77e5ca"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A49e9f44f-872c-48ab-9151-543bf628527b%3Aimage.png?table=block&amp;id=36960b50-99a4-8065-8f8f-dad05d77e5ca&amp;t=36960b50-99a4-8065-8f8f-dad05d77e5ca" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-36960b5099a48075a2eccfb613887181"><em>图 6：共享 </em><em><code class="notion-inline-code">HashSet</code></em><em> 和 </em><em><code class="notion-inline-code">LinkedHashSet</code></em><em> 从 </em><em><code class="notion-inline-code">null count = 3/4</code></em><em> 变成 </em><em><code class="notion-inline-code">0/4</code></em><em>，外层 </em><em><code class="notion-inline-code">List</code></em><em> 基线保持健康。</em></div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a48025b861d5f1acf9d926" data-id="36960b5099a48025b861d5f1acf9d926"><span><div id="36960b5099a48025b861d5f1acf9d926" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a48025b861d5f1acf9d926" title="6.2 PR 里我放了什么"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>6.2 PR 里我放了什么</b></span></span></h4><div class="notion-text notion-block-36960b5099a48024b0bce5b6a672a756">我最后提给 fastjson2 的 PR 是：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a480309e70d11d2adea0c7"><li><a class="notion-link" href="https://github.com/alibaba/fastjson2/pull/7641" target="_blank" rel="noopener noreferrer">fastjson2 PR #7641</a></li></ul><div class="notion-text notion-block-36960b5099a4802bbedbf79c56ae0ff6">PR 里我刻意把内容收成了 3 件事：</div><ol start="1" class="notion-list notion-list-numbered notion-block-36960b5099a480dcb745fcd4b2f75ab1" style="list-style-type:decimal"><li>改动点集中在 <code class="notion-inline-code">ObjectWriterImplCollection.java</code>。</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-36960b5099a48047a90dfed0f784d2c5" style="list-style-type:decimal"><li>增加回归测试 <code class="notion-inline-code">SharedReferenceInSetTest.java</code>，覆盖非 <code class="notion-inline-code">List</code> 集合元素里的共享引用场景。</li></ol><ol start="3" class="notion-list notion-list-numbered notion-block-36960b5099a4807395b0cda1b2153285" style="list-style-type:decimal"><li>PR 说明里把问题边界写清楚：这是 <code class="notion-inline-code">non-List collection elements + shared reference</code> 的问题，不是泛泛的“JSONB 共享引用都不对”。</li></ol><div class="notion-text notion-block-36960b5099a480d3b962c0e96a3d8d2c">我不想把它提成一个“到处都改一点”的大补丁。因为这类问题一旦进开源项目 review，改动范围越大，越难说服 reviewer。</div><div class="notion-text notion-block-36960b5099a48048a730d823ac7bae42">截至现在，这个 PR 还是 open 状态。所以我不会写“某个版本已经修了”。这类结论，得等 merge 和 release 之后再说。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a48066ae73c422e6e761ab" data-id="36960b5099a48066ae73c422e6e761ab"><span><div id="36960b5099a48066ae73c422e6e761ab" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a48066ae73c422e6e761ab" title="7. 收尾"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>7. 收尾</b></span></span></h3><div class="notion-text notion-block-36960b5099a480ee919ff22eb68d85b8">排查走到这里，我想讲清楚的东西已经够了。</div><div class="notion-text notion-block-36960b5099a480729547fe9b10ff3ecf">如果只记一句话，我希望是这一句：</div><blockquote class="notion-quote notion-block-36960b5099a480df91a2f4df305025c1"><div>这次不是 reader 平白无故把字段吃掉了，而是 writer 先写出了一条 reader 自己也无法成立的路径。</div></blockquote><div class="notion-text notion-block-36960b5099a48065853af7a894cd5e72">所以最后才会落到“修写端，不修读端”。</div><div class="notion-text notion-block-36960b5099a48006a421de104f891337">完整 demo 我已经整理到 GitHub：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a4809fa1c0e885133e7327"><li><a class="notion-link" href="https://github.com/yibiner/dubbo-fastjson2-shared-ref-bug-demo#" target="_blank" rel="noopener noreferrer">dubbo-fastjson2-shared-ref-bug-demo</a></li></ul><div class="notion-text notion-block-36960b5099a480e39d58c1c6f6899c2f">对应的 fastjson2 PR 在这里：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a480ecb256fb32e6872e02"><li><a class="notion-link" href="https://github.com/alibaba/fastjson2/pull/7641" target="_blank" rel="noopener noreferrer">fastjson2 PR #7641</a></li></ul><div class="notion-text notion-block-36960b5099a4804c8cecc4f9d32f845e">前半段排查解决的是“锅到底在哪”。这次继续往下走，解决的是“知道锅在哪以后，怎么把补丁收得足够小”。</div><div class="notion-text notion-block-36960b5099a480d39632e4d0bde0b8a8">对我来说，这两部分缺一不可。只会把问题指给别人看，不够。只会闷头改代码，也不够。</div><div class="notion-text notion-block-36960b5099a48082879dc40daaa72281">真正有价值的，是你既能把问题一路收窄到最小路径，也能把补丁收得足够小、足够准，让上游能接得住。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a4809b8f1afe0d163d00f5" data-id="36960b5099a4809b8f1afe0d163d00f5"><span><div id="36960b5099a4809b8f1afe0d163d00f5" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4809b8f1afe0d163d00f5" title="8. 参考资料"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>8. 参考资料</b></span></span></h4><ul class="notion-list notion-list-disc notion-block-36960b5099a48008931add685f4ffc39"><li>dubbo源码：<a class="notion-link" href="https://github.com/apache/dubbo.git" target="_blank" rel="noopener noreferrer">https://github.com/apache/dubbo.git</a></li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a48040809bf0e6a229b405"><li>fastjson2源码：<a class="notion-link" href="https://github.com/alibaba/fastjson2" target="_blank" rel="noopener noreferrer">https://github.com/alibaba/fastjson2</a></li></ul><div class="notion-blank notion-block-36960b5099a48025a2a9cd68e8ce6e8e"> </div><div class="notion-blank notion-block-36960b5099a480d6bccefd18ab126120"> </div></main></div>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[线上 4 个 RPC 参数丢了 3 个字段，到底谁干的]]></title>
            <link>https://yibin.dev/article/36960b50-99a4-804a-9ea2-e0a8c6112c14</link>
            <guid>https://yibin.dev/article/36960b50-99a4-804a-9ea2-e0a8c6112c14</guid>
            <pubDate>Thu, 30 Apr 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div id="notion-article" class="mx-auto overflow-hidden "><main class="notion light-mode notion-page notion-block-36960b5099a4804a9ea2e0a8c6112c14"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><div class="notion-text notion-block-36960b5099a480109d81e681de96b5e5">线上有个 RPC 调用的问题，把我看愣了。</div><div class="notion-text notion-block-36960b5099a4809a8d51c2add14f98b4">consumer 明明发出去了 4 个完整对象，provider 收到后却只剩 1 个对象还是完整的，另外 3 个对象里的同一个字段全变成了 <code class="notion-inline-code">null</code>。更麻烦的是，整条链路没有异常，没有超时，也没有重试失败。就是静悄悄地坏了。</div><div class="notion-text notion-block-36960b5099a48056adc5ef63cfddc5b0">这类问题最难的地方，不是修，而是先判断锅到底在哪。业务代码有嫌疑，Dubbo 有嫌疑，日志打印方式也有嫌疑。只要中间哪一层没排干净，最后的结论就很容易下错。</div><blockquote class="notion-quote notion-block-36960b5099a48075bcc7e8d04923f807"><div>说明：本文配图均为自制示意图，用来解释调用链路和定位过程，不对应真实线上界面截图。</div></blockquote><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a4802fb1e6fdcdf02fbf36" data-id="36960b5099a4802fb1e6fdcdf02fbf36"><span><div id="36960b5099a4802fb1e6fdcdf02fbf36" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4802fb1e6fdcdf02fbf36" title="1. 问题先摆出来"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>1. 问题先摆出来</b></span></span></h3><div class="notion-text notion-block-36960b5099a480c5a021cb070dfbe8b8">接口很普通：</div><div class="notion-text notion-block-36960b5099a4801fa627c641d9239c6c">一次业务调用里，会传 4 个 <code class="notion-inline-code">StatusQuery</code>。consumer 发包前打印出来是这样的：</div><div class="notion-text notion-block-36960b5099a480288e18dad541275695">provider 刚收到参数时，却变成了这样：</div><div class="notion-text notion-block-36960b5099a4806f9c35c72e469111e6">第 1 个对象正常。后面 3 个对象没有 <code class="notion-inline-code">statusCodeSet</code>，等价于 <code class="notion-inline-code">null</code>。</div><div class="notion-text notion-block-36960b5099a48016b0aff1d731b7f698">最麻烦的还不是“丢了 3 个字段”，而是整条链路没报任何错。对调用方来说，请求成功了；对下游业务来说，数据已经坏了。这种静默数据损坏，比直接抛异常更难排。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-36960b5099a480e5847eefaf7d9abfda"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3Aede22663-46af-4f56-b3a4-99afdf56ebe8%3Aimage.png?table=block&amp;id=36960b50-99a4-80e5-847e-efaf7d9abfda&amp;t=36960b50-99a4-80e5-847e-efaf7d9abfda" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-36960b5099a4809c9b82f383b9e9e0ce"><em>图 1：consumer 发 4 个完整对象，provider 收到 1 个完整对象和 3 个空字段对象。</em></div><div class="notion-text notion-block-36960b5099a48096bf11ee85e9f365ac">当时的环境版本是：</div><table class="notion-simple-table notion-block-36960b5099a480369913ddc0d4268305"><tbody><tr class="notion-simple-table-row notion-simple-table-header-row notion-block-36960b5099a480318637d11a3836451f"><td class="" style="width:256.5px"><div class="notion-simple-table-cell">组件</div></td><td class="" style="width:436px"><div class="notion-simple-table-cell">版本</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480459a36dddbc7246eaf"><td class="" style="width:256.5px"><div class="notion-simple-table-cell">JDK</div></td><td class="" style="width:436px"><div class="notion-simple-table-cell">1.8</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a48062a233ff403c43957d"><td class="" style="width:256.5px"><div class="notion-simple-table-cell">Spring Boot</div></td><td class="" style="width:436px"><div class="notion-simple-table-cell">2.7.18</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a48048a5dfe95bdc25333f"><td class="" style="width:256.5px"><div class="notion-simple-table-cell">Apache Dubbo</div></td><td class="" style="width:436px"><div class="notion-simple-table-cell">3.2.0</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480f1a171ec3b8c2958ff"><td class="" style="width:256.5px"><div class="notion-simple-table-cell">fastjson2</div></td><td class="" style="width:436px"><div class="notion-simple-table-cell">2.0.40</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480199511f1f24198f598"><td class="" style="width:256.5px"><div class="notion-simple-table-cell">注册中心</div></td><td class="" style="width:436px"><div class="notion-simple-table-cell">Nacos</div></td></tr></tbody></table><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a4808ea01be4703b38bdc6" data-id="36960b5099a4808ea01be4703b38bdc6"><span><div id="36960b5099a4808ea01be4703b38bdc6" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4808ea01be4703b38bdc6" title="2. 先止血，但这不算结案"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>2. 先止血，但这不算结案</b></span></span></h3><div class="notion-text notion-block-36960b5099a480569ddbc383d0fc06cf">出问题的业务代码，原来是这么构造参数的：</div><div class="notion-text notion-block-36960b5099a480bcb58dcb70417ec9f5">这里的 <code class="notion-inline-code">codeSet</code> 是外面提前 <code class="notion-inline-code">new</code> 出来的同一个 <code class="notion-inline-code">Set&lt;String&gt;</code>。4 个 <code class="notion-inline-code">StatusQuery</code> 指向的是同一个实例。</div><div class="notion-text notion-block-36960b5099a4801caff7e7b63073bf88">紧急止血的改法只有一行：</div><div class="notion-text notion-block-36960b5099a480edb801f4511929e807">每个对象各拿一份自己的 <code class="notion-inline-code">statusCodeSet</code>，问题立刻消失。</div><div class="notion-text notion-block-36960b5099a48059b762e920875dd6fc">但这个改法只能救火，不能结案。它只说明“共享引用”和问题有关，还说明不了到底是谁把字段吞掉了。</div><div class="notion-text notion-block-36960b5099a480b9bf65ede59265375f">如果这里就停，很容易留下 3 个坑：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a4801fa393d5ff9dd4116e"><li>以后别的接口也可能继续踩到同一类问题</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a4806f8941cca2a788c3b0"><li>团队会把锅扣到 DTO、Lombok 或者 <code class="notion-inline-code">HashSet</code> 身上</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a480af81cad3ace1294100"><li>一旦有人把止血代码改回去，线上还会再炸</li></ul><div class="notion-text notion-block-36960b5099a480449b82f420d168e488">所以我没有停在“能跑就行”，而是继续往下收。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a48024817ace2b3d2bb79b" data-id="36960b5099a48024817ace2b3d2bb79b"><span><div id="36960b5099a48024817ace2b3d2bb79b" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a48024817ace2b3d2bb79b" title="3. 先固定现场：Arthas 把证据链补齐"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>3. 先固定现场：Arthas 把证据链补齐</b></span></span></h3><div class="notion-text notion-block-36960b5099a4806e8708d0420df75aef">我当时的排查思路很简单：</div><ol start="1" class="notion-list notion-list-numbered notion-block-36960b5099a480a6bb08c7f048594256" style="list-style-type:decimal"><li>consumer 发出去之前，参数到底是不是完整的</li></ol><ol start="2" class="notion-list notion-list-numbered notion-block-36960b5099a480deb7f7f0de8ef01c59" style="list-style-type:decimal"><li>provider 刚进方法时，参数是不是已经坏了</li></ol><ol start="3" class="notion-list notion-list-numbered notion-block-36960b5099a4805ebb93e1bf438d5ef9" style="list-style-type:decimal"><li>如果刚进方法就坏了，那就继续往前看 wire 到底走了什么序列化</li></ol><div class="notion-text notion-block-36960b5099a48099abe4c816bb436225">先用 Arthas 把 provider 入口和编解码路径盯住：</div><div class="notion-text notion-block-36960b5099a480dba2f6cb962fbfed4f">这几条证据给出的结论很明确：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a480a0872ccbbc9a00d276"><li>provider 刚进业务方法时，参数已经是“1 个完整 + 3 个字段为 null”了，说明不是业务代码后改坏的</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a480d195a1df17876beadd"><li><code class="notion-inline-code">CodecSupport.getSerialization(URL)</code> 返回的是 <code class="notion-inline-code">FastJson2Serialization</code></li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a480e3b95eee042f4624ab"><li>对应的序列化 id 是 <code class="notion-inline-code">23</code>，也就是 fastjson2</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a480b8bed8fdd073a39d26"><li>decode 栈里能看到 <code class="notion-inline-code">FastJson2ObjectInput.readObject()</code></li></ul><div class="notion-text notion-block-36960b5099a4800b8a89dc1eceae7a31">继续往前追，关键线索出现在 invoker URL 上：</div><div class="notion-text notion-block-36960b5099a48082aadfc2e118c5c18f">这一步只能说明一件事：这条 RPC 的 wire，确实按 fastjson2 在编解码。它还不能直接证明“bug 就在 fastjson2”，但 Dubbo 这层的嫌疑已经很重了。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a4806db4adfa51e0f6a0f5" data-id="36960b5099a4806db4adfa51e0f6a0f5"><span><div id="36960b5099a4806db4adfa51e0f6a0f5" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4806db4adfa51e0f6a0f5" title="4. 复现不是一次成功的"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>4. 复现不是一次成功的</b></span></span></h3><div class="notion-text notion-block-36960b5099a480cb8552f44f612e3b89">这次定位最花时间的，不是最后那张单变量表，而是前面几次“看起来马上要成了，结果又不是它”的绕路。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a48021ae22d522b67b8173" data-id="36960b5099a48021ae22d522b67b8173"><span><div id="36960b5099a48021ae22d522b67b8173" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a48021ae22d522b67b8173" title="4.1 第一版 demo 走偏了"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>4.1 第一版 demo 走偏了</b></span></span></h4><div class="notion-text notion-block-36960b5099a48033a3a3c5184b4e843f">我第一版最小复现，走的是同 JVM、裸 <code class="notion-inline-code">DubboBootstrap</code> 的方式。结果问题一堆：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a4809e85bdf7bfa3357665"><li>fastjson2 路径下会出现“连上了，但 5 秒后静默断开”的诡异现象</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a480a49593ef5650fae597"><li><code class="notion-inline-code">PermittedSerializationKeeper</code> 还会因为 service key 对不上，直接把调用拒掉</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a4801ca44bdabda2160b10"><li>hessian2 能跑，fastjson2 有时又“看起来也正常”，和线上现象完全对不上</li></ul><div class="notion-text notion-block-36960b5099a480b6b64ec4a0ce576aed">这条路最大的问题，不是代码写不出来，而是干扰变量太多。你很难知道自己现在看到的异常，到底是这次线上 bug，还是 demo 搭法本身带来的副作用。</div><div class="notion-text notion-block-36960b5099a480f3a716ea511c0ce2c1">所以我很快放弃了这条路，改成双进程、Spring Boot starter 的方式。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a48009bcbac82569c95679" data-id="36960b5099a48009bcbac82569c95679"><span><div id="36960b5099a48009bcbac82569c95679" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a48009bcbac82569c95679" title="4.2 双进程能复现，但换个干净工程又不复现了"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>4.2 双进程能复现，但换个干净工程又不复现了</b></span></span></h4><div class="notion-text notion-block-36960b5099a4808b96d0cf55a25a6935">换成 Spring Boot 双 JVM 之后，问题一度能稳定复现。</div><div class="notion-text notion-block-36960b5099a4808c9b05c03c30fe9d31">但当我把代码迁到一个更干净的 demo 工程里时，bug 又消失了。4 个对象全都完整，连共享引用都被完整还原了。</div><div class="notion-text notion-block-36960b5099a4803d8073d90f7a628598">这种时候最容易犯的错，是开始同时改很多变量。版本、注解、端口、URL、<code class="notion-inline-code">version=&quot;1.0.0&quot;</code>，一起改，很快就会把自己绕进去。</div><div class="notion-text notion-block-36960b5099a4804e987efa884afb9c82">我当时就踩了这个坑。</div><div class="notion-text notion-block-36960b5099a480eabf3af87c82af682a">一度我以为决定性因素是 <code class="notion-inline-code">version=&quot;1.0.0&quot;</code>。后来做单变量回退，才发现不是。真正起作用的，是 refer URL 里那一小段 query 参数。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a48073a58ae6b4e96f54ec" data-id="36960b5099a48073a58ae6b4e96f54ec"><span><div id="36960b5099a48073a58ae6b4e96f54ec" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a48073a58ae6b4e96f54ec" title="4.3 最后锁定的，不是版本号，而是 URL query"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>4.3 最后锁定的，不是版本号，而是 URL query</b></span></span></h4><div class="notion-text notion-block-36960b5099a480b9a4b6dc0cd166935c">我把对照实验补成了最小集：</div><table class="notion-simple-table notion-block-36960b5099a480b1918cd8bd701bf488"><tbody><tr class="notion-simple-table-row notion-simple-table-header-row notion-block-36960b5099a480e8bc73eec376d10d44"><td class="" style="width:120px"><div class="notion-simple-table-cell">URL query</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">wire 最终落点</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">是否复现</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a4800484e3c7fd0c10ee62"><td class="" style="width:120px"><div class="notion-simple-table-cell">无</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">hessian2</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">否</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a4805094dafd5a94b589de"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">serialization=fastjson2</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">fastjson2</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">是</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480e39a29c01f8306cbdf"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">prefer.serialization=fastjson2</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">fastjson2</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">是</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a4807f9238f244ff4083fd"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">prefer.serialization=fastjson2,hessian2</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">fastjson2</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">是</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a4800e86f9ed232485f1d6"><td class="" style="width:120px"><div class="notion-simple-table-cell"><code class="notion-inline-code">prefer.serialization=hessian2,fastjson2</code></div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">hessian2</div></td><td class="" style="width:120px"><div class="notion-simple-table-cell">否</div></td></tr></tbody></table><div class="notion-text notion-block-36960b5099a48071ab73e2e228a969ac">这张表一下子把问题收窄了：</div><blockquote class="notion-quote notion-block-36960b5099a480d18452d46bf187a1d6"><div>只要 wire 最终落到 fastjson2，bug 就复现；落到 hessian2，就不复现。</div></blockquote><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a480899270c3b5803b93fd" data-id="36960b5099a480899270c3b5803b93fd"><span><div id="36960b5099a480899270c3b5803b93fd" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480899270c3b5803b93fd" title="4.4 线上线下真正的差异，在 Nacos metadata"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>4.4 线上线下真正的差异，在 Nacos metadata</b></span></span></h4><div class="notion-text notion-block-36960b5099a480d0999ad35d0fdb7d91">线上走注册中心，本地 demo 一开始走的是直连 URL。两边真正的差异，不在 DTO，也不在版本号，而在 provider 注册出去的 metadata。</div><div class="notion-text notion-block-36960b5099a480348fbed2e3c5647d83">线上 provider 的 Nacos metadata 里，有这样一段：</div><div class="notion-text notion-block-36960b5099a4801f8148e8f663d7dc50">这就解释了为什么本地第一版 demo 不复现：直连模式下，consumer 不会从注册中心 merge 这段 query，自然就不会走到和线上同一条路径。</div><div class="notion-text notion-block-36960b5099a4808c875cf3475ed63d76">把直连 URL 改成下面这样以后，复现就稳定了：</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-36960b5099a48025957fe0fac9fc4777"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A2c9f0d11-e7d2-4fdc-90af-8a1e1a9610a0%3Aimage.png?table=block&amp;id=36960b50-99a4-8025-957f-e0fac9fc4777&amp;t=36960b50-99a4-8025-957f-e0fac9fc4777" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-36960b5099a4807b848dea6d45a4300d"><em>图 2：定位真正起作用的变量，不是版本号，而是 invoker URL 上的序列化 query。</em></div><div class="notion-text notion-block-36960b5099a4802d9f11c79b6b60100c">到这里，我已经知道“线上为什么会命中这条路径”了。下一步要回答的是：Dubbo 到底在这条路径里做了什么。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a480338b01ea7af5d007b7" data-id="36960b5099a480338b01ea7af5d007b7"><span><div id="36960b5099a480338b01ea7af5d007b7" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480338b01ea7af5d007b7" title="5. 把 Dubbo 拿掉之前，先把 Dubbo 这层看清"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>5. 把 Dubbo 拿掉之前，先把 Dubbo 这层看清</b></span></span></h3><div class="notion-text notion-block-36960b5099a480278b77f04d21f6e987">要想证明锅不在 Dubbo，不能只靠“我感觉像”。得先把 Dubbo 到底做了什么说清楚。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a4802cb207e80f87de6ff1" data-id="36960b5099a4802cb207e80f87de6ff1"><span><div id="36960b5099a4802cb207e80f87de6ff1" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4802cb207e80f87de6ff1" title="5.1 Dubbo 是怎么选到 fastjson2 的"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>5.1 Dubbo 是怎么选到 fastjson2 的</b></span></span></h4><div class="notion-text notion-block-36960b5099a480deab95c05f2e3d47e8">核心入口在 <code class="notion-inline-code">CodecSupport</code>：</div><div class="notion-text notion-block-36960b5099a480f696e5c4ff2d246432">继续往里走，是 <code class="notion-inline-code">UrlUtils</code>：</div><div class="notion-text notion-block-36960b5099a48044a9f9da1b89b62526">兜底默认值在 <code class="notion-inline-code">DefaultSerializationSelector</code> 里，写死的是 <code class="notion-inline-code">hessian2</code>。</div><div class="notion-text notion-block-36960b5099a480eb8b68fdd0e261d147">这几段源码放在一起，结论就很直接了：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a4804e88c8fba75ff9361d"><li>URL 上没有 <code class="notion-inline-code">serialization</code> / <code class="notion-inline-code">prefer.serialization</code>，默认走 <code class="notion-inline-code">hessian2</code></li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a480acb53fca323d1c963f"><li>URL 上一旦把 <code class="notion-inline-code">fastjson2</code> 放到第一个，最终就会选到 <code class="notion-inline-code">FastJson2Serialization</code></li></ul><div class="notion-text notion-block-36960b5099a480aeaf4ac53c5b521e4d">所以前面那张对照表，不是偶然现象，而是源码行为的直接结果。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a48009804bc6c48b16f3e8" data-id="36960b5099a48009804bc6c48b16f3e8"><span><div id="36960b5099a48009804bc6c48b16f3e8" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a48009804bc6c48b16f3e8" title="5.2 为什么线上 invoker URL 会带上 prefer.serialization"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>5.2 为什么线上 invoker URL 会带上 </b><code class="notion-inline-code"><b>prefer.serialization</b></code></span></span></h4><div class="notion-text notion-block-36960b5099a480499095d6b536c7891d">关键在 <code class="notion-inline-code">ProtocolConfig.checkDefault()</code>：</div><div class="notion-text notion-block-36960b5099a480bb9b94c05fd146d3cd">这段代码至少能说明一件事：如果 provider 没显式写 <code class="notion-inline-code">preferSerialization</code>，Dubbo 会在运行时给它补一个值。</div><div class="notion-text notion-block-36960b5099a4802ca07ec134980fa2c9">provider 端常见的配置写法是：</div><div class="notion-text notion-block-36960b5099a4802399b6c5eb3f8f8825">但这段代码本身并不能单独证明线上最终一定会注册成哪个具体值。这个问题，还是要回到线上实际抓到的 Nacos metadata 去看。</div><div class="notion-text notion-block-36960b5099a48054b4eec477e49f0a5a">而我们线上实际抓到的，就是：</div><div class="notion-text notion-block-36960b5099a480bc9a8ee1de0a0f25f2">也就是说，这里真正站得住的结论是两段证据拼起来的：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a480979c7ad44e79e0b3a2"><li><code class="notion-inline-code">ProtocolConfig.checkDefault()</code> 说明 Dubbo 确实会参与补默认值</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a48059a8f2edc923f45d35"><li>线上 Nacos metadata 证明最终注册出来的值就是 <code class="notion-inline-code">fastjson2,hessian2</code></li></ul><div class="notion-text notion-block-36960b5099a48047ab5fe6365f398064">所以线上为什么命中 fastjson2，不是猜出来的，是“源码行为 + 线上 metadata”一起对上的结果。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-36960b5099a4800ba58ec5373212401e"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3Ab0b0104b-00e7-426e-859b-41a36ad2e5db%3Aimage.png?table=block&amp;id=36960b50-99a4-800b-a58e-c5373212401e&amp;t=36960b50-99a4-800b-a58e-c5373212401e" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-36960b5099a4807daffdce64d2d7db95"><em>图 3：provider metadata 里的 </em><em><code class="notion-inline-code">prefer.serialization</code></em><em> 最后被 merge 到 consumer 的 invoker URL。</em></div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a480ab9c07c782f3a038e8" data-id="36960b5099a480ab9c07c782f3a038e8"><span><div id="36960b5099a480ab9c07c782f3a038e8" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a480ab9c07c782f3a038e8" title="5.3 为什么 consumer 自己 yml 里写 fastjson2 也不生效"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>5.3 为什么 consumer 自己 yml 里写 fastjson2 也不生效</b></span></span></h4><div class="notion-text notion-block-36960b5099a480a9aba0cf4944cb07f1">这里还有一个很容易把人带沟里的点。</div><div class="notion-text notion-block-36960b5099a48054a564c80599248dc5">我当时也试过在 consumer 自己的 yml 里写：</div><div class="notion-text notion-block-36960b5099a4800faa89c38ddedaa33e">直觉上看，这么写以后，consumer 发包不就该走 fastjson2 了吗？</div><div class="notion-text notion-block-36960b5099a480f192fdd74af66cfb77">但 Dubbo 这里不是这么分层的。<code class="notion-inline-code">dubbo.protocol.*</code> 对应的是 <code class="notion-inline-code">ProtocolConfig</code>，它只影响当前应用自己 export 出去的 URL。provider 的业务接口 export 用它，consumer 自己 export 的 <code class="notion-inline-code">MetadataService</code> 也用它，但它<b>不参与 consumer 的 refer URL 组装</b>。</div><div class="notion-text notion-block-36960b5099a4805fbb14cbca360513c6">consumer 发包时真正看的，还是 <code class="notion-inline-code">ReferenceConfig</code> 最后拼出来的 invoker URL。也就是说，决定 wire 协议的不是“consumer 本地 yml 有没有写 fastjson2”，而是“最终 invoker URL 的 query 上有没有 <code class="notion-inline-code">serialization</code> 或 <code class="notion-inline-code">prefer.serialization</code>”。</div><div class="notion-text notion-block-36960b5099a480bd9f0ecb90913497cf">这也是为什么前面那组实验里，真正有决定性的始终是 URL query，不是本地 yml。</div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-36960b5099a4804e9bffddf85081a143" data-id="36960b5099a4804e9bffddf85081a143"><span><div id="36960b5099a4804e9bffddf85081a143" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4804e9bffddf85081a143" title="5.4 Dubbo 对 fastjson2 做了什么"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>5.4 Dubbo 对 fastjson2 做了什么</b></span></span></h4><div class="notion-text notion-block-36960b5099a4800faa73fe98b88db8b1">Dubbo 在这条链路里，真正做的主要是两件事。</div><div class="notion-text notion-block-36960b5099a48014bb86fbbf4b1f0854">第一件事，是把参数交给 <code class="notion-inline-code">FastJson2ObjectOutput</code>，并启用它那组 features：</div><div class="notion-text notion-block-36960b5099a480d49233d6d019659e94">这里最关键的是 <code class="notion-inline-code">ReferenceDetection</code>。它一开，共享引用就不会重复内联写值，而是会写成 <code class="notion-inline-code">$ref</code>。</div><div class="notion-text notion-block-36960b5099a4806fa7dade869b73c8cd">第二件事，是 provider 侧 decode 时，按 <code class="notion-inline-code">Class[]</code> 去读参数：</div><div class="notion-text notion-block-36960b5099a48076a7c0f28fb35bade7">对 <code class="notion-inline-code">Set&lt;StatusQuery&gt;</code> 这种签名来说，这里拿到的是 <code class="notion-inline-code">Set.class</code>。再往下会走进 <code class="notion-inline-code">FastJson2ObjectInput</code>，继续交给 JSONB 去 parse。</div><div class="notion-text notion-block-36960b5099a480d5bd82d681cc036f57">也就是说，Dubbo 在这条路径里，更像一个调度员：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a4805a997de93302e3da3a"><li>它决定最后选哪个序列化实现</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a4803ea998d51642cc5642"><li>它决定调用 fastjson2 时用哪组 writer / reader features</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a480218e5df7d350d5fcb0"><li>它把参数交给 fastjson2 去真正做字节层的写和读</li></ul><div class="notion-text notion-block-36960b5099a480afaecfefb5556b7c78">如果把 Dubbo 整层拿掉，只保留这组 features，问题还能复现，那锅就不能再算到 Dubbo 头上了。</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-36960b5099a480239034cc31b05e8052"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3Ae20e6187-2a62-4427-8fcf-bf217395af01%3Aimage.png?table=block&amp;id=36960b50-99a4-8023-9034-cc31b05e8052&amp;t=36960b50-99a4-8023-9034-cc31b05e8052" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-36960b5099a4801cb198dbd1f7d84739"><em>图 4：Dubbo 负责选择路径和透传 features，真正的字节写读仍然在 fastjson2 里发生。</em></div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a4800a9ba9e4430c8d6a6e" data-id="36960b5099a4800a9ba9e4430c8d6a6e"><span><div id="36960b5099a4800a9ba9e4430c8d6a6e" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4800a9ba9e4430c8d6a6e" title="6. 把 Dubbo 拿掉，问题还在"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>6. 把 Dubbo 拿掉，问题还在</b></span></span></h3><div class="notion-text notion-block-36960b5099a480909bc0fb88250b731d">我后面做的实验，就是把 Dubbo 完全剥掉，只保留它实际用到的那组 fastjson2 writer / reader features，直接跑：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a4803dbf7fe3472399071b"><li><code class="notion-inline-code">JSONB.toBytes(...)</code></li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a480498b1acdcf55cc2a0c"><li><code class="notion-inline-code">JSONB.parseObject(...)</code></li></ul><div class="notion-text notion-block-36960b5099a480449b4dc897b98c5656">而且这次不是传裸 <code class="notion-inline-code">Set.class</code>，我直接传了完整泛型：</div><div class="notion-text notion-block-36960b5099a48064afb2cb7132736f35">如果这样还能复现，就说明问题已经和 Dubbo 业务路径无关了。</div><div class="notion-text notion-block-36960b5099a480f08f88e8eab6bbb469">结果是：<b>能复现。</b></div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-36960b5099a480039c1fd789197ecc4c"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A9637c8ee-833f-4ddc-a6e6-993735d43615%3Aimage.png?table=block&amp;id=36960b50-99a4-8003-9c1f-d789197ecc4c&amp;t=36960b50-99a4-8003-9c1f-d789197ecc4c" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-36960b5099a48025ba13ee5ce31b4314"><em>图 5：去掉 Dubbo 后仍然是同样的结果，这一步把 Dubbo 从根因里排掉了。</em></div><div class="notion-text notion-block-36960b5099a480eb812ee6b1e30592ad">我又做了 5 组单变量对照，把几个最容易误判的方向全洗了一遍：</div><table class="notion-simple-table notion-block-36960b5099a4801db81ecf3bac51bacc"><tbody><tr class="notion-simple-table-row notion-simple-table-header-row notion-block-36960b5099a4801ca469f41409a2f3a7"><td class="" style="width:388px"><div class="notion-simple-table-cell">单变量变更</div></td><td class="" style="width:307.53125px"><div class="notion-simple-table-cell">结果</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a4800d97bccc4435125db6"><td class="" style="width:388px"><div class="notion-simple-table-cell">baseline：<code class="notion-inline-code">Set&lt;StatusQuery&gt;</code> + 共享 <code class="notion-inline-code">statusCodeSet</code> + Dubbo 真实 features</div></td><td class="" style="width:307.53125px"><div class="notion-simple-table-cell">1 完整 + 3 null</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480bd813cf4e8d74b4e72"><td class="" style="width:388px"><div class="notion-simple-table-cell">去掉 writer 的 <code class="notion-inline-code">ReferenceDetection</code></div></td><td class="" style="width:307.53125px"><div class="notion-simple-table-cell">4 完整</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a48041af6cd1335bcb5266"><td class="" style="width:388px"><div class="notion-simple-table-cell">去掉 reader 的 <code class="notion-inline-code">UseNativeObject</code></div></td><td class="" style="width:307.53125px"><div class="notion-simple-table-cell">1 完整 + 3 null</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a4803ea090d82f0afe053d"><td class="" style="width:388px"><div class="notion-simple-table-cell">把 DTO 从 <code class="notion-inline-code">@Data</code> 改成只按 <code class="notion-inline-code">sn</code> 做 <code class="notion-inline-code">equals/hashCode</code></div></td><td class="" style="width:307.53125px"><div class="notion-simple-table-cell">1 完整 + 3 null</div></td></tr><tr class="notion-simple-table-row notion-block-36960b5099a480e0a57ad6cbb70dcf57"><td class="" style="width:388px"><div class="notion-simple-table-cell">外层从 <code class="notion-inline-code">Set&lt;StatusQuery&gt;</code> 改成 <code class="notion-inline-code">List&lt;StatusQuery&gt;</code></div></td><td class="" style="width:307.53125px"><div class="notion-simple-table-cell">4 完整</div></td></tr></tbody></table><div class="notion-text notion-block-36960b5099a48047ac06fb22a8f4f0ce">这张表的价值很大。因为它把几个“看起来像根因”的东西排掉了：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a48081a85dd0a76cd25a52"><li><code class="notion-inline-code">UseNativeObject</code> 不是必要条件</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a4807ba726eb3ee8d79daf"><li>Lombok <code class="notion-inline-code">@Data</code> 不是必要条件</li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a4809bb623fe249770788e"><li>泛型擦除也不是必要条件</li></ul><div class="notion-text notion-block-36960b5099a480df8233f664c94dca14">最后真正留下来的，只有 3 条必要条件：</div><figure class="notion-asset-wrapper notion-asset-wrapper-image notion-block-36960b5099a48045b3e5e1533f3f6d4e"><div style="position:relative;display:flex;justify-content:center;align-self:center;width:100%;max-width:100%;flex-direction:column;height:100%"><img style="object-fit:cover" src="https://www.notion.so/image/attachment%3A1d9dd0e5-8c8e-4277-85dc-4603d9ff5102%3Aimage.png?table=block&amp;id=36960b50-99a4-8045-b3e5-e1533f3f6d4e&amp;t=36960b50-99a4-8045-b3e5-e1533f3f6d4e" alt="notion image" loading="lazy" decoding="async"/></div></figure><div class="notion-text notion-block-36960b5099a4800aaaacd4ced7570b39"><em>图 6：真正的最小条件只有 3 条，其他更像放大器，不是根开关。</em></div><div class="notion-text notion-block-36960b5099a480b09d74eed64a8ad62f">到这里就够了。</div><div class="notion-text notion-block-36960b5099a480eea8e4dfe2f6642862">第一篇的任务，不是把 fastjson2 内部每一行源码都拆开，而是把最小问题路径钉死：</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a4800a9e2dcac5aa0dc1fa" data-id="36960b5099a4800a9e2dcac5aa0dc1fa"><span><div id="36960b5099a4800a9e2dcac5aa0dc1fa" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a4800a9e2dcac5aa0dc1fa" title="7. 定位问题中间件"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title"><b>7. 定位问题中间件</b></span></span></h3><div class="notion-text notion-block-36960b5099a480f78b0bda4e3c52d340">到这里已经确认完了：问题不是 Dubbo 业务层的 bug，而是落在 fastjson2 这条序列化路径上。后面如果继续往下拆，我会单独写 fastjson2 的写端、读端和补丁思路。</div><div class="notion-text notion-block-36960b5099a4801d871dfd2cf97f2d87">完整 demo 我已经整理到 GitHub：</div><ul class="notion-list notion-list-disc notion-block-36960b5099a480d2b462fc42a4df5235"><li><a class="notion-link" href="https://github.com/yibiner/dubbo-fastjson2-shared-ref-bug-demo#" target="_blank" rel="noopener noreferrer">dubbo-fastjson2-shared-ref-bug-demo</a></li></ul><div class="notion-text notion-block-36960b5099a480ca8c6bd42761395e83">真正难的，从来不是“猜到像是哪个组件有问题”。</div><div class="notion-text notion-block-36960b5099a480a490dbc52538ad38c6">真正难的是，你得一层一层把中间件里没问题的部分排掉，跟着数据流走，把最小问题路径逼出来。等只剩最后那条路径的时候，锅是谁的，反而就不难看了。</div><h3 class="notion-h notion-h2 notion-h-indent-0 notion-block-36960b5099a48044960cc20257a024cc" data-id="36960b5099a48044960cc20257a024cc"><span><div id="36960b5099a48044960cc20257a024cc" class="notion-header-anchor"></div><a class="notion-hash-link" href="#36960b5099a48044960cc20257a024cc" title="8. 参考资料"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">8. 参考资料</span></span></h3><ul class="notion-list notion-list-disc notion-block-36960b5099a4808fac10e85fff283a67"><li>dubbo源码：<a class="notion-link" href="https://github.com/apache/dubbo.git" target="_blank" rel="noopener noreferrer">https://github.com/apache/dubbo.git</a></li></ul><ul class="notion-list notion-list-disc notion-block-36960b5099a480699e1afaeca61c7f42"><li>fastjson2源码：<a class="notion-link" href="https://github.com/alibaba/fastjson2" target="_blank" rel="noopener noreferrer">https://github.com/alibaba/fastjson2</a></li></ul><div class="notion-blank notion-block-36960b5099a48032b14aea46f12fb35c"> </div><div class="notion-blank notion-block-36960b5099a4803d837dc3e3ec000fd3"> </div></main></div>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Claude Code CLI 完整安装教程：从 0 到成功运行]]></title>
            <link>https://yibin.dev/article/31e60b50-99a4-80f4-8ef2-dbeb1efe9655</link>
            <guid>https://yibin.dev/article/31e60b50-99a4-80f4-8ef2-dbeb1efe9655</guid>
            <pubDate>Mon, 09 Mar 2026 00:00:00 GMT</pubDate>
            <content:encoded><![CDATA[<div id="notion-article" class="mx-auto overflow-hidden "><main class="notion light-mode notion-page notion-block-31e60b5099a480f48ef2dbeb1efe9655"><div class="notion-viewport"></div><div class="notion-collection-page-properties"></div><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-31e60b5099a4808988d6c37bdbff24ad" data-id="31e60b5099a4808988d6c37bdbff24ad"><span><div id="31e60b5099a4808988d6c37bdbff24ad" class="notion-header-anchor"></div><a class="notion-hash-link" href="#31e60b5099a4808988d6c37bdbff24ad" title="🧠 Claude 是什么"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">🧠 Claude 是什么</span></span></h2><div class="notion-text notion-block-31e60b5099a4803ca2a4e21ab32b73cd"><b>Anthropic</b> 推出的 <b>Claude</b> 是一款面向开发者与专业用户的先进大语言模型助手，主打：</div><ul class="notion-list notion-list-disc notion-block-31e60b5099a480e3b1e2f6133672b4f7"><li>更强的代码理解与生成能力</li></ul><ul class="notion-list notion-list-disc notion-block-31e60b5099a4807b96ece17694fd0d12"><li>更长的上下文处理能力</li></ul><ul class="notion-list notion-list-disc notion-block-31e60b5099a48091afbacc9981baa713"><li>更安全、可控的 AI 输出</li></ul><ul class="notion-list notion-list-disc notion-block-31e60b5099a480db9880e3ea841b2a3b"><li>更自然的对话体验</li></ul><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-31e60b5099a4807b9d1df69c8686a682" data-id="31e60b5099a4807b9d1df69c8686a682"><span><div id="31e60b5099a4807b9d1df69c8686a682" class="notion-header-anchor"></div><a class="notion-hash-link" href="#31e60b5099a4807b9d1df69c8686a682" title="📦 安装 Claude Code"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">📦 安装 Claude Code</span></span></h2><div class="notion-text notion-block-31e60b5099a4807297b2e47a5d874ece">官方提供了一键安装脚本，直接在终端运行：</div><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-31e60b5099a480e18383f3df060d9d25" data-id="31e60b5099a480e18383f3df060d9d25"><span><div id="31e60b5099a480e18383f3df060d9d25" class="notion-header-anchor"></div><a class="notion-hash-link" href="#31e60b5099a480e18383f3df060d9d25" title="⚙️ 配置 PATH 环境变量"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">⚙️ 配置 PATH 环境变量</span></span></h2><div class="notion-text notion-block-31e60b5099a480bf8dc1c80ea9d546db">执行以下命令，将 Claude 加入系统可执行路径：</div><div class="notion-text notion-block-31e60b5099a480abb8a0e8c9fedccb52">如果你使用的是 bash：</div><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-32d60b5099a48091a96beb61176d75f1" data-id="32d60b5099a48091a96beb61176d75f1"><span><div id="32d60b5099a48091a96beb61176d75f1" class="notion-header-anchor"></div><a class="notion-hash-link" href="#32d60b5099a48091a96beb61176d75f1" title="🖨 国内网络安装"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">🖨 国内网络安装</span></span></h2><div class="notion-text notion-block-32d60b5099a48018b409dd6281c865d0">使用 claude 官方脚本安装，是需要网络环境支持的，不然在下载脚本时候会报“地区不支持”。Claude Code 是一个 npm 包，所以可以直接使用 <code class="notion-inline-code">npm install -g @anthropic-ai/claude-code</code> 来安装，相关依赖的环境按照参考以下脚本。</div><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-31e60b5099a4807ea27bed9d49b70fda" data-id="31e60b5099a4807ea27bed9d49b70fda"><span><div id="31e60b5099a4807ea27bed9d49b70fda" class="notion-header-anchor"></div><a class="notion-hash-link" href="#31e60b5099a4807ea27bed9d49b70fda" title="✅ 验证是否安装成功"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">✅ 验证是否安装成功</span></span></h2><div class="notion-blank notion-block-31e60b5099a4809c8cc3ffcc1b1a506e"> </div><div class="notion-text notion-block-31e60b5099a4802dadb2dcb79bf4f4c0">此时命令行输入 <code class="notion-inline-code">claude</code>，会引导登录 claude 账号和配置，如果使用官方账号，按照引导完成输入就可以了</div><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-31e60b5099a480388003d6b0a5fe933d" data-id="31e60b5099a480388003d6b0a5fe933d"><span><div id="31e60b5099a480388003d6b0a5fe933d" class="notion-header-anchor"></div><a class="notion-hash-link" href="#31e60b5099a480388003d6b0a5fe933d" title="🧩 API 环境配置"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">🧩 API 环境配置</span></span></h2><div class="notion-text notion-block-31e60b5099a48048957de958eba58749">如果你希望使用 API Token（例如自建代理 / 企业网关），可以配置：</div><div class="notion-blank notion-block-31e60b5099a480928192ccf024b67ece"> </div><h4 class="notion-h notion-h3 notion-h-indent-1 notion-block-31e60b5099a480408d5efe07f20e754d" data-id="31e60b5099a480408d5efe07f20e754d"><span><div id="31e60b5099a480408d5efe07f20e754d" class="notion-header-anchor"></div><a class="notion-hash-link" href="#31e60b5099a480408d5efe07f20e754d" title="字段说明"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">字段说明</span></span></h4><table class="notion-simple-table notion-block-31e60b5099a480fd9485dd1fd8a60780"><tbody><tr class="notion-simple-table-row notion-simple-table-header-row notion-block-31e60b5099a480d284cdc0f9d1edf1c6"><td class="" style="width:217.5px"><div class="notion-simple-table-cell">配置项</div></td><td class="" style="width:583.828125px"><div class="notion-simple-table-cell">作用</div></td></tr><tr class="notion-simple-table-row notion-block-31e60b5099a480568866d3c94cf2f847"><td class="" style="width:217.5px"><div class="notion-simple-table-cell">ANTHROPIC_AUTH_TOKEN</div></td><td class="" style="width:583.828125px"><div class="notion-simple-table-cell">API 密钥</div></td></tr><tr class="notion-simple-table-row notion-block-31e60b5099a4804ba062f6dcb954aacc"><td class="" style="width:217.5px"><div class="notion-simple-table-cell">ANTHROPIC_BASE_URL</div></td><td class="" style="width:583.828125px"><div class="notion-simple-table-cell">自定义 API 地址（代理/镜像）</div></td></tr><tr class="notion-simple-table-row notion-block-31e60b5099a480229192e855a8c7d83f"><td class="" style="width:217.5px"><div class="notion-simple-table-cell">API_TIMEOUT_MS</div></td><td class="" style="width:583.828125px"><div class="notion-simple-table-cell">请求超时时间</div></td></tr><tr class="notion-simple-table-row notion-block-31e60b5099a480e5b49de974f8678601"><td class="" style="width:217.5px"><div class="notion-simple-table-cell">DEFAULT_*_MODEL</div></td><td class="" style="width:583.828125px"><div class="notion-simple-table-cell">默认模型配置</div></td></tr></tbody></table><div class="notion-text notion-block-31e60b5099a480a6b1f9e81397f2a72a">其他字段解释见官方文档：<a class="notion-link" href="https://code.claude.com/docs/zh-CN/settings" target="_blank" rel="noopener noreferrer">https://code.claude.com/docs/zh-CN/settings</a></div><h2 class="notion-h notion-h1 notion-h-indent-0 notion-block-31e60b5099a4803f83d6e7d8068912ca" data-id="31e60b5099a4803f83d6e7d8068912ca"><span><div id="31e60b5099a4803f83d6e7d8068912ca" class="notion-header-anchor"></div><a class="notion-hash-link" href="#31e60b5099a4803f83d6e7d8068912ca" title="🧭 跳过新手引导"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">🧭 跳过新手引导</span></span></h2><div class="notion-text notion-block-31e60b5099a4807282e2fd51f82f6806">在<code class="notion-inline-code">~/.claude.json</code> 文件中增加如下配置，用于跳过首次引导流程。</div><h3 class="notion-h notion-h2 notion-h-indent-1 notion-block-31e60b5099a48017a611e9d3606151e7" data-id="31e60b5099a48017a611e9d3606151e7"><span><div id="31e60b5099a48017a611e9d3606151e7" class="notion-header-anchor"></div><a class="notion-hash-link" href="#31e60b5099a48017a611e9d3606151e7" title="🎉 成功启动 Claude Code"><svg viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z"></path></svg></a><span class="notion-h-title">🎉 成功启动 Claude Code</span></span></h3><div class="notion-text notion-block-31e60b5099a4809ea952c423ac209346">此时即可开始交互：</div><div class="notion-text notion-block-31e60b5099a4808bae9bc0b967428b3f">说明本地 CLI 已可正常使用。</div><div class="notion-blank notion-block-31e60b5099a480acbe1fd6ed52fb845f"> </div><div class="notion-blank notion-block-31e60b5099a480c68af0fcedda8bed4a"> </div><div class="notion-blank notion-block-31e60b5099a480f58a0dc59a123ca92b"> </div></main></div>]]></content:encoded>
        </item>
    </channel>
</rss>