<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>后端 on 吾爱主机</title><link>https://blog.waihost.com/categories/%E5%90%8E%E7%AB%AF/</link><description>Recent content in 后端 on 吾爱主机</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Thu, 30 Jul 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://blog.waihost.com/categories/%E5%90%8E%E7%AB%AF/index.xml" rel="self" type="application/rss+xml"/><item><title>Redis 分布式锁：互斥、安全释放与续期工程实践</title><link>https://blog.waihost.com/posts/redis-distributed-lock-mutex-renewal-practice/</link><pubDate>Thu, 30 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/redis-distributed-lock-mutex-renewal-practice/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/redis-distributed-lock-mutex-renewal-practice.svg" alt="Featured image of post Redis 分布式锁：互斥、安全释放与续期工程实践" /&gt;&lt;p&gt;下单扣库存、定时任务防重、接口幂等窗口、分片任务抢占——多进程/多实例同时触达同一共享资源时，都需要&lt;strong&gt;互斥&lt;/strong&gt;。本地 &lt;code&gt;synchronized&lt;/code&gt; 或 &lt;code&gt;Mutex&lt;/code&gt; 只覆盖本机；跨进程时，工程上常见做法是把锁状态放到 &lt;strong&gt;Redis&lt;/strong&gt; 这类高可用共享存储里。&lt;/p&gt;
&lt;p&gt;本文依据 Redis 官方 &lt;em&gt;Distributed Locks with Redis&lt;/em&gt;、&lt;code&gt;SET&lt;/code&gt; 命令文档（含 Patterns）、&lt;em&gt;Scripting with Lua&lt;/em&gt; 与 &lt;code&gt;GETDEL&lt;/code&gt; 说明，把&lt;strong&gt;单实例正确加解锁、误删他人锁、主从异步复制下的安全边界、锁续期，以及 Redlock/fencing token 的工程取舍&lt;/strong&gt;讲清楚，并给出可复现命令实验与排错清单。它和本站「缓存穿透/击穿/雪崩」互补：那篇讲缓存一致性与流量保护，本篇讲&lt;strong&gt;跨实例互斥原语本身&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="一问题背景分布式互斥要什么"&gt;&lt;a href="#%e4%b8%80%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af%e5%88%86%e5%b8%83%e5%bc%8f%e4%ba%92%e6%96%a5%e8%a6%81%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;一、问题背景：分布式互斥要什么
&lt;/h2&gt;&lt;p&gt;官方把有效的分布式锁最小保证概括为三条（Safety / Liveness）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;属性&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;工程直觉&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;互斥（Safety）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;任意时刻最多一个客户端持有锁&lt;/td&gt;
					&lt;td&gt;两台机器不能同时“以为自己锁住了”&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;无死锁（Liveness A）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;持锁客户端崩溃或分区后，锁最终可被再次获取&lt;/td&gt;
					&lt;td&gt;必须有 TTL / 自动释放&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;容错（Liveness B）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;多数 Redis 节点存活时仍可加解锁（面向 Redlock 场景）&lt;/td&gt;
					&lt;td&gt;单点故障与多数派策略&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;只满足“看起来能 SET 成功”不够。真正会在生产踩雷的，通常是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;加锁非原子&lt;/strong&gt;：&lt;code&gt;SETNX&lt;/code&gt; 成功后再 &lt;code&gt;EXPIRE&lt;/code&gt;，进程在中间崩溃 → 永久锁死；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;解锁不安全&lt;/strong&gt;：业务超时后锁已过期被别人抢走，本客户端仍 &lt;code&gt;DEL&lt;/code&gt; → &lt;strong&gt;删掉别人的锁&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无 token 签名&lt;/strong&gt;：所有客户端写同一个固定 value，无法判断“这把锁还是不是我的”；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;把主从 failover 当强一致&lt;/strong&gt;：异步复制下会出现双持锁窗口。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="二核心机制单实例正确加解锁"&gt;&lt;a href="#%e4%ba%8c%e6%a0%b8%e5%bf%83%e6%9c%ba%e5%88%b6%e5%8d%95%e5%ae%9e%e4%be%8b%e6%ad%a3%e7%a1%ae%e5%8a%a0%e8%a7%a3%e9%94%81" class="header-anchor"&gt;&lt;/a&gt;二、核心机制：单实例正确加解锁
&lt;/h2&gt;&lt;h3 id="21-原子加锁set-key-token-nx-px-ttl"&gt;&lt;a href="#21-%e5%8e%9f%e5%ad%90%e5%8a%a0%e9%94%81set-key-token-nx-px-ttl" class="header-anchor"&gt;&lt;/a&gt;2.1 原子加锁：&lt;code&gt;SET key token NX PX ttl&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;官方单实例推荐写法（毫秒 TTL 示例）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SET resource_name my_random_value NX PX 30000
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对应语义（见 &lt;code&gt;SET&lt;/code&gt; 文档）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;选项&lt;/th&gt;
					&lt;th&gt;作用&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;NX&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;仅当 key 不存在&lt;/strong&gt;时才设置&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;PX milliseconds&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;以毫秒设置过期（也可用 &lt;code&gt;EX seconds&lt;/code&gt;）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;value = 随机 token&lt;/td&gt;
					&lt;td&gt;标识“这把锁属于哪个客户端/哪次加锁”&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;SET&lt;/code&gt; 成功返回 &lt;code&gt;OK&lt;/code&gt; 表示抢到锁；key 已存在时（&lt;code&gt;NX&lt;/code&gt; 条件不满足）返回空，表示未抢到。&lt;code&gt;NX&lt;/code&gt; 与过期时间在&lt;strong&gt;同一条命令&lt;/strong&gt;里完成，避免“加锁成功但没 TTL”的经典坑。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SET&lt;/code&gt; 文档的 Patterns 小节也写了同类形态：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SET resource-name anystring NX EX max-lock-time
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;并强调：简单 &lt;code&gt;DEL&lt;/code&gt; 解锁可工作，但更稳健的做法是 &lt;strong&gt;token + 条件删除&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="22-为什么-value-必须是随机-token"&gt;&lt;a href="#22-%e4%b8%ba%e4%bb%80%e4%b9%88-value-%e5%bf%85%e9%a1%bb%e6%98%af%e9%9a%8f%e6%9c%ba-token" class="header-anchor"&gt;&lt;/a&gt;2.2 为什么 value 必须是随机 token
&lt;/h3&gt;&lt;p&gt;官方要求 value &lt;strong&gt;在所有客户端、所有加锁请求间唯一&lt;/strong&gt;。建议量级约 20 字节随机（如 &lt;code&gt;/dev/urandom&lt;/code&gt;）；也可用“微秒时间戳 + 客户端 ID”等折中方案（安全性略弱，但多数场景够用）。&lt;/p&gt;
&lt;p&gt;token 的唯一用途：解锁时证明“我仍是当前持有者”，避免过期后误删他人锁。&lt;/p&gt;
&lt;h3 id="23-安全解锁条件删除而不是裸-del"&gt;&lt;a href="#23-%e5%ae%89%e5%85%a8%e8%a7%a3%e9%94%81%e6%9d%a1%e4%bb%b6%e5%88%a0%e9%99%a4%e8%80%8c%e4%b8%8d%e6%98%af%e8%a3%b8-del" class="header-anchor"&gt;&lt;/a&gt;2.3 安全解锁：条件删除，而不是裸 &lt;code&gt;DEL&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;错误写法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DEL resource_name
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;时序灾难：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端 A 加锁，TTL = 30s；&lt;/li&gt;
&lt;li&gt;A 业务卡住超过 30s，key 过期；&lt;/li&gt;
&lt;li&gt;客户端 B 成功 &lt;code&gt;SET NX&lt;/code&gt; 拿到同一资源；&lt;/li&gt;
&lt;li&gt;A 恢复后执行 &lt;code&gt;DEL&lt;/code&gt; → &lt;strong&gt;B 的锁被删掉&lt;/strong&gt;，C 又能进入临界区。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;官方正确路径有两层：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;（1）Redis 8.4+：&lt;code&gt;DELEX&lt;/code&gt; 条件删除（文档写法）&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DELEX key IFEQ my_random_value
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;（2）更通用：Lua 脚本（8.4 之前也适用）&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-lua" data-lang="lua"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;if&lt;/span&gt; &lt;span class="n"&gt;redis.call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;get&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kr"&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="n"&gt;redis.call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;del&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;调用形态：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EVAL &amp;#34;&amp;lt;script&amp;gt;&amp;#34; 1 resource_name token-value
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;GET&lt;/code&gt; 比较与 &lt;code&gt;DEL&lt;/code&gt; 在脚本内完成。官方 &lt;em&gt;Scripting with Lua&lt;/em&gt; 明确：脚本在服务器侧&lt;strong&gt;原子执行&lt;/strong&gt;，执行期间阻塞其他活动——这对“校验 token 再删除”至关重要，客户端侧的 &lt;code&gt;GET&lt;/code&gt; + &lt;code&gt;DEL&lt;/code&gt; 两步命令&lt;strong&gt;不是&lt;/strong&gt;安全等价物。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;注意：&lt;code&gt;GETDEL&lt;/code&gt; 会“读出并删除”，但它&lt;strong&gt;不校验 token&lt;/strong&gt;，不能单独当作安全解锁命令。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="24-锁有效期lock-validity-time的双重含义"&gt;&lt;a href="#24-%e9%94%81%e6%9c%89%e6%95%88%e6%9c%9flock-validity-time%e7%9a%84%e5%8f%8c%e9%87%8d%e5%90%ab%e4%b9%89" class="header-anchor"&gt;&lt;/a&gt;2.4 锁有效期（lock validity time）的双重含义
&lt;/h3&gt;&lt;p&gt;TTL 既是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;自动释放时间&lt;/strong&gt;（持锁者崩溃后的死锁自愈）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持锁者必须在此窗口内完成工作&lt;/strong&gt;的时间预算。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;互斥保证是&lt;strong&gt;时间窗口内的互斥&lt;/strong&gt;，不是“进程活着就永远持锁”。业务若可能超过 TTL，要么加大合理 TTL，要么做&lt;strong&gt;续期&lt;/strong&gt;，并在业务层准备好&lt;strong&gt;超时后的幂等/回滚&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="三主从复制为什么不够天然安全"&gt;&lt;a href="#%e4%b8%89%e4%b8%bb%e4%bb%8e%e5%a4%8d%e5%88%b6%e4%b8%ba%e4%bb%80%e4%b9%88%e4%b8%8d%e5%a4%9f%e5%a4%a9%e7%84%b6%e5%ae%89%e5%85%a8" class="header-anchor"&gt;&lt;/a&gt;三、主从复制为什么不够“天然安全”
&lt;/h2&gt;&lt;p&gt;单实例方案在“偶尔双持可接受”的业务里很实用；但若把 Redis 主从 + 自动 failover 当成强一致锁，会踩官方点名的竞态：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;客户端 A 在 master 上拿到锁；&lt;/li&gt;
&lt;li&gt;master 在把该写复制到 replica 前崩溃；&lt;/li&gt;
&lt;li&gt;replica 升主；&lt;/li&gt;
&lt;li&gt;客户端 B 在新 master 上再次 &lt;code&gt;SET NX&lt;/code&gt; 成功 → &lt;strong&gt;Safety 被破坏&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;原因：Redis 复制默认是&lt;strong&gt;异步&lt;/strong&gt;的。官方态度很务实：若故障场景下短暂双持可接受，复制方案可用；否则应认真评估 &lt;strong&gt;Redlock&lt;/strong&gt; 或更强的一致性组件，并在业务侧使用 &lt;strong&gt;fencing token&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="四redlock-概要多数派--耗时校验"&gt;&lt;a href="#%e5%9b%9bredlock-%e6%a6%82%e8%a6%81%e5%a4%9a%e6%95%b0%e6%b4%be--%e8%80%97%e6%97%b6%e6%a0%a1%e9%aa%8c" class="header-anchor"&gt;&lt;/a&gt;四、Redlock 概要：多数派 + 耗时校验
&lt;/h2&gt;&lt;p&gt;Redlock 假设有 &lt;strong&gt;N 个独立 Redis master&lt;/strong&gt;（示例 N=5，互不依赖复制协调）。客户端流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;记录当前毫秒时间戳 &lt;code&gt;T1&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;用&lt;strong&gt;相同 key 与相同随机 value&lt;/strong&gt;，在 N 个实例上并行 &lt;code&gt;SET ... NX PX&lt;/code&gt;；对每个实例使用&lt;strong&gt;远小于锁 TTL&lt;/strong&gt; 的连接/命令超时（例如 TTL=10s 时超时约 5–50ms），避免卡在不可用节点上；&lt;/li&gt;
&lt;li&gt;计算耗时 &lt;code&gt;elapsed = now - T1&lt;/code&gt;；&lt;strong&gt;仅当&lt;/strong&gt;在多数实例（至少 &lt;code&gt;N/2+1&lt;/code&gt;）加锁成功，且 &lt;code&gt;elapsed &amp;lt; TTL&lt;/code&gt; 时，才认为获得锁；&lt;/li&gt;
&lt;li&gt;有效剩余时间约为 &lt;code&gt;TTL - elapsed&lt;/code&gt;（再扣一点时钟漂移余量）；&lt;/li&gt;
&lt;li&gt;若失败：对&lt;strong&gt;所有&lt;/strong&gt;实例尝试解锁（包括“以为没锁上”的），尽快释放部分锁。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;失败重试应加&lt;strong&gt;随机退避&lt;/strong&gt;，降低多客户端同时抢锁导致的“脑裂式空转”。释放时无论是否自认成功，都应尝试释放，降低依赖 TTL 的可用性惩罚。&lt;/p&gt;
&lt;h3 id="41-续期lock-extension"&gt;&lt;a href="#41-%e7%bb%ad%e6%9c%9flock-extension" class="header-anchor"&gt;&lt;/a&gt;4.1 续期（lock extension）
&lt;/h3&gt;&lt;p&gt;若工作可拆成小步骤，可用较短默认 TTL，并在接近过期时向多数实例发送 Lua：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;仅当 key 存在且 value 仍是自己的 token 时，延长 TTL；&lt;/li&gt;
&lt;li&gt;仅当多数实例续期成功且仍在有效时间内，才认为续期成功；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;必须限制最大续期次数&lt;/strong&gt;，否则可能破坏 liveness（锁永远被同一客户端拖住）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这与许多客户端库里的 “watchdog / lock watchdog” 思路一致：续期是&lt;strong&gt;持锁期间的重新确认&lt;/strong&gt;，不是“进程活着就一定安全”。&lt;/p&gt;
&lt;h3 id="42-时钟与一致性免责声明官方-disclaimer"&gt;&lt;a href="#42-%e6%97%b6%e9%92%9f%e4%b8%8e%e4%b8%80%e8%87%b4%e6%80%a7%e5%85%8d%e8%b4%a3%e5%a3%b0%e6%98%8e%e5%ae%98%e6%96%b9-disclaimer" class="header-anchor"&gt;&lt;/a&gt;4.2 时钟与一致性免责声明（官方 disclaimer）
&lt;/h3&gt;&lt;p&gt;官方在文档末尾明确提醒：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;若关心正确性，应实现 &lt;strong&gt;fencing tokens&lt;/strong&gt;；不要假设“进程还活着 = 锁一定还在”；&lt;/li&gt;
&lt;li&gt;Redis TTL 不依赖单调时钟，&lt;strong&gt;墙钟跳变&lt;/strong&gt;理论上可能导致异常；应规范运维（避免手工乱改时间、正确配置 NTP），但仍不能把时钟理想化。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Martin Kleppmann 对 Redlock 的分析与 antirez 的回应均被官方列入阅读清单——工程选型时请当作&lt;strong&gt;必读辩论&lt;/strong&gt;，而不是站队口号。&lt;/p&gt;
&lt;h2 id="五工程增强fencing-token-与临界区设计"&gt;&lt;a href="#%e4%ba%94%e5%b7%a5%e7%a8%8b%e5%a2%9e%e5%bc%bafencing-token-%e4%b8%8e%e4%b8%b4%e7%95%8c%e5%8c%ba%e8%ae%be%e8%ae%a1" class="header-anchor"&gt;&lt;/a&gt;五、工程增强：Fencing Token 与临界区设计
&lt;/h2&gt;&lt;p&gt;即使锁服务偶发双持，业务仍可把破坏降到最低：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Fencing token&lt;/strong&gt;：每次成功加锁从&lt;strong&gt;单调递增&lt;/strong&gt;源（DB 序列、强一致元数据服务等）取一个 token；写共享资源时带上该 token；资源端&lt;strong&gt;拒绝&lt;/strong&gt;小于已接受 token 的写。这样后拿到“旧锁”的客户端写不进去。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;临界区尽量短&lt;/strong&gt;：锁内只做“决策 + 提交意图”，重 IO 放到锁外并用幂等键保护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务幂等&lt;/strong&gt;：以订单号、任务 ID、版本号做天然去重，锁失败/超时可安全重试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可观测性&lt;/strong&gt;：记录加锁耗时、失败原因、续期次数、解锁脚本返回 0 的次数（token 不匹配）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="六可复现实验单实例"&gt;&lt;a href="#%e5%85%ad%e5%8f%af%e5%a4%8d%e7%8e%b0%e5%ae%9e%e9%aa%8c%e5%8d%95%e5%ae%9e%e4%be%8b" class="header-anchor"&gt;&lt;/a&gt;六、可复现实验（单实例）
&lt;/h2&gt;&lt;p&gt;下面用 &lt;code&gt;redis-cli&lt;/code&gt; 演示互斥、过期与&lt;strong&gt;误删防护&lt;/strong&gt;。假设本地 &lt;code&gt;6379&lt;/code&gt; 有 Redis。&lt;/p&gt;
&lt;h3 id="61-互斥第二个客户端抢不到"&gt;&lt;a href="#61-%e4%ba%92%e6%96%a5%e7%ac%ac%e4%ba%8c%e4%b8%aa%e5%ae%a2%e6%88%b7%e7%ab%af%e6%8a%a2%e4%b8%8d%e5%88%b0" class="header-anchor"&gt;&lt;/a&gt;6.1 互斥：第二个客户端抢不到
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 终端 A：抢锁&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli SET lock:order:1001 &lt;span class="s2"&gt;&amp;#34;token-A-111&amp;#34;&lt;/span&gt; NX PX &lt;span class="m"&gt;15000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：OK&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 终端 B：同 key 再抢&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli SET lock:order:1001 &lt;span class="s2"&gt;&amp;#34;token-B-222&amp;#34;&lt;/span&gt; NX PX &lt;span class="m"&gt;15000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：(nil) —— NX 失败&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli GET lock:order:1001
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：token-A-111&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli PTTL lock:order:1001
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：正数毫秒&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="62-安全解锁-vs-危险-del"&gt;&lt;a href="#62-%e5%ae%89%e5%85%a8%e8%a7%a3%e9%94%81-vs-%e5%8d%b1%e9%99%a9-del" class="header-anchor"&gt;&lt;/a&gt;6.2 安全解锁 vs 危险 &lt;code&gt;DEL&lt;/code&gt;
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 正确：仅 token 匹配才删&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli EVAL &lt;span class="s1"&gt;&amp;#39;if redis.call(&amp;#34;get&amp;#34;, KEYS[1]) == ARGV[1] then return redis.call(&amp;#34;del&amp;#34;, KEYS[1]) else return 0 end&amp;#39;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; lock:order:1001 token-A-111
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 再抢，模拟 B 持锁&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli SET lock:order:1001 &lt;span class="s2"&gt;&amp;#34;token-B-222&amp;#34;&lt;/span&gt; NX PX &lt;span class="m"&gt;15000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# A 拿着过期旧 token 误删：应失败&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli EVAL &lt;span class="s1"&gt;&amp;#39;if redis.call(&amp;#34;get&amp;#34;, KEYS[1]) == ARGV[1] then return redis.call(&amp;#34;del&amp;#34;, KEYS[1]) else return 0 end&amp;#39;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; lock:order:1001 token-A-111
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：0 —— 不会删掉 B 的锁&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli GET lock:order:1001
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：仍是 token-B-222&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="63-续期仅持有者可延长"&gt;&lt;a href="#63-%e7%bb%ad%e6%9c%9f%e4%bb%85%e6%8c%81%e6%9c%89%e8%80%85%e5%8f%af%e5%bb%b6%e9%95%bf" class="header-anchor"&gt;&lt;/a&gt;6.3 续期：仅持有者可延长
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli SET lock:job:9 &lt;span class="s2"&gt;&amp;#34;token-C&amp;#34;&lt;/span&gt; NX PX &lt;span class="m"&gt;5000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 错误 token 续期（应不改 TTL / 不改 value）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli EVAL &lt;span class="s1"&gt;&amp;#39;if redis.call(&amp;#34;get&amp;#34;, KEYS[1]) == ARGV[1] then return redis.call(&amp;#34;pexpire&amp;#34;, KEYS[1], ARGV[2]) else return 0 end&amp;#39;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; lock:job:9 wrong-token &lt;span class="m"&gt;20000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 正确 token 续期&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli EVAL &lt;span class="s1"&gt;&amp;#39;if redis.call(&amp;#34;get&amp;#34;, KEYS[1]) == ARGV[1] then return redis.call(&amp;#34;pexpire&amp;#34;, KEYS[1], ARGV[2]) else return 0 end&amp;#39;&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt; lock:job:9 token-C &lt;span class="m"&gt;20000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;redis-cli PTTL lock:job:9
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：接近 20000ms&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="64-伪代码骨架语言无关"&gt;&lt;a href="#64-%e4%bc%aa%e4%bb%a3%e7%a0%81%e9%aa%a8%e6%9e%b6%e8%af%ad%e8%a8%80%e6%97%a0%e5%85%b3" class="header-anchor"&gt;&lt;/a&gt;6.4 伪代码骨架（语言无关）
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;secrets&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token_hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;px&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ttl_ms&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;UNLOCK_LUA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;if redis.call(&amp;#34;get&amp;#34;, KEYS[1]) == ARGV[1] then
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; return redis.call(&amp;#34;del&amp;#34;, KEYS[1])
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; return 0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UNLOCK_LUA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;with_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl_ms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ttl_ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;lock not acquired&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 可把 token 当作 fencing 的一部分上送&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;生产中请再补：续期协程/线程、最大持锁时间、指标与超时取消。&lt;/p&gt;
&lt;h2 id="七常见坑与排查清单"&gt;&lt;a href="#%e4%b8%83%e5%b8%b8%e8%a7%81%e5%9d%91%e4%b8%8e%e6%8e%92%e6%9f%a5%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;七、常见坑与排查清单
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;现象&lt;/th&gt;
					&lt;th&gt;可能原因&lt;/th&gt;
					&lt;th&gt;排查/修复&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;锁永远解不开&lt;/td&gt;
					&lt;td&gt;只 &lt;code&gt;SETNX&lt;/code&gt; 未设过期；或 TTL 极大&lt;/td&gt;
					&lt;td&gt;统一 &lt;code&gt;SET NX PX&lt;/code&gt;；审计历史 key&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;偶发并发进入临界区&lt;/td&gt;
					&lt;td&gt;解锁用了裸 &lt;code&gt;DEL&lt;/code&gt;；或业务超时后无 fencing&lt;/td&gt;
					&lt;td&gt;Lua/&lt;code&gt;DELEX IFEQ&lt;/code&gt;；加 fencing token&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;故障切换后双写&lt;/td&gt;
					&lt;td&gt;依赖异步主从当强一致锁&lt;/td&gt;
					&lt;td&gt;评估是否可接受；否则 Redlock/外部强一致 + fencing&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;加锁抖动高&lt;/td&gt;
					&lt;td&gt;固定重试无退避；热点锁竞争&lt;/td&gt;
					&lt;td&gt;随机退避 + 缩短临界区 + 锁粒度拆分&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;续期把锁续成“永不释放”&lt;/td&gt;
					&lt;td&gt;无限 watchdog&lt;/td&gt;
					&lt;td&gt;上限次数/墙钟最大持锁时间&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;集群下 Lua 访问了未声明的 key&lt;/td&gt;
					&lt;td&gt;违反脚本 key 规则&lt;/td&gt;
					&lt;td&gt;所有 key 经 &lt;code&gt;KEYS&lt;/code&gt; 传入；遵循 Cluster 槽约束&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;以为 &lt;code&gt;GETDEL&lt;/code&gt; 能安全解锁&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;GETDEL&lt;/code&gt; 不校验 owner&lt;/td&gt;
					&lt;td&gt;必须条件删除&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="八选型建议务实版"&gt;&lt;a href="#%e5%85%ab%e9%80%89%e5%9e%8b%e5%bb%ba%e8%ae%ae%e5%8a%a1%e5%ae%9e%e7%89%88" class="header-anchor"&gt;&lt;/a&gt;八、选型建议（务实版）
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;单机房、可接受极低概率异常双持、业务幂等完善&lt;/strong&gt;：单 Redis + &lt;code&gt;SET NX PX&lt;/code&gt; + token Lua 解锁通常足够，实现简单、延迟低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对互斥更敏感、可运维多实例独立 Redis&lt;/strong&gt;：评估 Redlock 及成熟库（官方文档列出 Redisson、Redsync、node-redlock 等），并读完 Kleppmann/antirez 讨论。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;金融级强一致临界区&lt;/strong&gt;：不要只靠 Redis TTL 语义；优先考虑带 fencing 的共识/事务系统，Redis 锁最多做“快速失败的互斥提示”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任何方案&lt;/strong&gt;：临界区短、幂等、可观测、可回滚，比纠结算法名词更重要。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="九总结"&gt;&lt;a href="#%e4%b9%9d%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;九、总结
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;分布式锁的底线是 &lt;strong&gt;互斥 + 可自动释放 + 安全解锁&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;单实例正确骨架：&lt;code&gt;SET key token NX PX ttl&lt;/code&gt; + &lt;strong&gt;token 条件删除&lt;/strong&gt;（Lua 或 Redis 8.4 &lt;code&gt;DELEX IFEQ&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;裸 &lt;code&gt;DEL&lt;/code&gt;、拆分的 &lt;code&gt;SETNX&lt;/code&gt;+&lt;code&gt;EXPIRE&lt;/code&gt;、固定 value，是三大经典错误。&lt;/li&gt;
&lt;li&gt;异步主从 failover 不能保证互斥；是否可接受取决于业务与是否有 fencing。&lt;/li&gt;
&lt;li&gt;Redlock 用多数派与耗时校验提升容错，但涉及时钟漂移、持久化/延迟重启与社区争议——&lt;strong&gt;官方自己要求你读分析文章&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;工程上把锁当作&lt;strong&gt;辅助互斥&lt;/strong&gt;，用 fencing token、幂等与短临界区兜底正确性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Redis 官方文档：&lt;em&gt;Distributed Locks with Redis&lt;/em&gt;（含单实例实现、Redlock、续期、disclaimer）&lt;br&gt;
&lt;a class="link" href="https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/" target="_blank" rel="noopener"
 &gt;https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis 命令：&lt;code&gt;SET&lt;/code&gt;（&lt;code&gt;NX&lt;/code&gt; / &lt;code&gt;PX&lt;/code&gt; / &lt;code&gt;EX&lt;/code&gt; 与 Patterns 中的加解锁说明）&lt;br&gt;
&lt;a class="link" href="https://redis.io/docs/latest/commands/set/" target="_blank" rel="noopener"
 &gt;https://redis.io/docs/latest/commands/set/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis 官方文档：&lt;em&gt;Scripting with Lua&lt;/em&gt;（脚本原子执行、&lt;code&gt;EVAL&lt;/code&gt;/&lt;code&gt;KEYS&lt;/code&gt;/&lt;code&gt;ARGV&lt;/code&gt;）&lt;br&gt;
&lt;a class="link" href="https://redis.io/docs/latest/develop/programmability/eval-intro/" target="_blank" rel="noopener"
 &gt;https://redis.io/docs/latest/develop/programmability/eval-intro/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis 命令：&lt;code&gt;GETDEL&lt;/code&gt;（读删一体，但&lt;strong&gt;不&lt;/strong&gt;替代 token 校验解锁）&lt;br&gt;
&lt;a class="link" href="https://redis.io/docs/latest/commands/getdel/" target="_blank" rel="noopener"
 &gt;https://redis.io/docs/latest/commands/getdel/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis 文档源码（可检索原文）：distributed-locks markdown&lt;br&gt;
&lt;a class="link" href="https://raw.githubusercontent.com/redis/docs/main/content/develop/clients/patterns/distributed-locks.md" target="_blank" rel="noopener"
 &gt;https://raw.githubusercontent.com/redis/docs/main/content/develop/clients/patterns/distributed-locks.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Martin Kleppmann：&lt;em&gt;How to do distributed locking&lt;/em&gt;（Redlock 分析，官方文末引用）&lt;br&gt;
&lt;a class="link" href="https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html" target="_blank" rel="noopener"
 &gt;https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;antirez：&lt;em&gt;Is Redlock safe?&lt;/em&gt;（对上述分析的回应，官方文末引用）&lt;br&gt;
&lt;a class="link" href="http://antirez.com/news/101" target="_blank" rel="noopener"
 &gt;http://antirez.com/news/101&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>Go channel 与 select：阻塞语义、关闭规则与工程实践</title><link>https://blog.waihost.com/posts/go-channel-select-blocking-practice/</link><pubDate>Wed, 29 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/go-channel-select-blocking-practice/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/go-channel-select-blocking-practice.svg" alt="Featured image of post Go channel 与 select：阻塞语义、关闭规则与工程实践" /&gt;&lt;p&gt;goroutine 之间如何安全地交换数据、如何表达“没有更多结果了”、如何在多个就绪通道里做非确定性选择——这些都落在 &lt;strong&gt;channel&lt;/strong&gt; 与 &lt;strong&gt;select&lt;/strong&gt; 上。Go 语言规范把 channel 定义为并发函数之间&lt;strong&gt;按指定元素类型发送/接收&lt;/strong&gt;的通信机制；官方博客的口号则是：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Do not communicate by sharing memory; instead, share memory by communicating.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;本文依据 Go 语言规范（Channel types / Send / Receive / close / Select statements）、&lt;em&gt;Effective Go&lt;/em&gt; 的 Channels 章节、官方博客 &lt;em&gt;Share Memory By Communicating&lt;/em&gt; 与 &lt;em&gt;Go Concurrency Patterns: Pipelines and cancellation&lt;/em&gt;，把&lt;strong&gt;无缓冲/有缓冲阻塞规则、关闭语义、select 调度、管道关闭与取消&lt;/strong&gt;讲清楚，并给出可复现实验与排错清单。它和「context 取消传播」互补：context 解决“该停了”，channel/select 解决“值怎么传、何时结束、多路怎么选”。&lt;/p&gt;
&lt;h2 id="一问题背景共享内存锁-vs-通道通信"&gt;&lt;a href="#%e4%b8%80%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af%e5%85%b1%e4%ba%ab%e5%86%85%e5%ad%98%e9%94%81-vs-%e9%80%9a%e9%81%93%e9%80%9a%e4%bf%a1" class="header-anchor"&gt;&lt;/a&gt;一、问题背景：共享内存锁 vs 通道通信
&lt;/h2&gt;&lt;p&gt;传统多线程模型里，线程通过共享数据结构 + 锁协作：谁拿到锁谁改状态。Go 的 CSP 风格把协作点显式化——&lt;strong&gt;发送/接收本身就是同步点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;常见踩坑往往不是“不会写 &lt;code&gt;ch &amp;lt;- v&lt;/code&gt;”，而是对阻塞条件判断错误：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;无缓冲通道上“先发后收”在单 goroutine 里会&lt;strong&gt;永久阻塞&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;向已关闭通道发送触发 &lt;strong&gt;runtime panic&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;nil&lt;/code&gt; 通道发送/接收会&lt;strong&gt;永远阻塞&lt;/strong&gt;，&lt;code&gt;select&lt;/code&gt; 里一不小心就“整段卡死”；&lt;/li&gt;
&lt;li&gt;只 &lt;code&gt;close&lt;/code&gt; 不约定所有权，多发送者并发 close 直接 panic；&lt;/li&gt;
&lt;li&gt;管道中游生产者退出却不 close，下游 &lt;code&gt;range&lt;/code&gt; 永不结束。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;把规范里的规则记成表，比背 API 更有用。&lt;/p&gt;
&lt;h2 id="二核心模型无缓冲握手与有缓冲队列"&gt;&lt;a href="#%e4%ba%8c%e6%a0%b8%e5%bf%83%e6%a8%a1%e5%9e%8b%e6%97%a0%e7%bc%93%e5%86%b2%e6%8f%a1%e6%89%8b%e4%b8%8e%e6%9c%89%e7%bc%93%e5%86%b2%e9%98%9f%e5%88%97" class="header-anchor"&gt;&lt;/a&gt;二、核心模型：无缓冲握手与有缓冲队列
&lt;/h2&gt;&lt;h3 id="1-创建与零值"&gt;&lt;a href="#1-%e5%88%9b%e5%bb%ba%e4%b8%8e%e9%9b%b6%e5%80%bc" class="header-anchor"&gt;&lt;/a&gt;1. 创建与零值
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// nil channel，零值&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 无缓冲（容量 0）&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ch2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 同样无缓冲&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;buf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 缓冲容量 8&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;规范要点：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;概念&lt;/th&gt;
					&lt;th&gt;规范语义&lt;/th&gt;
					&lt;th&gt;工程含义&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;元素类型&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;chan T&lt;/code&gt; / 方向 &lt;code&gt;chan&amp;lt;- T&lt;/code&gt;、&lt;code&gt;&amp;lt;-chan T&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;编译期限制发送/接收方向&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;make 容量&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;可选 capacity；0 或省略 → &lt;strong&gt;unbuffered&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;无缓冲 = 同步通信&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;缓冲通道&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;缓冲未满可发送、非空可接收且不阻塞&lt;/td&gt;
					&lt;td&gt;解耦生产/消费速率&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;零值 nil&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;未初始化的 channel 为 nil&lt;/td&gt;
					&lt;td&gt;对 nil 收发会永久阻塞&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;FIFO&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;通道是先进先出队列&lt;/td&gt;
					&lt;td&gt;多 goroutine 共用同一通道无需额外同步结构（仍要注意关闭约定）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;无缓冲通道：&lt;strong&gt;只有发送方与接收方都就绪时通信才成功&lt;/strong&gt;——值直接从发送方拷到接收方，同时完成同步。有缓冲通道：发送在缓冲有空位时即可完成；接收在缓冲有数据时即可完成。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Effective Go&lt;/em&gt; 的表述更偏工程直觉：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接收方&lt;strong&gt;总是&lt;/strong&gt;阻塞到有数据可收；&lt;/li&gt;
&lt;li&gt;无缓冲时，发送方阻塞到接收方取走值；&lt;/li&gt;
&lt;li&gt;有缓冲时，发送方阻塞到值被拷入缓冲；缓冲满则等到有接收方腾出空位。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-发送--接收何时可推进"&gt;&lt;a href="#2-%e5%8f%91%e9%80%81--%e6%8e%a5%e6%94%b6%e4%bd%95%e6%97%b6%e5%8f%af%e6%8e%a8%e8%bf%9b" class="header-anchor"&gt;&lt;/a&gt;2. 发送 / 接收何时可推进
&lt;/h3&gt;&lt;p&gt;规范对 send 的可推进条件可以压缩成：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;操作&lt;/th&gt;
					&lt;th&gt;可立即推进的条件&lt;/th&gt;
					&lt;th&gt;否则&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;无缓冲发送&lt;/td&gt;
					&lt;td&gt;已有接收方就绪&lt;/td&gt;
					&lt;td&gt;阻塞&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;有缓冲发送&lt;/td&gt;
					&lt;td&gt;缓冲未满&lt;/td&gt;
					&lt;td&gt;阻塞&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;向 &lt;strong&gt;closed&lt;/strong&gt; 发送&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;runtime panic&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;向 &lt;strong&gt;nil&lt;/strong&gt; 发送&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;永久阻塞&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;接收（开通道）&lt;/td&gt;
					&lt;td&gt;有数据或有发送方（无缓冲握手）&lt;/td&gt;
					&lt;td&gt;阻塞&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;从 &lt;strong&gt;closed&lt;/strong&gt; 接收&lt;/td&gt;
					&lt;td&gt;总是可立即推进&lt;/td&gt;
					&lt;td&gt;先排空缓冲，再得到零值&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;从 &lt;strong&gt;nil&lt;/strong&gt; 接收&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;永久阻塞&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 无缓冲：必须另一端参与&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ready&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 有缓冲：容量内发送不阻塞&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// b &amp;lt;- 3 // 若无接收方，此处阻塞&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-关闭ok-与-range"&gt;&lt;a href="#3-%e5%85%b3%e9%97%adok-%e4%b8%8e-range" class="header-anchor"&gt;&lt;/a&gt;3. 关闭、ok 与 range
&lt;/h3&gt;&lt;p&gt;内建 &lt;code&gt;close(ch)&lt;/code&gt; 表示“不会再有新值”。规范与管道博客共同强调的接收语义：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;关闭后，&lt;strong&gt;先前已发送、仍在缓冲中的值会被正常接收完&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;排空后，接收&lt;strong&gt;立即返回元素类型零值&lt;/strong&gt;且不阻塞；&lt;/li&gt;
&lt;li&gt;双返回值形式 &lt;code&gt;v, ok := &amp;lt;-ch&lt;/code&gt;：&lt;code&gt;ok == false&lt;/code&gt; 表示通道已关闭且没有更多值；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;close(nil)&lt;/code&gt;、&lt;code&gt;close&lt;/code&gt; 已关闭通道 → &lt;strong&gt;runtime panic&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;向已关闭通道发送 → panic&lt;/strong&gt;（这是最常见的线上事故之一）。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 自动在关闭且排空后结束&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 0 false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;关闭约定（工程硬规则）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;发送侧关闭&lt;/strong&gt;：通常由唯一“拥有发送权”的一方 close；多个发送者需要额外同步（例如用 &lt;code&gt;sync.Once&lt;/code&gt; 或先汇合再 close）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接收侧不要 close&lt;/strong&gt;（除非协议明确且全局只有你发送——极少见）。&lt;/li&gt;
&lt;li&gt;需要“广播停工”时，更常见的是 &lt;strong&gt;close 一个只读 done 信号通道&lt;/strong&gt;（见下文管道取消），而不是让每个工作通道被多方乱关。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="三select多路就绪与非确定性选择"&gt;&lt;a href="#%e4%b8%89select%e5%a4%9a%e8%b7%af%e5%b0%b1%e7%bb%aa%e4%b8%8e%e9%9d%9e%e7%a1%ae%e5%ae%9a%e6%80%a7%e9%80%89%e6%8b%a9" class="header-anchor"&gt;&lt;/a&gt;三、select：多路就绪与非确定性选择
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;select&lt;/code&gt; 像 switch，但 case 全部是&lt;strong&gt;通信操作&lt;/strong&gt;。规范给出的执行步骤值得逐条对照代码：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入 select 时，&lt;strong&gt;按源码顺序&lt;/strong&gt;对所有 case 的通道操作数（以及发送右侧表达式）&lt;strong&gt;各求值恰好一次&lt;/strong&gt;——副作用一定会发生，与最终选中哪个 case 无关；&lt;/li&gt;
&lt;li&gt;若&lt;strong&gt;一个或多个&lt;/strong&gt;通信可推进，用&lt;strong&gt;均匀伪随机&lt;/strong&gt;选中其中一个；&lt;/li&gt;
&lt;li&gt;若都不能推进且存在 &lt;code&gt;default&lt;/code&gt;，走 default；&lt;/li&gt;
&lt;li&gt;若都不能推进且无 default，&lt;strong&gt;阻塞&lt;/strong&gt;直到至少一个可推进；&lt;/li&gt;
&lt;li&gt;仅有 &lt;strong&gt;nil 通道&lt;/strong&gt; 且无 default 的 select &lt;strong&gt;永久阻塞&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handleA&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 发送成功&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 超时分支（timer channel）&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 非阻塞探测：此刻无人可通信&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="工程模式"&gt;&lt;a href="#%e5%b7%a5%e7%a8%8b%e6%a8%a1%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;工程模式
&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;1）非阻塞试探（try-send / try-recv）&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 队列满或无接收方：降级、丢弃或写指标&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;em&gt;Effective Go&lt;/em&gt; 用 free-list + &lt;code&gt;select&lt;/code&gt;/&lt;code&gt;default&lt;/code&gt; 演示：缓冲通道兼作资源池，满则新建。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2）有缓冲通道当信号量&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;sem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MaxOutstanding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;sem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}{}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 占坑；满则阻塞&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;sem&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 释放&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;容量限制同时在飞的 &lt;code&gt;process&lt;/code&gt; 数。注意：若外层对每个请求都 &lt;code&gt;go handle&lt;/code&gt; 而不做接纳控制，仍可能制造海量 goroutine——需要在入口再限流。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3）与 context 组合：可取消等待&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;resultCh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;channel 负责传结果，context 负责传“该停了”。二者不要互相替代。&lt;/p&gt;
&lt;h2 id="四可复现实验阻塞关闭与-select"&gt;&lt;a href="#%e5%9b%9b%e5%8f%af%e5%a4%8d%e7%8e%b0%e5%ae%9e%e9%aa%8c%e9%98%bb%e5%a1%9e%e5%85%b3%e9%97%ad%e4%b8%8e-select" class="header-anchor"&gt;&lt;/a&gt;四、可复现实验：阻塞、关闭与 select
&lt;/h2&gt;&lt;p&gt;下面用标准库即可在本机验证规范语义（Go 1.20+ 均可）。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;time&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;mustPanic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;recover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;panic OK:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ERROR: expected panic&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 1) 无缓冲：单 goroutine 发送会死锁（用超时模拟探测）&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;unexpected send success&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;unbuffered send blocked as expected&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 2) 有缓冲：容量内不阻塞&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;buffered len/cap:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 3) 关闭后接收&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;drain closed:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 42 true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;after empty:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 0 false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 4) 向关闭通道发送 → panic&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;mustPanic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;send-on-closed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 5) close(nil) → panic&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;mustPanic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;close-nil&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 6) select 伪随机：两个都就绪时多次运行会看到不同分支&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 重新填满，保证两 case 均可推进&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;a1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;a1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;a2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;a2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;a2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;select distribution:&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 7) 仅 nil + 无 default：用超时包裹，避免真死锁进程&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;nilCh&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;nilCh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;unreachable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;nil-only select would block forever without timeout&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;预期观察：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无缓冲在无人接收时发送走超时分支；&lt;/li&gt;
&lt;li&gt;关闭通道先拿到缓冲残留，再 &lt;code&gt;ok=false&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;向关闭通道发送、&lt;code&gt;close(nil)&lt;/code&gt; 触发 panic；&lt;/li&gt;
&lt;li&gt;双就绪 select 的计数大致接近（伪随机，不保证 50/50 精确）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="五管道关闭传播与-done-取消"&gt;&lt;a href="#%e4%ba%94%e7%ae%a1%e9%81%93%e5%85%b3%e9%97%ad%e4%bc%a0%e6%92%ad%e4%b8%8e-done-%e5%8f%96%e6%b6%88" class="header-anchor"&gt;&lt;/a&gt;五、管道：关闭传播与 done 取消
&lt;/h2&gt;&lt;p&gt;官方 &lt;em&gt;Pipelines&lt;/em&gt; 博客的模型：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;A pipeline is a series of stages connected by channels, where each stage is a group of goroutines running the same function.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;每阶段从上游 channel 读、处理后写入下游 channel。结束信号有两种常见方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;关闭数据通道&lt;/strong&gt;：表示“没有更多输入”。因&lt;strong&gt;对已关闭通道的接收总可立即完成&lt;/strong&gt;，下游 &lt;code&gt;range&lt;/code&gt; 能自然退出；发送方在发完后 &lt;code&gt;close(out)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显式取消（done channel）&lt;/strong&gt;：当下游提前退出、不再消费时，上游若继续发送会阻塞。博客做法是额外传入 &lt;code&gt;done &amp;lt;-chan struct{}&lt;/code&gt;，由下游（或 &lt;code&gt;main&lt;/code&gt;）&lt;strong&gt;close(done)&lt;/strong&gt; 做广播，上游在 &lt;code&gt;select&lt;/code&gt; 里同时监听 &lt;code&gt;done&lt;/code&gt; 与数据发送，从而放弃未完成发送。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;gen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;nums&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;nums&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// main 中：&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 退出时广播取消&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;sq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;gen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 提前结束也安全：done 关闭后上游不再堵在发送上&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这与 &lt;code&gt;context&lt;/code&gt; 的取消树是同一思想的两种载体：&lt;code&gt;done&lt;/code&gt; 通道是轻量广播；复杂调用链更推荐 &lt;code&gt;context.Context&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="六常见坑与排查清单"&gt;&lt;a href="#%e5%85%ad%e5%b8%b8%e8%a7%81%e5%9d%91%e4%b8%8e%e6%8e%92%e6%9f%a5%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;六、常见坑与排查清单
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;症状&lt;/th&gt;
					&lt;th&gt;可能根因&lt;/th&gt;
					&lt;th&gt;排查 / 修复&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;fatal error: all goroutines are asleep - deadlock!&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;无缓冲自收自发；所有 goroutine 互相等待&lt;/td&gt;
					&lt;td&gt;用 &lt;code&gt;go&lt;/code&gt; 起对端；检查是否缺接收/发送&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;panic: send on closed channel&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;接收方误 close，或多发送者重复 close&lt;/td&gt;
					&lt;td&gt;单一发送所有者 close；多生产者用 WaitGroup 汇合后再 close&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;panic: close of closed channel&lt;/code&gt; / &lt;code&gt;close of nil channel&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;重复 close 或 close 零值&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;sync.Once&lt;/code&gt;；初始化后再 close&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;goroutine 数量持续上涨&lt;/td&gt;
					&lt;td&gt;发送阻塞无人收；&lt;code&gt;go handle&lt;/code&gt; 无限接纳&lt;/td&gt;
					&lt;td&gt;有缓冲限流 + context 取消；pprof &lt;code&gt;goroutine&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;range&lt;/code&gt; 永不结束&lt;/td&gt;
					&lt;td&gt;上游未 close&lt;/td&gt;
					&lt;td&gt;契约：最后一个发送者负责 close&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;select “总是”走同一分支&lt;/td&gt;
					&lt;td&gt;另一 case 实际不可推进（nil/满/空）&lt;/td&gt;
					&lt;td&gt;打印 &lt;code&gt;len/cap&lt;/code&gt;、是否 nil、是否已 close&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;定时器泄漏&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;time.After&lt;/code&gt; 在热路径大量创建&lt;/td&gt;
					&lt;td&gt;复用 &lt;code&gt;time.NewTimer&lt;/code&gt; 并 &lt;code&gt;Stop&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据竞争仍在&lt;/td&gt;
					&lt;td&gt;channel 传的是指针/切片头，共享底层数组&lt;/td&gt;
					&lt;td&gt;发送所有权转移；或拷贝；或明确只读契约&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;补充 runtime 视角（&lt;code&gt;runtime/chan.go&lt;/code&gt; 注释）：缓冲通道上，缓冲非空时接收队列为空、缓冲未满时发送队列为空——实现上把“数据在缓冲”与“对端在等待队列”分成清晰不变量。排障时你不需要背这些字段，但要理解：&lt;strong&gt;阻塞 = 进入 sendq/recvq 等待被对端或 close 唤醒&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="七总结"&gt;&lt;a href="#%e4%b8%83%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;七、总结
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;无缓冲 channel = 通信 + 同步&lt;/strong&gt;；有缓冲 = 带容量的 FIFO，用于削峰与信号量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关闭是“不再发送”的信号&lt;/strong&gt;，不是“销毁”；排空后接收零值；&lt;strong&gt;禁止向已关闭通道发送&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nil 通道收发永久阻塞&lt;/strong&gt;；&lt;code&gt;select&lt;/code&gt; 求值有副作用，多就绪 case &lt;strong&gt;伪随机&lt;/strong&gt;，无就绪且无 default 则阻塞。&lt;/li&gt;
&lt;li&gt;管道靠 &lt;strong&gt;close 数据通道&lt;/strong&gt; 表达结束，靠 &lt;strong&gt;done/context&lt;/strong&gt; 表达提前取消，避免上游堵死。&lt;/li&gt;
&lt;li&gt;与上一篇 &lt;em&gt;Go context 取消传播&lt;/em&gt; 搭配：context 管生命周期，channel/select 管数据面与多路选择。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;把“谁发送、谁关闭、谁取消、缓冲多大”四件事写进代码评审清单，比事后查 deadlock 日志便宜得多。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;a class="link" href="https://go.dev/ref/spec" target="_blank" rel="noopener"
 &gt;The Go Programming Language Specification — Channel types / Send statements / Receive operator / Close / Select statements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://go.dev/doc/effective_go#channels" target="_blank" rel="noopener"
 &gt;Effective Go — Channels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://go.dev/blog/share-memory-by-communicating" target="_blank" rel="noopener"
 &gt;Share Memory By Communicating (The Go Blog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://go.dev/blog/pipelines" target="_blank" rel="noopener"
 &gt;Go Concurrency Patterns: Pipelines and cancellation (The Go Blog)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/golang/go/blob/master/src/runtime/chan.go" target="_blank" rel="noopener"
 &gt;Go runtime channel implementation (&lt;code&gt;src/runtime/chan.go&lt;/code&gt;)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;本站相关：&lt;a class="link" href="https://blog.waihost.com/posts/go-context-cancellation-timeout-practice/" &gt;Go context 取消传播与超时控制：原理、API 与工程实践&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>AgentScope Java 2.0 实战：构建带权限审批与可恢复执行的生产级 Agent</title><link>https://blog.waihost.com/posts/agentscope-java-production-agent/</link><pubDate>Thu, 23 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/agentscope-java-production-agent/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/agentscope-java-production-agent.svg" alt="Featured image of post AgentScope Java 2.0 实战：构建带权限审批与可恢复执行的生产级 Agent" /&gt;&lt;p&gt;能调用 Shell、修改文件和派发子任务的 Agent，比普通聊天机器人更有价值，也更危险。Prompt 中一句“执行危险操作前请确认”不是安全边界：模型可能误判上下文、被工具输出诱导，或者在重试中重复执行副作用。&lt;/p&gt;
&lt;p&gt;AgentScope Java 2.0 把这类生产问题放进框架核心：每次工具调用都经过权限系统，结果是 &lt;code&gt;ALLOW&lt;/code&gt;、&lt;code&gt;DENY&lt;/code&gt; 或 &lt;code&gt;ASK&lt;/code&gt;；&lt;code&gt;ASK&lt;/code&gt; 会暂停 Agent，等待用户确认后再恢复。同时，Harness、Workspace、Sandbox 和分布式状态后端用来承载长时间任务。&lt;/p&gt;
&lt;p&gt;本文基于 AgentScope Java &lt;code&gt;2.0.0 GA&lt;/code&gt; 和 JDK 17，先搭建一个 HarnessAgent，再实现“读操作自动放行、写操作必须审批、生产资源不可绕过”的权限策略，最后说明 session 恢复与沙箱的边界。&lt;/p&gt;
&lt;h2 id="一reactagent-与-harnessagent-的分工"&gt;&lt;a href="#%e4%b8%80reactagent-%e4%b8%8e-harnessagent-%e7%9a%84%e5%88%86%e5%b7%a5" class="header-anchor"&gt;&lt;/a&gt;一、ReActAgent 与 HarnessAgent 的分工
&lt;/h2&gt;&lt;p&gt;AgentScope Java 2.0 提供两层抽象：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;抽象&lt;/th&gt;
					&lt;th&gt;适合场景&lt;/th&gt;
					&lt;th&gt;核心关注点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;ReActAgent&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;轻量聊天、受限 Tool Calling&lt;/td&gt;
					&lt;td&gt;推理—行动循环、模型、工具&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;HarnessAgent&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;长任务、文件/代码 Agent、企业部署&lt;/td&gt;
					&lt;td&gt;Workspace、Memory、Skills、Subagent、Sandbox、恢复&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果只需调用一个天气 API，&lt;code&gt;ReActAgent&lt;/code&gt; 已经足够。若 Agent 要在目录里读写文件、执行命令、跨会话继续任务，应该从 Harness 开始，因为这些能力需要共享一致的运行上下文。&lt;/p&gt;
&lt;p&gt;官方 2.0 的设计还强调 Agent 本身无状态：运行状态通过 &lt;code&gt;(userId, sessionId)&lt;/code&gt; 和后端管理。这比把可变消息列表放进单例 Agent 更适合多实例部署。&lt;/p&gt;
&lt;h2 id="二依赖和模型配置"&gt;&lt;a href="#%e4%ba%8c%e4%be%9d%e8%b5%96%e5%92%8c%e6%a8%a1%e5%9e%8b%e9%85%8d%e7%bd%ae" class="header-anchor"&gt;&lt;/a&gt;二、依赖和模型配置
&lt;/h2&gt;&lt;p&gt;AgentScope Java 2.0 要求 JDK 17+。Harness 与模型提供商从 2.0 起拆分为独立模块：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;maven.compiler.release&amp;gt;&lt;/span&gt;17&lt;span class="nt"&gt;&amp;lt;/maven.compiler.release&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;agentscope.version&amp;gt;&lt;/span&gt;2.0.0&lt;span class="nt"&gt;&amp;lt;/agentscope.version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.agentscope&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;agentscope-harness&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${agentscope.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.agentscope&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;agentscope-extensions-model-dashscope&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${agentscope.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其他官方可选模型模块包括 OpenAI、Anthropic、Gemini 和 Ollama。使用 DashScope 时，把密钥放进环境变量而不是源码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;DASHSCOPE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;你的密钥&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后创建最小 HarnessAgent：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;io.agentscope.core.agent.RuntimeContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;io.agentscope.core.message.UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;io.agentscope.harness.agent.HarnessAgent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.nio.file.Paths&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OpsAgentApp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HarnessAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;HarnessAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ops_assistant&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sysPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 你是运维分析助手。先读后写，输出每一步依据；
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 未获得权限系统批准时，不得声称已经修改任何资源。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;dashscope:qwen-plus&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;.agentscope/workspace&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RuntimeContext&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RuntimeContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;alice&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;incident-20260724-001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;检查工作区里的故障日志并给出处理建议&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTextContent&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;模型字符串由 ModelRegistry 解析。&lt;code&gt;workspace&lt;/code&gt; 不是随便给 Agent 一个宿主机根目录，而应该是专门分配、权限收敛的工作目录。&lt;/p&gt;
&lt;h2 id="三权限系统如何做决策"&gt;&lt;a href="#%e4%b8%89%e6%9d%83%e9%99%90%e7%b3%bb%e7%bb%9f%e5%a6%82%e4%bd%95%e5%81%9a%e5%86%b3%e7%ad%96" class="header-anchor"&gt;&lt;/a&gt;三、权限系统如何做决策
&lt;/h2&gt;&lt;p&gt;权限系统位于 &lt;code&gt;io.agentscope.core.permission&lt;/code&gt;，拦截每次工具调用。它综合三类信息：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Rules&lt;/strong&gt;：针对工具和调用模式的显式 ALLOW、ASK、DENY；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mode&lt;/strong&gt;：未命中规则时的默认策略；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in Checks&lt;/strong&gt;：工具根据真实入参执行的不可绕过检查。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;决策顺序中，DENY 规则和危险路径检查优先。即使启用 &lt;code&gt;BYPASS&lt;/code&gt;，这类保护仍然生效。&lt;/p&gt;
&lt;p&gt;官方提供五种常用模式：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;模式&lt;/th&gt;
					&lt;th&gt;未命中规则时的行为&lt;/th&gt;
					&lt;th&gt;适用情况&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;DEFAULT&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;要求显式规则或用户确认&lt;/td&gt;
					&lt;td&gt;推荐默认值&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;ACCEPT_EDITS&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;放行工作目录内安全文件操作&lt;/td&gt;
					&lt;td&gt;用户在场的开发任务&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;EXPLORE&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;只读，拒绝写与命令&lt;/td&gt;
					&lt;td&gt;代码探索、规划&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;BYPASS&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;通常放行，但不可绕过的检查仍有效&lt;/td&gt;
					&lt;td&gt;完全可信且隔离的 Sandbox&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;DONT_ASK&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;将 ASK 变为 DENY&lt;/td&gt;
					&lt;td&gt;无人值守任务&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;DONT_ASK&lt;/code&gt; 尤其重要：定时任务没有用户在线，不能让 Agent 永久等待审批，也不应默认放行。&lt;/p&gt;
&lt;h2 id="四配置-allowask-与-deny-规则"&gt;&lt;a href="#%e5%9b%9b%e9%85%8d%e7%bd%ae-allowask-%e4%b8%8e-deny-%e8%a7%84%e5%88%99" class="header-anchor"&gt;&lt;/a&gt;四、配置 ALLOW、ASK 与 DENY 规则
&lt;/h2&gt;&lt;p&gt;下面的策略允许安全读取，删除操作必须询问，删表始终拒绝：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;io.agentscope.core.permission.PermissionBehavior&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;io.agentscope.core.permission.PermissionContextState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;io.agentscope.core.permission.PermissionMode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;io.agentscope.core.permission.PermissionRule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;PermissionContextState&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;permissions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PermissionContextState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PermissionMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DEFAULT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAllowRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;safe_read&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PermissionRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;safe_read&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PermissionBehavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ALLOW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;projectSettings&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addAskRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;dangerous_delete&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PermissionRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;dangerous_delete&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PermissionBehavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ASK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;projectSettings&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;addDenyRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;drop_table&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PermissionRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;drop_table&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PermissionBehavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DENY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;projectSettings&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以把权限上下文交给底层 &lt;code&gt;ReActAgent&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ReActAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ReActAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;controlled_agent&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sysPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;先检查，再操作&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;permissionContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;若使用 Harness，应在构建 Harness 的 delegate/permission 配置位置应用同一上下文；具体 Builder 入口以 2.0.0 文档和当前模块为准，不要从 RC 版博客复制包名。&lt;/p&gt;
&lt;p&gt;规则来源 &lt;code&gt;projectSettings&lt;/code&gt;、&lt;code&gt;userSettings&lt;/code&gt;、&lt;code&gt;session&lt;/code&gt; 或 &lt;code&gt;suggested&lt;/code&gt; 应进入审计日志。由用户在一次 ASK 中接受的 suggested rule，可能影响后续相同调用，UI 必须明确告诉用户这是“一次允许”还是“以后允许”。&lt;/p&gt;
&lt;h2 id="五自定义不可绕过的工具检查"&gt;&lt;a href="#%e4%ba%94%e8%87%aa%e5%ae%9a%e4%b9%89%e4%b8%8d%e5%8f%af%e7%bb%95%e8%bf%87%e7%9a%84%e5%b7%a5%e5%85%b7%e6%a3%80%e6%9f%a5" class="header-anchor"&gt;&lt;/a&gt;五、自定义不可绕过的工具检查
&lt;/h2&gt;&lt;p&gt;静态规则不一定能识别业务语义。比如同一个 &lt;code&gt;restart_service&lt;/code&gt; 工具，测试环境可以自动执行，生产环境必须审批。自定义 Tool 可覆盖 &lt;code&gt;checkPermissions&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;io.agentscope.core.permission.PermissionDecision&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;io.agentscope.core.tool.ToolBase&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;io.agentscope.core.tool.ToolExecutionContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.util.Map&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;reactor.core.publisher.Mono&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RestartServiceTool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;extends&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ToolBase&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;RestartServiceTool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ToolBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;restart_service&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;重启指定环境中的服务&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readOnly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mono&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PermissionDecision&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;checkPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ToolExecutionContext&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;environment&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;prod&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mono&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;just&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PermissionDecision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;目标是生产环境，需要人工批准：&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Mono&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;just&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PermissionDecision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;passthrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;非生产环境，继续按 rules/mode 判断&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;PASSTHROUGH&lt;/code&gt; 不是 ALLOW，它表示把决定权交回规则和模式。对明确禁止的目标可直接返回 &lt;code&gt;deny(...)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;框架还内置危险路径保护，例如 &lt;code&gt;.ssh/&lt;/code&gt;、&lt;code&gt;.git/&lt;/code&gt;、&lt;code&gt;.aws/&lt;/code&gt;、&lt;code&gt;.kube/&lt;/code&gt;、&lt;code&gt;.env&lt;/code&gt; 和凭据文件。命中危险路径时会强制 ASK；自定义 Tool 也可以增加危险文件和目录。&lt;/p&gt;
&lt;h2 id="六ask-后怎样暂停与恢复"&gt;&lt;a href="#%e5%85%adask-%e5%90%8e%e6%80%8e%e6%a0%b7%e6%9a%82%e5%81%9c%e4%b8%8e%e6%81%a2%e5%a4%8d" class="header-anchor"&gt;&lt;/a&gt;六、ASK 后怎样暂停与恢复
&lt;/h2&gt;&lt;p&gt;权限结果为 ASK 时，Agent 不执行工具，而是以 &lt;code&gt;GenerateReason.PERMISSION_ASKING&lt;/code&gt; 暂停。返回消息中包含状态为 &lt;code&gt;ASKING&lt;/code&gt; 的 &lt;code&gt;ToolUseBlock&lt;/code&gt;，调用方应：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把工具名和脱敏后的参数展示给用户；&lt;/li&gt;
&lt;li&gt;让用户选择拒绝、仅本次允许或接受建议规则；&lt;/li&gt;
&lt;li&gt;构造 &lt;code&gt;ConfirmResult&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;使用相同 &lt;code&gt;(userId, sessionId)&lt;/code&gt; 恢复 Agent。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;核心确认对象如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ConfirmResult&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;confirm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConfirmResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// confirmed&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 本次待确认调用&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSuggestedRules&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 仅在用户明确接受时传入&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;生产 UI 不应只放一个模糊的“确认”按钮。至少显示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工具名称与风险级别；&lt;/li&gt;
&lt;li&gt;目标环境、资源 ID、路径；&lt;/li&gt;
&lt;li&gt;是否产生副作用；&lt;/li&gt;
&lt;li&gt;建议规则会影响一次还是后续调用；&lt;/li&gt;
&lt;li&gt;session、用户和过期时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;确认结果必须由服务端绑定当前用户和待审批记录，不能接受客户端随意提交另一个 &lt;code&gt;toolCall&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="七流式事件让执行过程可观察"&gt;&lt;a href="#%e4%b8%83%e6%b5%81%e5%bc%8f%e4%ba%8b%e4%bb%b6%e8%ae%a9%e6%89%a7%e8%a1%8c%e8%bf%87%e7%a8%8b%e5%8f%af%e8%a7%82%e5%af%9f" class="header-anchor"&gt;&lt;/a&gt;七、流式事件让执行过程可观察
&lt;/h2&gt;&lt;p&gt;Harness 可以输出统一事件流：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;streamEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;分析日志；如果需要修改配置先请求批准&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;doOnNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getType&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TEXT_BLOCK_DELTA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="n"&gt;TextBlockDeltaEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getDelta&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TOOL_CALL_START&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;[tool] &amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;ToolCallStartEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="na"&gt;getToolCallName&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;blockLast&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;事件流可驱动 Web UI、飞书/钉钉 Channel 或审计平台。注意不要把所有原始参数直接写日志；命令、Prompt 和工具结果可能包含 Token、路径或客户数据。&lt;/p&gt;
&lt;h2 id="八workspacesandbox-和权限各管一层"&gt;&lt;a href="#%e5%85%abworkspacesandbox-%e5%92%8c%e6%9d%83%e9%99%90%e5%90%84%e7%ae%a1%e4%b8%80%e5%b1%82" class="header-anchor"&gt;&lt;/a&gt;八、Workspace、Sandbox 和权限各管一层
&lt;/h2&gt;&lt;p&gt;三者不能互相替代：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Workspace&lt;/strong&gt; 组织文件、Memory、Skills 和子 Agent 工作上下文；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permission&lt;/strong&gt; 决定某次工具调用是否允许、拒绝或询问；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sandbox&lt;/strong&gt; 限制已经获准的代码实际能访问哪些系统资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即使权限系统批准 &lt;code&gt;rm temp.txt&lt;/code&gt;，Sandbox 仍应阻止进程读取宿主机 SSH 密钥。反过来，Docker 隔离也不能代替业务审批：容器里的工具可能仍持有生产 API 凭据。&lt;/p&gt;
&lt;p&gt;可按风险逐级部署：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;开发：专用本地目录 + DEFAULT
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;测试：Docker Sandbox + ACCEPT_EDITS
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;生产交互：Kubernetes Sandbox + DEFAULT + HITL
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;生产无人值守：最小工具集 + DONT_ASK + 服务端白名单
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;每个 Sandbox 还应配置 CPU、内存、进程数、超时、只读根文件系统和网络出口策略。&lt;/p&gt;
&lt;h2 id="九可恢复执行的正确边界"&gt;&lt;a href="#%e4%b9%9d%e5%8f%af%e6%81%a2%e5%a4%8d%e6%89%a7%e8%a1%8c%e7%9a%84%e6%ad%a3%e7%a1%ae%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;九、可恢复执行的正确边界
&lt;/h2&gt;&lt;p&gt;AgentScope Java 2.0 支持 Redis、MySQL、PostgreSQL、OSS/COS 等分布式 session 和 memory 后端，并面向跨副本 session 恢复。但“恢复 Agent 状态”仍不等于“业务操作恰好一次”。&lt;/p&gt;
&lt;p&gt;经典故障窗口：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;工具成功重启服务
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; 进程在保存工具结果前崩溃
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; 恢复后模型再次请求重启
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;解决方案在工具层：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OperationRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;operationId&lt;/code&gt; 由可信服务端生成；&lt;/li&gt;
&lt;li&gt;业务库对它建立唯一约束；&lt;/li&gt;
&lt;li&gt;重复请求返回第一次结果，而不是再次执行；&lt;/li&gt;
&lt;li&gt;checkpoint 记录 operationId 与状态；&lt;/li&gt;
&lt;li&gt;外部系统也尽可能使用幂等键。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;userId + sessionId&lt;/code&gt; 用于隔离会话，&lt;code&gt;operationId&lt;/code&gt; 用于隔离副作用，二者不要混为一谈。&lt;/p&gt;
&lt;h2 id="十生产部署清单"&gt;&lt;a href="#%e5%8d%81%e7%94%9f%e4%ba%a7%e9%83%a8%e7%bd%b2%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;十、生产部署清单
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 默认使用 &lt;code&gt;DEFAULT&lt;/code&gt; 或 &lt;code&gt;EXPLORE&lt;/code&gt;，不把 &lt;code&gt;BYPASS&lt;/code&gt; 当省事开关；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 无人值守任务使用 &lt;code&gt;DONT_ASK&lt;/code&gt;，未明确允许的调用直接拒绝；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 生产、凭据和危险路径在 Tool 内做不可绕过检查；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; ASK 绑定用户、session、调用参数摘要和过期时间；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Suggested Rule 的持久化范围对用户透明；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Sandbox 有资源限制、网络出口策略和短期凭据；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 所有写工具都有幂等键与审计记录；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 使用共享状态后端，并测试滚动发布和跨副本恢复；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 对每种工具做 ALLOW、ASK、DENY 和恢复故障测试；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 日志和事件流经过脱敏。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="十一常见问题"&gt;&lt;a href="#%e5%8d%81%e4%b8%80%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98" class="header-anchor"&gt;&lt;/a&gt;十一、常见问题
&lt;/h2&gt;&lt;h3 id="agent-一直要求确认"&gt;&lt;a href="#agent-%e4%b8%80%e7%9b%b4%e8%a6%81%e6%b1%82%e7%a1%ae%e8%ae%a4" class="header-anchor"&gt;&lt;/a&gt;Agent 一直要求确认
&lt;/h3&gt;&lt;p&gt;检查规则的 &lt;code&gt;toolName&lt;/code&gt; 和 &lt;code&gt;ruleContent&lt;/code&gt; 是否真的匹配调用；若默认 &lt;code&gt;DEFAULT&lt;/code&gt; 且没有 ALLOW 规则，询问是预期行为。不要直接切到 &lt;code&gt;BYPASS&lt;/code&gt;，先补齐最小权限规则。&lt;/p&gt;
&lt;h3 id="定时任务卡在-ask"&gt;&lt;a href="#%e5%ae%9a%e6%97%b6%e4%bb%bb%e5%8a%a1%e5%8d%a1%e5%9c%a8-ask" class="header-anchor"&gt;&lt;/a&gt;定时任务卡在 ASK
&lt;/h3&gt;&lt;p&gt;无人值守场景改用 &lt;code&gt;DONT_ASK&lt;/code&gt;，并显式允许安全工具。ASK 会转成 DENY，从而让任务失败得可观察，而不是永久等待不存在的用户。&lt;/p&gt;
&lt;h3 id="工作区恢复后找不到文件"&gt;&lt;a href="#%e5%b7%a5%e4%bd%9c%e5%8c%ba%e6%81%a2%e5%a4%8d%e5%90%8e%e6%89%be%e4%b8%8d%e5%88%b0%e6%96%87%e4%bb%b6" class="header-anchor"&gt;&lt;/a&gt;工作区恢复后找不到文件
&lt;/h3&gt;&lt;p&gt;本地 Workspace 不适合多副本。使用共享存储或远程文件系统，并保证所有副本使用一致的路径映射、租户隔离和版本策略。&lt;/p&gt;
&lt;h3 id="已有-sandbox为什么还要权限系统"&gt;&lt;a href="#%e5%b7%b2%e6%9c%89-sandbox%e4%b8%ba%e4%bb%80%e4%b9%88%e8%bf%98%e8%a6%81%e6%9d%83%e9%99%90%e7%b3%bb%e7%bb%9f" class="header-anchor"&gt;&lt;/a&gt;已有 Sandbox，为什么还要权限系统
&lt;/h3&gt;&lt;p&gt;Sandbox 限制“能破坏多大范围”，权限系统限制“此刻是否应该执行”。前者是隔离，后者是意图授权，必须叠加。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;AgentScope Java 2.0 的核心价值不是又实现了一次 ReAct，而是把 Agent 当成一个需要治理的长期运行系统：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;权限系统在每次工具调用前作出 ALLOW、ASK 或 DENY；&lt;/li&gt;
&lt;li&gt;Tool 的 Built-in Check 能根据真实参数建立不可绕过的业务安全线；&lt;/li&gt;
&lt;li&gt;HITL 用同一 session 暂停和恢复，而不是只在 Prompt 里“请求确认”；&lt;/li&gt;
&lt;li&gt;Workspace 管上下文，Sandbox 管隔离，分布式后端管恢复；&lt;/li&gt;
&lt;li&gt;幂等和业务事实仍由工具与数据库负责。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你的 Agent 会改文件、执行命令或触碰生产资源，先设计权限、审批、隔离和恢复，再讨论模型是否更聪明。生产 Agent 最重要的能力，不是永远做对，而是做错时被阻止、失败后可恢复、重试时不重复造成损失。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/agentscope-ai/agentscope-java" target="_blank" rel="noopener"
 &gt;AgentScope Java GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://java.agentscope.io/v2/zh/intro.html" target="_blank" rel="noopener"
 &gt;AgentScope Java 2.0 中文文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://java.agentscope.io/v2/zh/docs/building-blocks/permission-system.html" target="_blank" rel="noopener"
 &gt;Permission System&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://java.agentscope.io/v2/zh/docs/harness/workspace.html" target="_blank" rel="noopener"
 &gt;Workspace 与 Sandbox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://java.agentscope.io/v2/zh/docs/others/going-to-production.html" target="_blank" rel="noopener"
 &gt;生产部署&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/agentscope-ai/agentscope-java/releases/tag/v2.0.0" target="_blank" rel="noopener"
 &gt;AgentScope Java 2.0.0 Release&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Google ADK Java 实战：用 Code-first 方式编排多 Agent 工作流</title><link>https://blog.waihost.com/posts/google-adk-java-multi-agent/</link><pubDate>Thu, 23 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/google-adk-java-multi-agent/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/google-adk-java-multi-agent.svg" alt="Featured image of post Google ADK Java 实战：用 Code-first 方式编排多 Agent 工作流" /&gt;&lt;p&gt;Google Agent Development Kit（ADK）Java 是一个代码优先的 Agent 工具包：Agent 的角色、工具和编排直接写在 Java 中，可以进入版本控制、单元测试和部署流程。它不是只能调用 Gemini 的聊天 SDK，而是提供 &lt;code&gt;LlmAgent&lt;/code&gt;、Sequential/Parallel/Loop Agent、Session/State、Dev UI 和 A2A 集成。&lt;/p&gt;
&lt;p&gt;本文基于 Google ADK Java &lt;code&gt;1.7.0&lt;/code&gt;，实现一个“研究—审查—成稿”的顺序多 Agent 工作流，并说明工具、Session State、Dev UI 以及 A2A 与 MCP 的分工。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;截至 2026-07-23，官方 README 仍将 ADK Java 标记为 &lt;strong&gt;Preview / Pre-GA&lt;/strong&gt;，并说明相关能力按 “as is” 提供、支持可能有限。它适合学习、PoC 和可控试点；关键生产系统上线前必须锁版本、做回归测试并准备替代路径。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="一adk-的-code-first-是什么"&gt;&lt;a href="#%e4%b8%80adk-%e7%9a%84-code-first-%e6%98%af%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;一、ADK 的 Code-first 是什么
&lt;/h2&gt;&lt;p&gt;代码优先不只是“不写 YAML”，而是把 Agent 系统的重要结构放进类型化代码：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BaseAgent
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── LlmAgent 模型驱动，能调用工具或委派
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── SequentialAgent 按顺序执行子 Agent
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;├── ParallelAgent 并行执行独立子 Agent
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── LoopAgent 在条件下重复执行
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;LLM 适合处理语义判断和非结构化内容；Sequential/Parallel/Loop 则提供确定性控制。一个可靠系统往往混用二者，而不是让一个大模型临场规划全部步骤。&lt;/p&gt;
&lt;p&gt;本文流程是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用户主题
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; researcher：形成事实清单，写入 research_notes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; reviewer：检查证据与风险，写入 review_notes
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; writer：读取前两步结果，形成文章提纲
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;三个专业 Agent 可以使用不同 Prompt，输出通过 Session State 传递。&lt;/p&gt;
&lt;h2 id="二依赖凭据与版本状态"&gt;&lt;a href="#%e4%ba%8c%e4%be%9d%e8%b5%96%e5%87%ad%e6%8d%ae%e4%b8%8e%e7%89%88%e6%9c%ac%e7%8a%b6%e6%80%81" class="header-anchor"&gt;&lt;/a&gt;二、依赖、凭据与版本状态
&lt;/h2&gt;&lt;p&gt;Maven 依赖：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;maven.compiler.release&amp;gt;&lt;/span&gt;17&lt;span class="nt"&gt;&amp;lt;/maven.compiler.release&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;adk.version&amp;gt;&lt;/span&gt;1.7.0&lt;span class="nt"&gt;&amp;lt;/adk.version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.google.adk&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;google-adk&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${adk.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.google.adk&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;google-adk-dev&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${adk.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;google-adk-dev&lt;/code&gt; 用于本地开发 UI，不建议不加区分地打进生产镜像。&lt;/p&gt;
&lt;p&gt;使用 Google AI Studio / Gemini API 时，凭据名称和认证方式应以当前 ADK 官方文档为准，常见本地配置是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;GOOGLE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;你的密钥&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果使用 Vertex AI，还需要 Google Cloud Project、区域和 ADC 等配置。不要把服务账号 JSON 放进源码或容器镜像。&lt;/p&gt;
&lt;h2 id="三先实现一个可验证的-functiontool"&gt;&lt;a href="#%e4%b8%89%e5%85%88%e5%ae%9e%e7%8e%b0%e4%b8%80%e4%b8%aa%e5%8f%af%e9%aa%8c%e8%af%81%e7%9a%84-functiontool" class="header-anchor"&gt;&lt;/a&gt;三、先实现一个可验证的 FunctionTool
&lt;/h2&gt;&lt;p&gt;ADK 可以把静态 Java 方法包装成工具。下面的工具只返回受控的本地资料，便于无外部搜索依赖地验证调用链：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;com.google.adk.tools.Annotations.Schema&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.util.Map&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResearchTools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;lookupFrameworkFact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;framework&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;框架名称，例如 Spring AI Alibaba 或 AgentScope Java&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;framework&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;switch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;framework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;spring ai alibaba&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;success&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;fact&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;面向 Java 的 Agent、Workflow 与 Multi-Agent 框架&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;agentscope java&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;success&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;fact&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;面向分布式、生产级、长时间运行 Agent 的 Java 框架&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;status&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;not_found&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;fact&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;本地资料中没有该框架&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;包装为 &lt;code&gt;FunctionTool&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;com.google.adk.tools.FunctionTool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;factTool&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FunctionTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ResearchTools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;lookupFrameworkFact&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;工具参数用 &lt;code&gt;@Schema&lt;/code&gt; 写清名称和描述，返回值使用结构化 &lt;code&gt;Map&lt;/code&gt; 或 POJO。模型可以决定是否调用，但 Java 方法必须独立完成参数校验、鉴权、超时和幂等。&lt;/p&gt;
&lt;h2 id="四定义三个专业-llmagent"&gt;&lt;a href="#%e5%9b%9b%e5%ae%9a%e4%b9%89%e4%b8%89%e4%b8%aa%e4%b8%93%e4%b8%9a-llmagent" class="header-anchor"&gt;&lt;/a&gt;四、定义三个专业 LlmAgent
&lt;/h2&gt;&lt;h3 id="1-researcher"&gt;&lt;a href="#1-researcher" class="header-anchor"&gt;&lt;/a&gt;1. Researcher
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;com.google.adk.agents.LlmAgent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;LlmAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;researcher&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LlmAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;researcher&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;查找并整理主题相关事实&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gemini-2.0-flash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 根据用户主题整理事实清单。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 可调用 lookupFrameworkFact；找不到的内容明确标为待核验，禁止编造。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 输出简洁 Markdown 要点。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factTool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;outputKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;research_notes&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;outputKey&lt;/code&gt; 会把 Agent 最终输出写入 Session State，供后续 Agent 使用。&lt;/p&gt;
&lt;h3 id="2-reviewer"&gt;&lt;a href="#2-reviewer" class="header-anchor"&gt;&lt;/a&gt;2. Reviewer
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;LlmAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reviewer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LlmAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;reviewer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;审查研究事实、缺口与风险&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gemini-2.0-flash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 阅读 session state 中的 research_notes。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 分成：已确认、证据不足、可能过时三类；
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 不得把推测升级成事实，给出下一步核验建议。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;outputKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;review_notes&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-writer"&gt;&lt;a href="#3-writer" class="header-anchor"&gt;&lt;/a&gt;3. Writer
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;LlmAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;LlmAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;writer&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;把研究和审查结果组织成文章提纲&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gemini-2.0-flash&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 使用 research_notes 和 review_notes 生成中文技术文章提纲。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 对证据不足的结论保留警示；不输出版本发布稿，强调可复用原理与实践。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;outputKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;article_outline&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Prompt 应告诉 Agent 从 State 读取哪些键、输出写到哪里。否则三个 Agent 虽然按顺序运行，却不一定有效协作。&lt;/p&gt;
&lt;h2 id="五用-sequentialagent-显式编排"&gt;&lt;a href="#%e4%ba%94%e7%94%a8-sequentialagent-%e6%98%be%e5%bc%8f%e7%bc%96%e6%8e%92" class="header-anchor"&gt;&lt;/a&gt;五、用 SequentialAgent 显式编排
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;com.google.adk.agents.BaseAgent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;com.google.adk.agents.SequentialAgent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;java.util.List&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ArticleWorkflow&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BaseAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ROOT_AGENT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SequentialAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;article_workflow&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;研究、审查并生成文章提纲&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subAgents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;researcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;顺序编排的价值是依赖关系明确：Reviewer 不会在 Researcher 之前运行，Writer 也不会绕过审查。它比让一个“主管 Agent”自行决定顺序更容易测试。&lt;/p&gt;
&lt;p&gt;何时用其他类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ParallelAgent&lt;/code&gt;：两个子任务互不依赖，例如同时查官方文档和运行基准；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LoopAgent&lt;/code&gt;：审查不通过就重写，但必须设置退出条件和最大轮数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LlmAgent + subAgents&lt;/code&gt;：让模型按语义把任务委派给专业 Agent。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要把有先后依赖的任务硬塞进 Parallel，也不要创建没有最大次数的 Loop。&lt;/p&gt;
&lt;h2 id="六启动-dev-ui"&gt;&lt;a href="#%e5%85%ad%e5%90%af%e5%8a%a8-dev-ui" class="header-anchor"&gt;&lt;/a&gt;六、启动 Dev UI
&lt;/h2&gt;&lt;p&gt;ADK Java 自带开发 UI，可用于交互、查看事件和调试 Agent。最简单入口：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;com.google.adk.web.AdkWebServer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Main&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;static&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AdkWebServer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ArticleWorkflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ROOT_AGENT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;启动后在本地页面输入：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;比较 Java Agent 框架在工具权限和恢复方面的差异
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;开发 UI 适合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;看模型是否调用了工具；&lt;/li&gt;
&lt;li&gt;检查专业 Agent 的执行顺序；&lt;/li&gt;
&lt;li&gt;观察 State 中输出键；&lt;/li&gt;
&lt;li&gt;保存用于演示和人工回归的会话。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它不是生产认证网关。上线时仍需自己的 API 层、身份系统、限流、审计和 Secret 管理。&lt;/p&gt;
&lt;h2 id="七sessionstate-与-artifact"&gt;&lt;a href="#%e4%b8%83sessionstate-%e4%b8%8e-artifact" class="header-anchor"&gt;&lt;/a&gt;七、Session、State 与 Artifact
&lt;/h2&gt;&lt;p&gt;ADK 中要区分：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;概念&lt;/th&gt;
					&lt;th&gt;用途&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Session&lt;/td&gt;
					&lt;td&gt;某用户/某次连续交互的容器&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;State&lt;/td&gt;
					&lt;td&gt;小型结构化状态，如研究摘要、审核结论&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Event&lt;/td&gt;
					&lt;td&gt;用户、模型、工具与 Agent 运行过程&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Artifact&lt;/td&gt;
					&lt;td&gt;文件、报告、图片等较大对象&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;outputKey&lt;/code&gt; 写入的是 State，不应把大文件或整份日志塞进去。大对象应使用 Artifact 服务，并在 State 中只保存引用和摘要。&lt;/p&gt;
&lt;p&gt;生产环境还要决定 Session Service 的持久化实现。进程内 Session 适合 Demo；多实例部署需要共享后端，并测试并发更新、过期、迁移和恢复。&lt;/p&gt;
&lt;p&gt;即使 Session 能恢复，工具副作用仍要幂等。流程崩溃可能发生在外部 API 已成功、结果尚未写回 Event/State 的窗口。&lt;/p&gt;
&lt;h2 id="八确定性-workflow-与-llm-路由怎样选择"&gt;&lt;a href="#%e5%85%ab%e7%a1%ae%e5%ae%9a%e6%80%a7-workflow-%e4%b8%8e-llm-%e8%b7%af%e7%94%b1%e6%80%8e%e6%a0%b7%e9%80%89%e6%8b%a9" class="header-anchor"&gt;&lt;/a&gt;八、确定性 Workflow 与 LLM 路由怎样选择
&lt;/h2&gt;&lt;p&gt;优先确定性编排的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;合规审批顺序固定；&lt;/li&gt;
&lt;li&gt;后一步依赖前一步产物；&lt;/li&gt;
&lt;li&gt;需要精确控制成本和最大循环；&lt;/li&gt;
&lt;li&gt;每个阶段都要测试和审计。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;适合 LLM 路由的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户意图类别多，规则难以穷举；&lt;/li&gt;
&lt;li&gt;多个专业 Agent 能力边界清楚；&lt;/li&gt;
&lt;li&gt;选错路由的损失低，且可以回退；&lt;/li&gt;
&lt;li&gt;工具和 Agent 都没有越权能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;常用组合是：顶层确定性 Workflow 控制阶段，某个阶段内部用 LlmAgent 选择工具或子 Agent。&lt;/p&gt;
&lt;h2 id="九a2a-与-mcp-的分工"&gt;&lt;a href="#%e4%b9%9da2a-%e4%b8%8e-mcp-%e7%9a%84%e5%88%86%e5%b7%a5" class="header-anchor"&gt;&lt;/a&gt;九、A2A 与 MCP 的分工
&lt;/h2&gt;&lt;p&gt;ADK 官方重点支持 A2A，用于远程 Agent 之间的能力发现和通信。MCP 则主要面向工具、资源和 Prompt 的互操作。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;文章 Agent
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --A2A--&amp;gt; 独立部署的法务审查 Agent
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --MCP--&amp;gt; 搜索、数据库、文件等工具服务器
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;二者不是替代关系：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;协议&lt;/th&gt;
					&lt;th&gt;对端&lt;/th&gt;
					&lt;th&gt;典型能力&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;A2A&lt;/td&gt;
					&lt;td&gt;另一个自治 Agent&lt;/td&gt;
					&lt;td&gt;委派任务、跟踪远程任务、交换结果&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;MCP&lt;/td&gt;
					&lt;td&gt;工具/资源服务器&lt;/td&gt;
					&lt;td&gt;调函数、读资源、获取 Prompt&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;跨进程后要验证认证、超时、取消、流式、重试和版本协商。协议兼容不代表安全策略自动兼容。&lt;/p&gt;
&lt;h2 id="十preview-阶段怎样控制风险"&gt;&lt;a href="#%e5%8d%81preview-%e9%98%b6%e6%ae%b5%e6%80%8e%e6%a0%b7%e6%8e%a7%e5%88%b6%e9%a3%8e%e9%99%a9" class="header-anchor"&gt;&lt;/a&gt;十、Preview 阶段怎样控制风险
&lt;/h2&gt;&lt;p&gt;官方 1.7.0 已发布到 Maven Central，但 README 仍标记 Preview / Pre-GA，且部分评测能力仍在演进。工程上应：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;锁定 &lt;code&gt;1.7.0&lt;/code&gt;，不使用动态版本；&lt;/li&gt;
&lt;li&gt;用自己的 &lt;code&gt;AgentWorkflow&lt;/code&gt; 接口包住 ADK 类型；&lt;/li&gt;
&lt;li&gt;保存固定输入、工具调用和状态输出作为回归集；&lt;/li&gt;
&lt;li&gt;将模型与工具封装在可替换适配层；&lt;/li&gt;
&lt;li&gt;不把 Dev UI 暴露到公网生产环境；&lt;/li&gt;
&lt;li&gt;对每次升级验证 Session schema、事件格式和工具调用；&lt;/li&gt;
&lt;li&gt;为迁移到其他工作流引擎保留业务层边界。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;“1.x”不自动等于 GA，官方发布阶段声明比版本号更重要。&lt;/p&gt;
&lt;h2 id="十一常见问题"&gt;&lt;a href="#%e5%8d%81%e4%b8%80%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98" class="header-anchor"&gt;&lt;/a&gt;十一、常见问题
&lt;/h2&gt;&lt;h3 id="后续-agent-读不到前一步输出"&gt;&lt;a href="#%e5%90%8e%e7%bb%ad-agent-%e8%af%bb%e4%b8%8d%e5%88%b0%e5%89%8d%e4%b8%80%e6%ad%a5%e8%be%93%e5%87%ba" class="header-anchor"&gt;&lt;/a&gt;后续 Agent 读不到前一步输出
&lt;/h3&gt;&lt;p&gt;确认前一步设置 &lt;code&gt;outputKey&lt;/code&gt;，后续 Prompt 使用同一个 State 键；还要确认 Workflow 使用同一 Session，而不是为每步创建新会话。&lt;/p&gt;
&lt;h3 id="模型不调用-functiontool"&gt;&lt;a href="#%e6%a8%a1%e5%9e%8b%e4%b8%8d%e8%b0%83%e7%94%a8-functiontool" class="header-anchor"&gt;&lt;/a&gt;模型不调用 FunctionTool
&lt;/h3&gt;&lt;p&gt;检查方法是否为可反射调用的静态方法、名称是否传给 &lt;code&gt;FunctionTool.create&lt;/code&gt;、参数 Schema 是否明确，以及具体模型是否支持工具调用。&lt;/p&gt;
&lt;h3 id="parallelagent-结果互相覆盖"&gt;&lt;a href="#parallelagent-%e7%bb%93%e6%9e%9c%e4%ba%92%e7%9b%b8%e8%a6%86%e7%9b%96" class="header-anchor"&gt;&lt;/a&gt;ParallelAgent 结果互相覆盖
&lt;/h3&gt;&lt;p&gt;每个并行 Agent 使用唯一 &lt;code&gt;outputKey&lt;/code&gt;，汇总 Agent 再读取多个键。不要让并行分支写同一个 State 字段。&lt;/p&gt;
&lt;h3 id="loopagent-不停运行"&gt;&lt;a href="#loopagent-%e4%b8%8d%e5%81%9c%e8%bf%90%e8%a1%8c" class="header-anchor"&gt;&lt;/a&gt;LoopAgent 不停运行
&lt;/h3&gt;&lt;p&gt;增加确定性退出条件和最大迭代数，同时记录每轮结果。不要只靠 Prompt 中“完成后停止”。&lt;/p&gt;
&lt;h3 id="本地成功上云后丢-session"&gt;&lt;a href="#%e6%9c%ac%e5%9c%b0%e6%88%90%e5%8a%9f%e4%b8%8a%e4%ba%91%e5%90%8e%e4%b8%a2-session" class="header-anchor"&gt;&lt;/a&gt;本地成功，上云后丢 Session
&lt;/h3&gt;&lt;p&gt;本地 Dev UI 常使用进程内服务。生产要配置共享 Session/Artifact 后端，并验证多副本路由、过期和并发更新。&lt;/p&gt;
&lt;h2 id="十二生产清单"&gt;&lt;a href="#%e5%8d%81%e4%ba%8c%e7%94%9f%e4%ba%a7%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;十二、生产清单
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 明确标记 ADK Java 为 Preview / Pre-GA，并锁定精确版本；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 工作流顺序由代码控制，LLM 只处理需要语义判断的节点；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 每个 State Key 有唯一写入者或明确合并规则；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Artifact 与 State 分开保存；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 工具有鉴权、幂等、超时和结构化错误；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Loop 有最大次数，Parallel 分支避免写冲突；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; Dev UI 只用于受控开发环境；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; A2A 和 MCP 分别设计认证与审计；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 升级前运行固定 Agent/Tool/State 回归集；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 多实例环境使用共享 Session 服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;Google ADK Java 最有价值的地方，是让 Java 开发者在同一套代码中组合两类控制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LlmAgent&lt;/code&gt; 处理语义、工具选择和非结构化内容；&lt;/li&gt;
&lt;li&gt;Sequential、Parallel、Loop 等 Workflow Agent 提供确定性控制；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;outputKey&lt;/code&gt;、Session State 和 Artifact 负责阶段间传递；&lt;/li&gt;
&lt;li&gt;Dev UI 降低本地调试成本；&lt;/li&gt;
&lt;li&gt;A2A 连接独立 Agent，MCP 连接工具和资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在 Preview 阶段，建议从边界明确的内部工作流开始，而不是立刻替换关键核心系统。Code-first 的真正收益不只是代码看起来清晰，而是工作流能进入代码审查、测试、版本管理和故障演练。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/google/adk-java" target="_blank" rel="noopener"
 &gt;Google ADK Java GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://google.github.io/adk-docs/" target="_blank" rel="noopener"
 &gt;Google ADK 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/google/adk-samples" target="_blank" rel="noopener"
 &gt;Google ADK Samples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://central.sonatype.com/artifact/com.google.adk/google-adk/1.7.0" target="_blank" rel="noopener"
 &gt;ADK Java 1.7.0 Maven Central&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/a2aproject/A2A" target="_blank" rel="noopener"
 &gt;A2A Protocol&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/google/adk-java/tree/main/a2a" target="_blank" rel="noopener"
 &gt;ADK Java A2A 集成说明&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>LangChain4j 实战：用 AI Services 与 Tool Calling 构建可控 Java Agent</title><link>https://blog.waihost.com/posts/langchain4j-ai-services-tool-calling/</link><pubDate>Thu, 23 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/langchain4j-ai-services-tool-calling/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/langchain4j-ai-services-tool-calling.svg" alt="Featured image of post LangChain4j 实战：用 AI Services 与 Tool Calling 构建可控 Java Agent" /&gt;&lt;p&gt;LangChain4j 最容易被误解成“Python LangChain 的 Java 移植版”。实际上，它更强调 Java 接口、POJO、注解、类型安全和依赖注入。开发者可以把大模型能力定义成一个普通 Java Service，再组合 Chat Memory、RAG、&lt;code&gt;@Tool&lt;/code&gt; 和 MCP。&lt;/p&gt;
&lt;p&gt;本文使用 LangChain4j &lt;code&gt;1.18.0&lt;/code&gt; 和 JDK 17，实现一个订单查询与退款申请助手。模型可以读取订单，但退款属于副作用操作，必须先创建待审批申请，不能让模型直接完成退款。这个案例能说明：Tool Calling Agent 的重点不在“模型会调用函数”，而在于怎样让工具接口、状态和权限保持可控。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;LangChain4j 的核心 AI Services、Tool Calling 与模型集成已经广泛使用；但官方明确说明整个 &lt;code&gt;langchain4j-agentic&lt;/code&gt; 高层模块仍属于 experimental。本文先使用稳定的 AI Services + Tools 路线，不把实验性多 Agent API 当作生产前提。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="一agent-的最小闭环"&gt;&lt;a href="#%e4%b8%80agent-%e7%9a%84%e6%9c%80%e5%b0%8f%e9%97%ad%e7%8e%af" class="header-anchor"&gt;&lt;/a&gt;一、Agent 的最小闭环
&lt;/h2&gt;&lt;p&gt;一个最小 Tool Calling Agent 包含：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用户消息
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; AI Service 组装系统消息和历史
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; ChatModel 判断是否调用工具
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; LangChain4j 执行 @Tool 方法
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; 工具结果回到模型
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; 模型生成最终回答
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这已经具备“观察—行动—再观察”的基本能力。只有当任务需要多个专业 Agent、共享工作区和复杂编排时，才有必要引入更高层 Agentic 模块。&lt;/p&gt;
&lt;p&gt;在工程上，Agent 由三部分构成：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;部分&lt;/th&gt;
					&lt;th&gt;责任&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;AI Service 接口&lt;/td&gt;
					&lt;td&gt;定义输入输出、系统约束和会话边界&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;ChatModel&lt;/td&gt;
					&lt;td&gt;生成回答与工具调用请求&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Tool 对象&lt;/td&gt;
					&lt;td&gt;在 Java 可信边界内访问业务系统&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;模型提出调用，Java 工具决定什么能够真正执行。&lt;/p&gt;
&lt;h2 id="二依赖和模型"&gt;&lt;a href="#%e4%ba%8c%e4%be%9d%e8%b5%96%e5%92%8c%e6%a8%a1%e5%9e%8b" class="header-anchor"&gt;&lt;/a&gt;二、依赖和模型
&lt;/h2&gt;&lt;p&gt;LangChain4j 1.18.0 的最低 JDK 是 17。使用 OpenAI 兼容模型时可先引入：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;maven.compiler.release&amp;gt;&lt;/span&gt;17&lt;span class="nt"&gt;&amp;lt;/maven.compiler.release&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;langchain4j.version&amp;gt;&lt;/span&gt;1.18.0&lt;span class="nt"&gt;&amp;lt;/langchain4j.version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;dependencyManagement&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;dev.langchain4j&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;langchain4j-bom&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${langchain4j.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;type&amp;gt;&lt;/span&gt;pom&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;import&lt;span class="nt"&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/dependencyManagement&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;dev.langchain4j&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;langchain4j&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;dev.langchain4j&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;langchain4j-open-ai&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;BOM 中部分扩展模块仍可能采用 beta 版本号。生产项目要看实际依赖树，而不能只看到 BOM 是 &lt;code&gt;1.18.0&lt;/code&gt; 就认为所有模块都达到相同稳定级别。&lt;/p&gt;
&lt;p&gt;模型配置示例：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dev.langchain4j.model.chat.ChatModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dev.langchain4j.model.openai.OpenAiChatModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ChatModel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OpenAiChatModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;OPENAI_API_KEY&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;modelName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;gpt-4.1-mini&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;具体模型名和 Tool Calling 能力以提供商当前文档为准。若接入 OpenAI-compatible 服务，还需设置 &lt;code&gt;baseUrl&lt;/code&gt;，并实测结构化参数、并行工具调用和流式行为，而不能只验证普通聊天。&lt;/p&gt;
&lt;h2 id="三用-java-接口定义-ai-service"&gt;&lt;a href="#%e4%b8%89%e7%94%a8-java-%e6%8e%a5%e5%8f%a3%e5%ae%9a%e4%b9%89-ai-service" class="header-anchor"&gt;&lt;/a&gt;三、用 Java 接口定义 AI Service
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dev.langchain4j.service.MemoryId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dev.langchain4j.service.SystemMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dev.langchain4j.service.UserMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;OrderAssistant&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@SystemMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 你是订单售后助手。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 查询订单前必须获得明确订单号；不得猜测订单状态。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 退款只能创建待审批申请，不能声称退款已经到账。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 最终回答要区分：查询结果、待审批、已拒绝和已完成。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@MemoryId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@UserMessage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;接口有两个值得注意的边界：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;@MemoryId&lt;/code&gt; 只是选择会话记忆的键，不应直接作为业务身份凭据；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String&lt;/code&gt; 返回值适合聊天，业务系统若需要可靠状态，应从工具/数据库读取，而不是解析模型自然语言。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;真实 HTTP 服务应该从认证上下文得到 &lt;code&gt;userId&lt;/code&gt;，不能让客户端任意指定其他用户的 Memory ID。&lt;/p&gt;
&lt;h2 id="四设计只读工具与副作用工具"&gt;&lt;a href="#%e5%9b%9b%e8%ae%be%e8%ae%a1%e5%8f%aa%e8%af%bb%e5%b7%a5%e5%85%b7%e4%b8%8e%e5%89%af%e4%bd%9c%e7%94%a8%e5%b7%a5%e5%85%b7" class="header-anchor"&gt;&lt;/a&gt;四、设计只读工具与副作用工具
&lt;/h2&gt;&lt;p&gt;先定义订单对象和仓库：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ownerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BigDecimal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;paidAmount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;查询工具只读，但仍要做归属校验：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dev.langchain4j.agent.tool.P&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dev.langchain4j.agent.tool.Tool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderTools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RefundRequestRepository&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;refunds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CurrentUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;OrderTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RefundRequestRepository&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;refunds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CurrentUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refunds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;refunds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentUser&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;查询当前登录用户的订单状态和实付金额&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OrderView&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@P&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;订单号，例如 O-20260725-001&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IllegalArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;订单不存在&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;requireOwner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OrderView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;paidAmount&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;requireOwner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ownerId&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SecurityException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;无权访问该订单&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;副作用工具不要直接叫 &lt;code&gt;refund&lt;/code&gt;，而是创建待审批申请：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;为当前用户的订单创建退款审批申请；不会直接退款&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RefundRequestView&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;prepareRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@P&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;订单号&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@P&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;退款原因，必须来自用户明确表达&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@P&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;本次操作幂等键&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IllegalArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;订单不存在&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;requireOwner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;PAID&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;IllegalStateException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;只有 PAID 订单可申请退款&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;refunds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByOperationId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orElseGet&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;refunds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createPending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;operationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里有三条硬规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;身份来自 &lt;code&gt;CurrentUser&lt;/code&gt;，不作为 Tool 参数暴露给模型；&lt;/li&gt;
&lt;li&gt;Java 代码再次校验订单状态和归属；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;operationId&lt;/code&gt; 建立唯一约束，使模型重试不会创建重复申请。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;真正执行退款的 &lt;code&gt;approveAndExecuteRefund(approvalId)&lt;/code&gt; 应放在后台审批服务中，不注册给该 Agent。&lt;/p&gt;
&lt;h2 id="五绑定模型memory-和-tools"&gt;&lt;a href="#%e4%ba%94%e7%bb%91%e5%ae%9a%e6%a8%a1%e5%9e%8bmemory-%e5%92%8c-tools" class="header-anchor"&gt;&lt;/a&gt;五、绑定模型、Memory 和 Tools
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dev.langchain4j.memory.chat.MessageWindowChatMemory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dev.langchain4j.service.AiServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;OrderAssistant&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;assistant&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AiServices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderAssistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chatMemoryProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memoryId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MessageWindowChatMemory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withMaxMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;20&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderTools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;调用方式和普通 Java Service 相似：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;assistant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;authenticatedUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;帮我查订单 O-20260725-001；如果已支付，&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;以 operationId=refund-7f24 创建退款申请，原因是重复下单&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;模型可能先调用 &lt;code&gt;getOrder&lt;/code&gt;，观察状态后再调用 &lt;code&gt;prepareRefund&lt;/code&gt;，最后告知用户“已创建待审批申请”。它没有真正退款的工具，因此即使 Prompt 被忽略，也无法跨过审批边界。&lt;/p&gt;
&lt;h2 id="六chat-memory-不是业务数据库"&gt;&lt;a href="#%e5%85%adchat-memory-%e4%b8%8d%e6%98%af%e4%b8%9a%e5%8a%a1%e6%95%b0%e6%8d%ae%e5%ba%93" class="header-anchor"&gt;&lt;/a&gt;六、Chat Memory 不是业务数据库
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;MessageWindowChatMemory&lt;/code&gt; 只保留有限消息窗口，适合控制上下文长度。它不保证：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;永久保存历史；&lt;/li&gt;
&lt;li&gt;多实例共享；&lt;/li&gt;
&lt;li&gt;业务事实可审计；&lt;/li&gt;
&lt;li&gt;工具副作用 exactly-once；&lt;/li&gt;
&lt;li&gt;用户身份安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应区分：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;数据&lt;/th&gt;
					&lt;th&gt;应放在哪里&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;最近对话与 Tool Result&lt;/td&gt;
					&lt;td&gt;Chat Memory Store&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;订单和退款状态&lt;/td&gt;
					&lt;td&gt;业务数据库&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;用户身份与租户&lt;/td&gt;
					&lt;td&gt;认证上下文&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;幂等键和审批状态&lt;/td&gt;
					&lt;td&gt;业务数据库唯一约束&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;长文档知识&lt;/td&gt;
					&lt;td&gt;向量库/RAG，而不是无限消息窗口&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Memory 里即使写着“退款成功”，也不能作为事实。下一轮回答前，应通过只读工具重新查询业务状态。&lt;/p&gt;
&lt;h2 id="七工具描述决定模型能否正确使用"&gt;&lt;a href="#%e4%b8%83%e5%b7%a5%e5%85%b7%e6%8f%8f%e8%bf%b0%e5%86%b3%e5%ae%9a%e6%a8%a1%e5%9e%8b%e8%83%bd%e5%90%a6%e6%ad%a3%e7%a1%ae%e4%bd%bf%e7%94%a8" class="header-anchor"&gt;&lt;/a&gt;七、工具描述决定模型能否正确使用
&lt;/h2&gt;&lt;p&gt;LangChain4j 支持两层工具 API：底层 &lt;code&gt;ChatModel + ToolSpecification&lt;/code&gt;，以及高层 AI Services + &lt;code&gt;@Tool&lt;/code&gt;。大多数业务从高层开始即可。&lt;/p&gt;
&lt;p&gt;工具描述要回答：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;什么情况下调用；&lt;/li&gt;
&lt;li&gt;每个参数代表什么；&lt;/li&gt;
&lt;li&gt;是否有副作用；&lt;/li&gt;
&lt;li&gt;返回值是什么状态；&lt;/li&gt;
&lt;li&gt;哪些前置条件必须满足。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;反例：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;处理订单&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;模型既不知道是查询、取消还是退款，也无法稳定生成参数。&lt;/p&gt;
&lt;p&gt;改进：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;只查询当前用户订单；无副作用&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;OrderView&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;getOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@P&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;完整订单号&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;返回值优先使用 record/POJO，而不是拼接含糊字符串。工具错误也要映射为有限错误码，避免把数据库堆栈或敏感信息原样送回模型。&lt;/p&gt;
&lt;h2 id="八如何对工具做测试"&gt;&lt;a href="#%e5%85%ab%e5%a6%82%e4%bd%95%e5%af%b9%e5%b7%a5%e5%85%b7%e5%81%9a%e6%b5%8b%e8%af%95" class="header-anchor"&gt;&lt;/a&gt;八、如何对工具做测试
&lt;/h2&gt;&lt;p&gt;Agent 测试要分层：&lt;/p&gt;
&lt;h3 id="1-纯-java-工具测试"&gt;&lt;a href="#1-%e7%ba%af-java-%e5%b7%a5%e5%85%b7%e6%b5%8b%e8%af%95" class="header-anchor"&gt;&lt;/a&gt;1. 纯 Java 工具测试
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Test&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;duplicateOperationReturnsSameRefundRequest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prepareRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;O-1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;重复下单&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;op-123&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prepareRefund&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;O-1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;重复下单&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;op-123&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这层不调用模型，验证鉴权、状态机、幂等和异常。&lt;/p&gt;
&lt;h3 id="2-模型工具选择测试"&gt;&lt;a href="#2-%e6%a8%a1%e5%9e%8b%e5%b7%a5%e5%85%b7%e9%80%89%e6%8b%a9%e6%b5%8b%e8%af%95" class="header-anchor"&gt;&lt;/a&gt;2. 模型工具选择测试
&lt;/h3&gt;&lt;p&gt;准备固定请求集，记录模型是否：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;缺订单号时先追问；&lt;/li&gt;
&lt;li&gt;未查询状态就申请退款；&lt;/li&gt;
&lt;li&gt;编造不存在的 Tool；&lt;/li&gt;
&lt;li&gt;把“咨询退款政策”误判成创建退款；&lt;/li&gt;
&lt;li&gt;在工具失败后声称成功。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-端到端故障测试"&gt;&lt;a href="#3-%e7%ab%af%e5%88%b0%e7%ab%af%e6%95%85%e9%9a%9c%e6%b5%8b%e8%af%95" class="header-anchor"&gt;&lt;/a&gt;3. 端到端故障测试
&lt;/h3&gt;&lt;p&gt;模拟超时、429、工具异常和进程重启，确认不会重复副作用，最终状态来自数据库。&lt;/p&gt;
&lt;p&gt;不要只凭一条成功 Demo 判断 Agent 可用。&lt;/p&gt;
&lt;h2 id="九mcp-什么时候加入"&gt;&lt;a href="#%e4%b9%9dmcp-%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e5%8a%a0%e5%85%a5" class="header-anchor"&gt;&lt;/a&gt;九、MCP 什么时候加入
&lt;/h2&gt;&lt;p&gt;如果工具和 Agent 在同一进程、都是 Java Bean，直接注册 &lt;code&gt;@Tool&lt;/code&gt; 最简单。以下情况再考虑 MCP：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工具由独立团队维护，需要跨语言/跨进程提供；&lt;/li&gt;
&lt;li&gt;多个 Agent 客户端需要共享同一工具目录；&lt;/li&gt;
&lt;li&gt;希望统一发现工具、资源和 Prompt；&lt;/li&gt;
&lt;li&gt;工具生命周期与业务应用不同。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;引入 MCP 后，仍要保留认证、租户隔离、超时、幂等和审批。MCP 解决互操作，不自动解决工具可信问题。&lt;/p&gt;
&lt;h2 id="十实验性-agentic-模块应该怎样用"&gt;&lt;a href="#%e5%8d%81%e5%ae%9e%e9%aa%8c%e6%80%a7-agentic-%e6%a8%a1%e5%9d%97%e5%ba%94%e8%af%a5%e6%80%8e%e6%a0%b7%e7%94%a8" class="header-anchor"&gt;&lt;/a&gt;十、实验性 Agentic 模块应该怎样用
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;langchain4j-agentic&lt;/code&gt; 提供 &lt;code&gt;@Agent&lt;/code&gt;、&lt;code&gt;AgenticScope&lt;/code&gt;、Sequential、Parallel、Loop、Supervisor 等模式。官方文档明确写着 &lt;strong&gt;whole module experimental&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;适合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;技术验证；&lt;/li&gt;
&lt;li&gt;内部工具；&lt;/li&gt;
&lt;li&gt;固定版本的 PoC；&lt;/li&gt;
&lt;li&gt;可以承担 API 变化成本的项目。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;生产采用前应：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;锁定精确依赖版本；&lt;/li&gt;
&lt;li&gt;给编排层包一层自己的接口；&lt;/li&gt;
&lt;li&gt;用回归集验证共享变量、错误传播和工具副作用；&lt;/li&gt;
&lt;li&gt;不让实验 API 渗透整个业务代码；&lt;/li&gt;
&lt;li&gt;为升级准备迁移测试。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若需求只是一个 Agent 调用几个工具，不必为了“多智能体”标签提前引入实验模块。&lt;/p&gt;
&lt;h2 id="十一常见问题"&gt;&lt;a href="#%e5%8d%81%e4%b8%80%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98" class="header-anchor"&gt;&lt;/a&gt;十一、常见问题
&lt;/h2&gt;&lt;h3 id="模型从不调用工具"&gt;&lt;a href="#%e6%a8%a1%e5%9e%8b%e4%bb%8e%e4%b8%8d%e8%b0%83%e7%94%a8%e5%b7%a5%e5%85%b7" class="header-anchor"&gt;&lt;/a&gt;模型从不调用工具
&lt;/h3&gt;&lt;p&gt;确认具体模型支持 Tool Calling；检查工具描述是否清楚、参数是否可生成，并把温度调低。OpenAI-compatible 不代表工具协议完全兼容，要抓取请求响应做实测。&lt;/p&gt;
&lt;h3 id="同一用户的对话串到一起"&gt;&lt;a href="#%e5%90%8c%e4%b8%80%e7%94%a8%e6%88%b7%e7%9a%84%e5%af%b9%e8%af%9d%e4%b8%b2%e5%88%b0%e4%b8%80%e8%b5%b7" class="header-anchor"&gt;&lt;/a&gt;同一用户的对话串到一起
&lt;/h3&gt;&lt;p&gt;检查 &lt;code&gt;@MemoryId&lt;/code&gt; 是否使用稳定、不可伪造且带租户命名空间的键。多实例还需要共享 ChatMemoryStore；进程内 Memory 不会自动同步。&lt;/p&gt;
&lt;h3 id="退款工具被重复调用"&gt;&lt;a href="#%e9%80%80%e6%ac%be%e5%b7%a5%e5%85%b7%e8%a2%ab%e9%87%8d%e5%a4%8d%e8%b0%83%e7%94%a8" class="header-anchor"&gt;&lt;/a&gt;退款工具被重复调用
&lt;/h3&gt;&lt;p&gt;不要试图只靠 Prompt 阻止。使用服务端生成的 operationId、数据库唯一约束和“重复请求返回原结果”的工具语义。&lt;/p&gt;
&lt;h3 id="tool-抛出异常后模型仍说成功"&gt;&lt;a href="#tool-%e6%8a%9b%e5%87%ba%e5%bc%82%e5%b8%b8%e5%90%8e%e6%a8%a1%e5%9e%8b%e4%bb%8d%e8%af%b4%e6%88%90%e5%8a%9f" class="header-anchor"&gt;&lt;/a&gt;Tool 抛出异常后模型仍说成功
&lt;/h3&gt;&lt;p&gt;让工具返回结构化 &lt;code&gt;status/errorCode&lt;/code&gt;，在 AI Service 外再查询业务事实。高风险接口不要把最终成功判定交给自然语言。&lt;/p&gt;
&lt;h3 id="换模型后效果明显变化"&gt;&lt;a href="#%e6%8d%a2%e6%a8%a1%e5%9e%8b%e5%90%8e%e6%95%88%e6%9e%9c%e6%98%8e%e6%98%be%e5%8f%98%e5%8c%96" class="header-anchor"&gt;&lt;/a&gt;换模型后效果明显变化
&lt;/h3&gt;&lt;p&gt;统一 API 只能统一调用方式，不能抹平模型在 Tool Calling、结构化输出、并行调用和上下文窗口上的差异。每个模型都要跑相同回归集。&lt;/p&gt;
&lt;h2 id="十二生产清单"&gt;&lt;a href="#%e5%8d%81%e4%ba%8c%e7%94%9f%e4%ba%a7%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;十二、生产清单
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; API Key 只从环境变量或 Secret Manager 读取；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; &lt;code&gt;@MemoryId&lt;/code&gt; 与认证用户绑定并包含租户命名空间；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 只读和写工具分离，Agent 默认只获得最小工具集；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 身份、金额、角色不允许由模型自由填写；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 所有写工具使用幂等键和唯一约束；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 高风险动作只生成待审批请求；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 工具返回结构化结果与有限错误码；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 模型、工具、token、延迟和 operationId 可关联追踪；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 使用固定测试集比较模型与版本；&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 若采用 Agentic 模块，明确标记 experimental 并隔离 API。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;使用 LangChain4j 构建可控 Agent 的关键，不是让一个 Java 接口看起来像魔法，而是保持职责边界：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;AI Services 负责消息、Memory 和模型交互；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Tool&lt;/code&gt; 负责把有限业务能力暴露给模型；&lt;/li&gt;
&lt;li&gt;Java 服务负责身份、权限、状态机和参数校验；&lt;/li&gt;
&lt;li&gt;数据库负责事实、审批与幂等；&lt;/li&gt;
&lt;li&gt;高层 &lt;code&gt;langchain4j-agentic&lt;/code&gt; 只有在确实需要复杂编排、并接受实验性 API 风险时再引入。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从“只读查询 + 创建待审批申请”开始，远比直接把退款、删库或发邮件工具交给模型可靠。Agent 可以拥有选择工具的自由，但不应该拥有绕过业务规则的自由。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/langchain4j/langchain4j" target="_blank" rel="noopener"
 &gt;LangChain4j GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.langchain4j.dev/get-started" target="_blank" rel="noopener"
 &gt;LangChain4j Get Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.langchain4j.dev/tutorials/ai-services" target="_blank" rel="noopener"
 &gt;AI Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.langchain4j.dev/tutorials/tools" target="_blank" rel="noopener"
 &gt;Tools / Function Calling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.langchain4j.dev/tutorials/chat-memory" target="_blank" rel="noopener"
 &gt;Chat Memory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.langchain4j.dev/tutorials/agents" target="_blank" rel="noopener"
 &gt;Agents and Agentic AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.langchain4j.dev/tutorials/mcp" target="_blank" rel="noopener"
 &gt;MCP&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Node.js 事件循环与 nextTick / microtask：原理、阶段与工程实践</title><link>https://blog.waihost.com/posts/nodejs-event-loop-nexttick-microtask/</link><pubDate>Thu, 23 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/nodejs-event-loop-nexttick-microtask/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/nodejs-event-loop-nexttick-microtask.svg" alt="Featured image of post Node.js 事件循环与 nextTick / microtask：原理、阶段与工程实践" /&gt;&lt;p&gt;HTTP 服务偶发“全站卡住几秒”、定时器比预期晚很多、&lt;code&gt;setTimeout(0)&lt;/code&gt; 和 &lt;code&gt;setImmediate&lt;/code&gt; 顺序飘忽不定、同步校验回调却读到未初始化变量——这些现象往往不是“Node 慢”，而是对 &lt;strong&gt;事件循环（Event Loop）阶段、nextTick 队列、微任务队列、Worker Pool&lt;/strong&gt; 理解不到位。&lt;/p&gt;
&lt;p&gt;本文基于 Node.js 官方 Learn 文档 &lt;em&gt;The Node.js Event Loop&lt;/em&gt;、&lt;em&gt;Don&amp;rsquo;t Block the Event Loop&lt;/em&gt;，以及 API 文档 &lt;code&gt;process.nextTick&lt;/code&gt; / Timers，把调度机制讲清楚，并给出可复现实验与排障清单。&lt;/p&gt;
&lt;h2 id="一问题背景单线程-js-为何能扛大量并发"&gt;&lt;a href="#%e4%b8%80%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af%e5%8d%95%e7%ba%bf%e7%a8%8b-js-%e4%b8%ba%e4%bd%95%e8%83%bd%e6%89%9b%e5%a4%a7%e9%87%8f%e5%b9%b6%e5%8f%91" class="header-anchor"&gt;&lt;/a&gt;一、问题背景：单线程 JS 为何能扛大量并发
&lt;/h2&gt;&lt;p&gt;官方对事件循环的定位很直接：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;The event loop is what allows Node.js to perform &lt;strong&gt;non-blocking I/O&lt;/strong&gt; operations — despite the fact that a single JavaScript thread is used by default — by offloading operations to the system kernel whenever possible.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;也就是说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;应用层 JS 默认跑在一条线程&lt;/strong&gt;（Event Loop / main thread）。&lt;/li&gt;
&lt;li&gt;能丢给内核的非阻塞 I/O（如多数网络 I/O）完成后，内核通知 Node，相关回调进入 &lt;strong&gt;poll&lt;/strong&gt; 等队列等待执行。&lt;/li&gt;
&lt;li&gt;不便做成纯非阻塞的昂贵工作（部分 &lt;code&gt;fs&lt;/code&gt;、&lt;code&gt;dns.lookup&lt;/code&gt;、&lt;code&gt;crypto.pbkdf2&lt;/code&gt;、&lt;code&gt;zlib&lt;/code&gt; 等）会提交到 libuv 的 &lt;strong&gt;Worker Pool&lt;/strong&gt;，做完再回到事件循环。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;Don&amp;rsquo;t Block the Event Loop&lt;/em&gt; 给出的工程拇指法则是：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Node.js is fast when the work associated with each client at any given time is &lt;strong&gt;&amp;ldquo;small&amp;rdquo;&lt;/strong&gt;.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;线程少、上下文切换少，是 Node 能用较少资源服务大量连接的原因；代价是：&lt;strong&gt;任意一次过长的同步回调，都会让其他客户端得不到轮转&lt;/strong&gt;——既伤吞吐，也构成 DoS 面（恶意大输入、灾难性正则等）。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;线程类型&lt;/th&gt;
					&lt;th&gt;跑什么&lt;/th&gt;
					&lt;th&gt;阻塞后果&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Event Loop&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;初始化、JS 回调、&lt;code&gt;await&lt;/code&gt;/&lt;code&gt;then&lt;/code&gt; 续体、网络等非阻塞 I/O 编排&lt;/td&gt;
					&lt;td&gt;所有请求排队；定时器/I/O 回调整体延迟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Worker Pool&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;昂贵任务（多数 &lt;code&gt;fs&lt;/code&gt;、部分 DNS/Crypto/Zlib、C++ 提交的任务）&lt;/td&gt;
					&lt;td&gt;池子被占满时同类任务排队；回压到业务&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="二核心模型阶段队列--阶段外的-nexttick"&gt;&lt;a href="#%e4%ba%8c%e6%a0%b8%e5%bf%83%e6%a8%a1%e5%9e%8b%e9%98%b6%e6%ae%b5%e9%98%9f%e5%88%97--%e9%98%b6%e6%ae%b5%e5%a4%96%e7%9a%84-nexttick" class="header-anchor"&gt;&lt;/a&gt;二、核心模型：阶段队列 + 阶段外的 nextTick
&lt;/h2&gt;&lt;h3 id="1-启动与相位图"&gt;&lt;a href="#1-%e5%90%af%e5%8a%a8%e4%b8%8e%e7%9b%b8%e4%bd%8d%e5%9b%be" class="header-anchor"&gt;&lt;/a&gt;1. 启动与相位图
&lt;/h3&gt;&lt;p&gt;进程启动后会：初始化事件循环 → 执行入口脚本（其中可调异步 API、定时器、&lt;code&gt;process.nextTick()&lt;/code&gt;）→ 再进入循环迭代。&lt;/p&gt;
&lt;p&gt;官方简化相位（每个 box 是一个 &lt;strong&gt;phase&lt;/strong&gt;）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ┌───────────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ timers │ setTimeout / setInterval
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─────────────┬─────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ┌───────────────────────────┐
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;┌─&amp;gt;│ pending callbacks │ 延迟到下一轮的部分系统 I/O 回调
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ └─────────────┬─────────────┘
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ idle, prepare （内部使用）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ poll 取新 I/O 事件；执行 I/O 回调
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ check setImmediate 回调
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ close callbacks 如 socket.on(&amp;#39;close&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;│ v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└───────────── timers ──────────┘
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一般规则：进入某 phase 后，先做该 phase 专属操作，再 FIFO 执行队列回调，直到队列耗尽或达到系统相关的上限，再进入下一 phase。&lt;/p&gt;
&lt;p&gt;文档还强调：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;timers&lt;/strong&gt; 指定的是“阈值（threshold）之后&lt;strong&gt;尽早&lt;/strong&gt;可执行”，不是精确闹钟；OS 调度与其它回调都会让它推迟。&lt;/li&gt;
&lt;li&gt;从 &lt;strong&gt;libuv 1.45.0（Node.js 20）&lt;/strong&gt; 起，&lt;strong&gt;每个事件循环迭代中 timers 在 poll 之后运行&lt;/strong&gt;；为兼容，进入循环前仍会跑一轮 timers。这会影响某些场景下 &lt;code&gt;setImmediate&lt;/code&gt; 与定时器的相对时序。&lt;/li&gt;
&lt;li&gt;每一轮事件循环之间，若不再等待任何异步 I/O 或定时器，进程可干净退出。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-各-phase-在工程上的含义"&gt;&lt;a href="#2-%e5%90%84-phase-%e5%9c%a8%e5%b7%a5%e7%a8%8b%e4%b8%8a%e7%9a%84%e5%90%ab%e4%b9%89" class="header-anchor"&gt;&lt;/a&gt;2. 各 phase 在工程上的含义
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Phase&lt;/th&gt;
					&lt;th&gt;主要职责&lt;/th&gt;
					&lt;th&gt;工程注意&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;timers&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;setTimeout&lt;/code&gt; / &lt;code&gt;setInterval&lt;/code&gt; 到期回调&lt;/td&gt;
					&lt;td&gt;阈值语义；可能被长回调推迟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;pending callbacks&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;部分系统操作延迟回调（如某些 TCP &lt;code&gt;ECONNREFUSED&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;平台差异存在，但日常业务少直接依赖&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;poll&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;检索新 I/O；执行几乎所有 I/O 回调（除 close、定时器、&lt;code&gt;setImmediate&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;空队列时可能阻塞等待；有 &lt;code&gt;setImmediate&lt;/code&gt; 时可能结束 poll 进入 check&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;check&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;setImmediate&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;“poll 完成后立即跑一轮脚本”的专用相位&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;close callbacks&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;如 &lt;code&gt;socket.destroy()&lt;/code&gt; 触发的 &lt;code&gt;'close'&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;否则 close 也可能经 &lt;code&gt;nextTick&lt;/code&gt; 发出&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="3-processnexttick-不在相位图里"&gt;&lt;a href="#3-processnexttick-%e4%b8%8d%e5%9c%a8%e7%9b%b8%e4%bd%8d%e5%9b%be%e9%87%8c" class="header-anchor"&gt;&lt;/a&gt;3. &lt;code&gt;process.nextTick&lt;/code&gt; 不在相位图里
&lt;/h3&gt;&lt;p&gt;官方明确：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;code&gt;process.nextTick()&lt;/code&gt; is &lt;strong&gt;not technically part of the event loop&lt;/strong&gt;. Instead, the &lt;strong&gt;nextTickQueue&lt;/strong&gt; will be processed after the current operation is completed, &lt;strong&gt;regardless of the current phase&lt;/strong&gt;.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;任意 phase 里调用 &lt;code&gt;nextTick&lt;/code&gt;，都会在&lt;strong&gt;当前操作结束后、事件循环继续前进前&lt;/strong&gt;排空 nextTick 队列。因此递归 &lt;code&gt;nextTick&lt;/code&gt; 可以 &lt;strong&gt;饿死 I/O&lt;/strong&gt;（永远到不了 poll）——这是合法但危险的行为。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;process&lt;/code&gt; API 进一步说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nextTick(callback)&lt;/code&gt; 把回调放进 &lt;strong&gt;next tick queue&lt;/strong&gt;；当前 JS 栈操作结束后、事件循环继续前&lt;strong&gt;完全排空&lt;/strong&gt;该队列。&lt;/li&gt;
&lt;li&gt;稳定性标注为 &lt;strong&gt;Legacy&lt;/strong&gt;，多数用户代码更推荐 &lt;strong&gt;&lt;code&gt;queueMicrotask()&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;每次 next tick 队列排空后，会&lt;strong&gt;立即&lt;/strong&gt;再排空 &lt;strong&gt;microtask 队列&lt;/strong&gt;（Promise 的 &lt;code&gt;then&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt;/&lt;code&gt;finally&lt;/code&gt;、&lt;code&gt;queueMicrotask&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模块系统差异&lt;/strong&gt;（官方示例）：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CJS&lt;/strong&gt;：通常 &lt;code&gt;nextTick&lt;/code&gt; → Promise/&lt;code&gt;queueMicrotask&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ESM&lt;/strong&gt;：模块求值本身已在 microtask 路径上，常见顺序是 Promise/&lt;code&gt;queueMicrotask&lt;/code&gt; → &lt;code&gt;nextTick&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// CJS 常见输出：nextTick → resolve → microtask
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nextTick&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;node:process&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;resolve&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;queueMicrotask&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;microtask&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;nextTick&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;nextTick&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ESM 常见输出：resolve → microtask → nextTick
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nextTick&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;from&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;node:process&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;resolve&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;queueMicrotask&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;microtask&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;nextTick&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;nextTick&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="4-setimmediate-vs-settimeout0"&gt;&lt;a href="#4-setimmediate-vs-settimeout0" class="header-anchor"&gt;&lt;/a&gt;4. &lt;code&gt;setImmediate&lt;/code&gt; vs &lt;code&gt;setTimeout(0)&lt;/code&gt;
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;API&lt;/th&gt;
					&lt;th&gt;语义（官方）&lt;/th&gt;
					&lt;th&gt;典型落点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;setImmediate(fn)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;poll 阶段结束后执行脚本&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;check&lt;/strong&gt; phase&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;setTimeout(fn, 0)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;至少等待阈值（0ms）后尽早执行&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;timers&lt;/strong&gt; phase&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;主模块顶层&lt;/strong&gt;同时调度两者：顺序&lt;strong&gt;不确定&lt;/strong&gt;，取决于进程性能与调度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在 I/O 回调内&lt;/strong&gt;（例如 &lt;code&gt;fs.readFile&lt;/code&gt; 回调里）同时调度：官方保证 &lt;strong&gt;&lt;code&gt;setImmediate&lt;/code&gt; 总是先于 timers&lt;/strong&gt;，与 timers 数量无关。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;官方也坦言命名历史包袱：&lt;code&gt;nextTick&lt;/code&gt; 比 &lt;code&gt;setImmediate&lt;/code&gt; 更“立即”，名字几乎应互换；但为兼容 npm 生态不会改名。&lt;strong&gt;一般推荐优先用 &lt;code&gt;setImmediate&lt;/code&gt; 推理成本更低&lt;/strong&gt;；&lt;code&gt;nextTick&lt;/code&gt; 留给“必须在调用栈展开后、循环前进前”的场景。&lt;/p&gt;
&lt;h2 id="三实践可复现实验"&gt;&lt;a href="#%e4%b8%89%e5%ae%9e%e8%b7%b5%e5%8f%af%e5%a4%8d%e7%8e%b0%e5%ae%9e%e9%aa%8c" class="header-anchor"&gt;&lt;/a&gt;三、实践：可复现实验
&lt;/h2&gt;&lt;h3 id="实验-aio-周期内-immediate-恒先于-timeout0"&gt;&lt;a href="#%e5%ae%9e%e9%aa%8c-aio-%e5%91%a8%e6%9c%9f%e5%86%85-immediate-%e6%81%92%e5%85%88%e4%ba%8e-timeout0" class="header-anchor"&gt;&lt;/a&gt;实验 A：I/O 周期内 immediate 恒先于 timeout(0)
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// save as io-order.js (CJS)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;node:fs&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;timeout&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;setImmediate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;immediate&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;node io-order.js
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 期望：&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# immediate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# timeout&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="实验-bnexttick-在当前操作后插入早于进入下一-phase"&gt;&lt;a href="#%e5%ae%9e%e9%aa%8c-bnexttick-%e5%9c%a8%e5%bd%93%e5%89%8d%e6%93%8d%e4%bd%9c%e5%90%8e%e6%8f%92%e5%85%a5%e6%97%a9%e4%ba%8e%e8%bf%9b%e5%85%a5%e4%b8%8b%e4%b8%80-phase" class="header-anchor"&gt;&lt;/a&gt;实验 B：nextTick 在“当前操作后”插入，早于进入下一 phase
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// nexttick-vs-immediate.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;node:fs&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;setImmediate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;immediate&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nextTick&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;nextTick&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;promise&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在 I/O 回调这个“当前操作”末尾，会先排空 &lt;strong&gt;nextTick&lt;/strong&gt;，再处理 microtask，然后事件循环才能推进到后续 phase（check 中的 immediate）。你应看到 &lt;code&gt;nextTick&lt;/code&gt; 先于 &lt;code&gt;immediate&lt;/code&gt;；CJS 下 &lt;code&gt;nextTick&lt;/code&gt; 通常也先于 &lt;code&gt;promise&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="实验-c为何-api-必须全同步或全异步"&gt;&lt;a href="#%e5%ae%9e%e9%aa%8c-c%e4%b8%ba%e4%bd%95-api-%e5%bf%85%e9%a1%bb%e5%85%a8%e5%90%8c%e6%ad%a5%e6%88%96%e5%85%a8%e5%bc%82%e6%ad%a5" class="header-anchor"&gt;&lt;/a&gt;实验 C：为何 API 必须“全同步或全异步”
&lt;/h3&gt;&lt;p&gt;官方反例：签名像异步、实际同步调用回调，调用方无法判断后续语句与回调的先后：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// BAD：可能同步也可能异步
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;maybeSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;node:fs&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;file&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;推荐：同步路径也 &lt;code&gt;nextTick(cb)&lt;/code&gt;（或统一走 Promise），保证“总是异步”：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nextTick&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;node:process&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;node:fs&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;definitelyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;nextTick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;file&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;构造函数里 &lt;code&gt;emit&lt;/code&gt; 同理：应 &lt;code&gt;nextTick(() =&amp;gt; this.emit('event'))&lt;/code&gt;，让调用方先挂上监听器。&lt;/p&gt;
&lt;h3 id="实验-d同步计算堵死循环对照"&gt;&lt;a href="#%e5%ae%9e%e9%aa%8c-d%e5%90%8c%e6%ad%a5%e8%ae%a1%e7%ae%97%e5%a0%b5%e6%ad%bb%e5%be%aa%e7%8e%af%e5%af%b9%e7%85%a7" class="header-anchor"&gt;&lt;/a&gt;实验 D：同步计算堵死循环（对照）
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// block-loop.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;node:http&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/block&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;// 同步忙等 3s
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;blocked\n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ok\n&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;listen 3456&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;开两个终端：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 终端 1：触发阻塞&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sS http://127.0.0.1:3456/block &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 终端 2：立即请求轻量路径&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;time&lt;/span&gt; curl -sS http://127.0.0.1:3456/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;会观察到轻量请求也被拖慢——因为 &lt;strong&gt;Event Loop 被同一线程的同步代码占满&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="四常见坑与排查"&gt;&lt;a href="#%e5%9b%9b%e5%b8%b8%e8%a7%81%e5%9d%91%e4%b8%8e%e6%8e%92%e6%9f%a5" class="header-anchor"&gt;&lt;/a&gt;四、常见坑与排查
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;现象&lt;/th&gt;
					&lt;th&gt;可能机制&lt;/th&gt;
					&lt;th&gt;处理方向&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;全站偶发卡顿、延迟尖刺&lt;/td&gt;
					&lt;td&gt;同步 CPU 重活 / 大 JSON / 灾难正则 / 长循环&lt;/td&gt;
					&lt;td&gt;限制输入；分区 yield；&lt;code&gt;worker_threads&lt;/code&gt; / 子进程卸载&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;setTimeout(100)&lt;/code&gt; 变成 105ms+&lt;/td&gt;
					&lt;td&gt;poll 长回调占住队列；阈值语义 + 其它回调&lt;/td&gt;
					&lt;td&gt;缩短 poll 回调；避免在 I/O 回调里做重计算&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;顶层 &lt;code&gt;setTimeout(0)&lt;/code&gt; 与 &lt;code&gt;setImmediate&lt;/code&gt; 顺序随机&lt;/td&gt;
					&lt;td&gt;主模块非 I/O 周期，时序依赖性能&lt;/td&gt;
					&lt;td&gt;需要稳定顺序时放到 I/O 回调，或只用一种调度 API&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;递归 &lt;code&gt;nextTick&lt;/code&gt; 后 I/O 永不完成&lt;/td&gt;
					&lt;td&gt;nextTick 队列饿死 poll&lt;/td&gt;
					&lt;td&gt;改 &lt;code&gt;setImmediate&lt;/code&gt; / 批处理 + yield&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;CJS/ESM 下 Promise 与 nextTick 顺序不一致&lt;/td&gt;
					&lt;td&gt;官方文档写明的队列次序差异&lt;/td&gt;
					&lt;td&gt;不要依赖跨模块类型的微次序；业务用明确的 async 编排&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;fs&lt;/code&gt;/&lt;code&gt;crypto&lt;/code&gt; 变慢但 CPU 不高&lt;/td&gt;
					&lt;td&gt;Worker Pool 打满&lt;/td&gt;
					&lt;td&gt;评估 &lt;code&gt;UV_THREADPOOL_SIZE&lt;/code&gt;、减少同步 &lt;code&gt;*Sync&lt;/code&gt;、合并小 I/O&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;同步 API 伪装异步&lt;/td&gt;
					&lt;td&gt;调用方竞态&lt;/td&gt;
					&lt;td&gt;统一异步边界；错误路径也 &lt;code&gt;nextTick&lt;/code&gt;/&lt;code&gt;queueMicrotask&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;不要阻塞 Event Loop 的检查清单（摘自官方思路）：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个回调的计算复杂度是否随用户输入线性/超线性膨胀？是否有上限校验？&lt;/li&gt;
&lt;li&gt;是否存在嵌套量词正则（REDOS）？简单包含用 &lt;code&gt;indexOf&lt;/code&gt;/&lt;code&gt;includes&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;大对象是否在主线程 &lt;code&gt;JSON.parse&lt;/code&gt; / &lt;code&gt;stringify&lt;/code&gt;？能否限制体积或卸载到 Worker？&lt;/li&gt;
&lt;li&gt;是否误用 &lt;code&gt;*Sync&lt;/code&gt; 文件/加密 API 于请求路径？&lt;/li&gt;
&lt;li&gt;需要“下一轮再继续”时，默认优先 &lt;code&gt;setImmediate&lt;/code&gt; / &lt;code&gt;queueMicrotask&lt;/code&gt;，慎用无限 &lt;code&gt;nextTick&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;分区（Partitioning）示例思路&lt;/strong&gt;：把 O(n) 求和拆成多步，每步 O(1)，用 &lt;code&gt;setImmediate&lt;/code&gt;/&lt;code&gt;queueMicrotask&lt;/code&gt; 让出循环，避免单次回调霸占主线程。更重的任务则应 &lt;strong&gt;Offloading&lt;/strong&gt; 到 Worker（&lt;code&gt;worker_threads&lt;/code&gt;、独立进程或 C++/N-API 任务），而不是只靠在主循环里切片。&lt;/p&gt;
&lt;h2 id="五总结"&gt;&lt;a href="#%e4%ba%94%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;五、总结
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Node 的并发模型是 &lt;strong&gt;单线程 Event Loop + libuv Worker Pool&lt;/strong&gt;：I/O 与任务要“碎、短、可让出”。&lt;/li&gt;
&lt;li&gt;相位队列决定 &lt;strong&gt;timers / poll / check / close&lt;/strong&gt; 等回调归属；&lt;strong&gt;nextTick 与 microtask 是阶段之间的优先通道&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;I/O 回调内&lt;/strong&gt; &lt;code&gt;setImmediate&lt;/code&gt; 稳定早于 &lt;code&gt;setTimeout(0)&lt;/code&gt;；主模块顶层两者顺序不稳定。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nextTick&lt;/code&gt; 适合“栈展开后、循环前进前”的 API 契约；滥用会饿死 I/O。新代码多数场景优先 &lt;strong&gt;&lt;code&gt;queueMicrotask&lt;/code&gt; / &lt;code&gt;setImmediate&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;性能与安全同一条线：限制输入、避免 REDOS、大 JSON 与 CPU 重活卸载，才能让“少线程高并发”真正成立。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;把调度图画进团队约定后，再写中间件、ORM 钩子、连接池回调，会少掉一大类“玄学时序 bug”。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;a class="link" href="https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick" target="_blank" rel="noopener"
 &gt;The Node.js Event Loop&lt;/a&gt; — Node.js Learn（阶段图、poll/check/timers、&lt;code&gt;setImmediate&lt;/code&gt; vs &lt;code&gt;setTimeout&lt;/code&gt;、&lt;code&gt;process.nextTick&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nodejs.org/en/learn/asynchronous-work/dont-block-the-event-loop" target="_blank" rel="noopener"
 &gt;Don&amp;rsquo;t Block the Event Loop (or the Worker Pool)&lt;/a&gt; — Node.js Learn（Event Loop vs Worker Pool、阻塞危害、分区/卸载、REDOS/JSON）&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nodejs.org/en/learn/asynchronous-work/understanding-processnexttick" target="_blank" rel="noopener"
 &gt;Understanding process.nextTick()&lt;/a&gt; — Node.js Learn&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nodejs.org/api/process.html#processnexttickcallback-args" target="_blank" rel="noopener"
 &gt;&lt;code&gt;process.nextTick&lt;/code&gt; / &lt;code&gt;queueMicrotask&lt;/code&gt; 对比&lt;/a&gt; — Node.js API（CJS/ESM 次序、Legacy 说明）&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://nodejs.org/api/timers.html" target="_blank" rel="noopener"
 &gt;Timers&lt;/a&gt; — Node.js API（&lt;code&gt;setImmediate&lt;/code&gt; / &lt;code&gt;setTimeout&lt;/code&gt;、ref/unref）&lt;/li&gt;
&lt;li&gt;源码文档镜像：&lt;a class="link" href="https://raw.githubusercontent.com/nodejs/node/main/doc/api/process.md" target="_blank" rel="noopener"
 &gt;nodejs/node &lt;code&gt;doc/api/process.md&lt;/code&gt;&lt;/a&gt;、&lt;a class="link" href="https://raw.githubusercontent.com/nodejs/node/main/doc/api/timers.md" target="_blank" rel="noopener"
 &gt;&lt;code&gt;doc/api/timers.md&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>Spring AI Alibaba 实战：用 Supervisor 构建可持久化的 Java 多智能体助手</title><link>https://blog.waihost.com/posts/spring-ai-alibaba-multi-agent-supervisor/</link><pubDate>Thu, 23 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/spring-ai-alibaba-multi-agent-supervisor/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/spring-ai-alibaba-multi-agent-supervisor.svg" alt="Featured image of post Spring AI Alibaba 实战：用 Supervisor 构建可持久化的 Java 多智能体助手" /&gt;&lt;p&gt;很多 Java Agent 示例只演示“模型调用一个工具”，但真实业务通常横跨多个边界：日历 Agent 理解时间并检查冲突，邮件 Agent 负责收件人和正文，协调者还要决定调用顺序、传递结果，并在失败后恢复。&lt;/p&gt;
&lt;p&gt;本文基于 Spring AI Alibaba 官方稳定依赖线 &lt;code&gt;1.1.2.2&lt;/code&gt;，实现一个 &lt;strong&gt;Supervisor + 专业子 Agent&lt;/strong&gt; 的个人助理。重点不是某个模型的聊天接口，而是四个可复用的工程问题：如何划分 Agent 边界、怎样把子 Agent 暴露成工具、如何保存执行状态，以及如何控制有副作用的操作。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;本文代码结构依据官方 &lt;code&gt;examples/multiagent-patterns/supervisor&lt;/code&gt; 示例整理。Spring AI Alibaba 同时存在面向 Spring AI 2.0 / Spring Boot 4 的 &lt;code&gt;2.0.0-M1.1&lt;/code&gt; 里程碑版本；生产项目不要把 milestone 与本文稳定线混用。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="一为什么需要-supervisor"&gt;&lt;a href="#%e4%b8%80%e4%b8%ba%e4%bb%80%e4%b9%88%e9%9c%80%e8%a6%81-supervisor" class="header-anchor"&gt;&lt;/a&gt;一、为什么需要 Supervisor
&lt;/h2&gt;&lt;p&gt;多 Agent 并不等于“把四个聊天机器人放进一个群”。一个可控系统至少要区分三层：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;层次&lt;/th&gt;
					&lt;th&gt;职责&lt;/th&gt;
					&lt;th&gt;本文对应实现&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;决策层&lt;/td&gt;
					&lt;td&gt;拆解请求、选择能力、安排调用顺序&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;personal_assistant&lt;/code&gt; Supervisor&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;专业层&lt;/td&gt;
					&lt;td&gt;在受限领域内推理和调用工具&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;schedule_event&lt;/code&gt;、&lt;code&gt;manage_email&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;执行层&lt;/td&gt;
					&lt;td&gt;访问日历、邮件、数据库等真实系统&lt;/td&gt;
					&lt;td&gt;Java &lt;code&gt;@Tool&lt;/code&gt; 方法&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;用户说“下周二开评审会，并给设计组发提醒”时，Supervisor 先调用日历 Agent；拿到时间和创建结果后，再把必要信息交给邮件 Agent。专业 Agent 看不到无关工具，工具层也不承担自然语言规划。&lt;/p&gt;
&lt;p&gt;这种设计的价值是 &lt;strong&gt;缩小每次模型决策的工具集合&lt;/strong&gt;。如果把查日历、建会议、发邮件、查工单、改数据库等几十个工具全部交给一个 Agent，模型更容易选错工具，权限也难以分层。&lt;/p&gt;
&lt;h2 id="二环境与依赖"&gt;&lt;a href="#%e4%ba%8c%e7%8e%af%e5%a2%83%e4%b8%8e%e4%be%9d%e8%b5%96" class="header-anchor"&gt;&lt;/a&gt;二、环境与依赖
&lt;/h2&gt;&lt;p&gt;官方稳定示例使用 JDK 17、Spring Boot 3.5.7、Spring AI 1.1.2 和 Spring AI Alibaba 1.1.2.2：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;parent&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-parent&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;3.5.7&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/parent&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;properties&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;java.version&amp;gt;&lt;/span&gt;17&lt;span class="nt"&gt;&amp;lt;/java.version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;spring-ai.version&amp;gt;&lt;/span&gt;1.1.2&lt;span class="nt"&gt;&amp;lt;/spring-ai.version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;spring-ai-alibaba.version&amp;gt;&lt;/span&gt;1.1.2.2&lt;span class="nt"&gt;&amp;lt;/spring-ai-alibaba.version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/properties&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;dependencyManagement&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.ai&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-ai-bom&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${spring-ai.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;type&amp;gt;&lt;/span&gt;pom&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;import&lt;span class="nt"&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.alibaba.cloud.ai&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-ai-alibaba-bom&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${spring-ai-alibaba.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;type&amp;gt;&lt;/span&gt;pom&lt;span class="nt"&gt;&amp;lt;/type&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;scope&amp;gt;&lt;/span&gt;import&lt;span class="nt"&gt;&amp;lt;/scope&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/dependencyManagement&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.alibaba.cloud.ai&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-ai-alibaba-starter-dashscope&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.alibaba.cloud.ai&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-ai-alibaba-agent-framework&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;配置密钥时不要写进仓库：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;dashscope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;api-key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${AI_DASHSCOPE_API_KEY}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;运行前设置环境变量：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;AI_DASHSCOPE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;你的密钥&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果换成其他 Spring AI &lt;code&gt;ChatModel&lt;/code&gt;，Agent 编排代码本身可以保持不变，但工具调用、结构化输出和并行调用能力仍取决于具体模型。&lt;/p&gt;
&lt;h2 id="三先写最窄的业务工具"&gt;&lt;a href="#%e4%b8%89%e5%85%88%e5%86%99%e6%9c%80%e7%aa%84%e7%9a%84%e4%b8%9a%e5%8a%a1%e5%b7%a5%e5%85%b7" class="header-anchor"&gt;&lt;/a&gt;三、先写最窄的业务工具
&lt;/h2&gt;&lt;p&gt;工具的参数和描述会进入模型上下文。名称模糊、参数万能化，会直接降低调用准确率。下面用内存实现代替真实日历系统：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CalendarTools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;检查指定 ISO-8601 起止时间内是否有空闲时段&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;getAvailableTimeSlots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@ToolParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;开始时间，例如 2026-07-28T14:00:00+08:00&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@ToolParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;结束时间，例如 2026-07-28T15:00:00+08:00&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;AVAILABLE: &amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34; ~ &amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;创建日历事件；调用前必须已经确认时间可用&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;createCalendarEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@ToolParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;事件标题&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@ToolParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ISO-8601 开始时间&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@ToolParam&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ISO-8601 结束时间&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;CREATED eventId=&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;, title=&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;邮件工具同样保持窄接口：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailTools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;发送邮件。仅接受已经确认的收件人、主题和正文&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;SENT to=&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;, subject=&amp;#34;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;演示可以直接返回字符串，生产环境必须补上：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;幂等键&lt;/strong&gt;：重试不能创建两场相同会议或发送两封邮件；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;身份鉴权&lt;/strong&gt;：工具从服务端上下文读取租户和用户，不能信任模型传入身份；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数校验&lt;/strong&gt;：邮箱白名单、时间范围、正文长度均由 Java 代码强制检查；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;审计日志&lt;/strong&gt;：记录调用者、工具名、参数摘要、结果和 trace ID；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超时与熔断&lt;/strong&gt;：模型重试不应无限放大下游故障。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="四构建两个专业-reactagent"&gt;&lt;a href="#%e5%9b%9b%e6%9e%84%e5%bb%ba%e4%b8%a4%e4%b8%aa%e4%b8%93%e4%b8%9a-reactagent" class="header-anchor"&gt;&lt;/a&gt;四、构建两个专业 ReactAgent
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;ReactAgent&lt;/code&gt; 负责“思考—选择工具—观察结果—继续”的循环。每个专业 Agent 只注册本领域工具：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Configuration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AgentConfiguration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MemorySaver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;memorySaver&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MemorySaver&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ReactAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;calendarAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChatModel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CalendarTools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ReactAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;schedule_event&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;检查空闲时间并创建日历事件，输入为自然语言日程请求&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 你是日历助理。先把时间转换为带时区的 ISO-8601 格式，
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 再检查空闲时段；只有可用时才创建事件。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 最终回答必须包含事件 ID 和确认后的起止时间。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;methodTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inputType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ReactAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;emailAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChatModel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;EmailTools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ReactAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;manage_email&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;根据明确的收件人和上下文撰写并发送邮件&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 你是邮件助理。不得猜测收件地址；缺少地址就要求补充。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 发送前检查主题、正文和会议时间是否一致。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 最终回答给出发送状态，不输出敏感凭据。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;methodTools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;inputType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里的 &lt;code&gt;name&lt;/code&gt; 和 &lt;code&gt;description&lt;/code&gt; 不是装饰字段。Supervisor 会根据它们决定是否调用子 Agent，因此描述要写清楚 &lt;strong&gt;何时调用、输入是什么、能产生什么结果&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="五把子-agent-变成-supervisor-的工具"&gt;&lt;a href="#%e4%ba%94%e6%8a%8a%e5%ad%90-agent-%e5%8f%98%e6%88%90-supervisor-%e7%9a%84%e5%b7%a5%e5%85%b7" class="header-anchor"&gt;&lt;/a&gt;五、把子 Agent 变成 Supervisor 的工具
&lt;/h2&gt;&lt;p&gt;Spring AI Alibaba 提供 &lt;code&gt;AgentTool.getFunctionToolCallback(...)&lt;/code&gt;，可以把整个专业 Agent 包装成上层 Agent 可调用的工具：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ReactAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;supervisorAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ChatModel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ReactAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;calendarAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ReactAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;emailAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;MemorySaver&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;memorySaver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ReactAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;personal_assistant&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;systemPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 你是任务协调者，可以安排日历和发送邮件。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 先拆解请求，再按依赖顺序调用专业工具。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 邮件依赖会议结果时，必须先完成日历操作。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; 不要声称执行了未返回成功结果的操作。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;memorySaver&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AgentTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFunctionToolCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;calendarAgent&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AgentTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getFunctionToolCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emailAgent&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;调用入口非常直接：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DemoRunner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;implements&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ApplicationRunner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ReactAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;supervisorAgent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;DemoRunner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ReactAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;supervisorAgent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supervisorAgent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;supervisorAgent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ApplicationArguments&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;throws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;下周二 14:00 安排一小时设计评审，&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;然后给 design@example.com 发一封提醒邮件&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AssistantMessage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;supervisorAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getText&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一次典型执行链是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;User
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; Supervisor
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; schedule_event
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; get_available_time_slots
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; create_calendar_event
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; manage_email
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; send_email
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -&amp;gt; 汇总最终结果
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;子 Agent 是工具，但它内部仍然可以执行多轮 ReAct。这样既保留自治能力，又把自治限制在专业边界内。&lt;/p&gt;
&lt;h2 id="六状态持久化不等于聊天记忆"&gt;&lt;a href="#%e5%85%ad%e7%8a%b6%e6%80%81%e6%8c%81%e4%b9%85%e5%8c%96%e4%b8%8d%e7%ad%89%e4%ba%8e%e8%81%8a%e5%a4%a9%e8%ae%b0%e5%bf%86" class="header-anchor"&gt;&lt;/a&gt;六、状态持久化不等于聊天记忆
&lt;/h2&gt;&lt;p&gt;示例中的 &lt;code&gt;MemorySaver&lt;/code&gt; 适合本地开发和单进程验证。它保存的是图执行所需的 checkpoint，使 Agent 能围绕同一线程继续运行；它不是生产级共享数据库，也不能替代业务幂等。&lt;/p&gt;
&lt;p&gt;工程上要区分三类状态：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;状态&lt;/th&gt;
					&lt;th&gt;示例&lt;/th&gt;
					&lt;th&gt;推荐存储&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;会话上下文&lt;/td&gt;
					&lt;td&gt;用户偏好、已确认的时间&lt;/td&gt;
					&lt;td&gt;Chat Memory / 会话库&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Agent checkpoint&lt;/td&gt;
					&lt;td&gt;当前节点、工具结果、待恢复位置&lt;/td&gt;
					&lt;td&gt;持久化 Saver&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;业务事实&lt;/td&gt;
					&lt;td&gt;已创建会议、邮件发送记录&lt;/td&gt;
					&lt;td&gt;业务数据库，带唯一约束&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;即便 checkpoint 恢复成功，若工具在崩溃前已经写入业务系统、但结果还没写回 checkpoint，恢复后仍可能重复调用。因此可靠流程要让工具接受 &lt;code&gt;operationId&lt;/code&gt;，并在业务库建立唯一约束。&lt;/p&gt;
&lt;h2 id="七给副作用加一道人工确认"&gt;&lt;a href="#%e4%b8%83%e7%bb%99%e5%89%af%e4%bd%9c%e7%94%a8%e5%8a%a0%e4%b8%80%e9%81%93%e4%ba%ba%e5%b7%a5%e7%a1%ae%e8%ae%a4" class="header-anchor"&gt;&lt;/a&gt;七、给副作用加一道人工确认
&lt;/h2&gt;&lt;p&gt;会议和邮件都属于有副作用操作。最稳妥的方式不是只在 Prompt 中写“请先确认”，而是在工具层建立状态机：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DRAFT -&amp;gt; WAITING_APPROVAL -&amp;gt; EXECUTING -&amp;gt; SUCCEEDED / FAILED
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Agent 第一次调用只生成草稿并返回 &lt;code&gt;approvalId&lt;/code&gt;；API 层收到用户确认后，才调用真正的发送方法。这样即使模型忽略提示，也没有直接执行权限。&lt;/p&gt;
&lt;p&gt;还可以进一步拆成两个工具：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;prepare_email(...)&lt;/code&gt;：任何 Agent 都能调用，只生成草稿；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;send_approved_email(approvalId)&lt;/code&gt;：不接受正文，只执行服务端已经批准且未过期的草稿。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这比让模型把 &lt;code&gt;approved=true&lt;/code&gt; 当参数传进来安全得多。&lt;/p&gt;
&lt;h2 id="八常见问题与排查"&gt;&lt;a href="#%e5%85%ab%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98%e4%b8%8e%e6%8e%92%e6%9f%a5" class="header-anchor"&gt;&lt;/a&gt;八、常见问题与排查
&lt;/h2&gt;&lt;h3 id="1-子-agent-从不被调用"&gt;&lt;a href="#1-%e5%ad%90-agent-%e4%bb%8e%e4%b8%8d%e8%a2%ab%e8%b0%83%e7%94%a8" class="header-anchor"&gt;&lt;/a&gt;1. 子 Agent 从不被调用
&lt;/h3&gt;&lt;p&gt;检查 &lt;code&gt;name&lt;/code&gt;、&lt;code&gt;description&lt;/code&gt; 是否明确，并确认模型支持 Tool Calling。描述不要只写“邮件助手”，而要说明输入和使用时机。&lt;/p&gt;
&lt;h3 id="2-依赖冲突或类找不到"&gt;&lt;a href="#2-%e4%be%9d%e8%b5%96%e5%86%b2%e7%aa%81%e6%88%96%e7%b1%bb%e6%89%be%e4%b8%8d%e5%88%b0" class="header-anchor"&gt;&lt;/a&gt;2. 依赖冲突或类找不到
&lt;/h3&gt;&lt;p&gt;不要混用 Spring AI 1.1.x、Spring AI Alibaba 1.1.x 与 2.0 milestone。使用 BOM 统一版本，并执行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mvn dependency:tree
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;重点检查 &lt;code&gt;spring-ai-*&lt;/code&gt; 是否出现多条不兼容版本线。&lt;/p&gt;
&lt;h3 id="3-时间被理解错"&gt;&lt;a href="#3-%e6%97%b6%e9%97%b4%e8%a2%ab%e7%90%86%e8%a7%a3%e9%94%99" class="header-anchor"&gt;&lt;/a&gt;3. 时间被理解错
&lt;/h3&gt;&lt;p&gt;“明天下午”必须结合用户时区和当前日期解析。生产系统应把当前时间、时区作为可信上下文注入，并在工具层拒绝无时区或已经过期的时间。&lt;/p&gt;
&lt;h3 id="4-重启后任务消失"&gt;&lt;a href="#4-%e9%87%8d%e5%90%af%e5%90%8e%e4%bb%bb%e5%8a%a1%e6%b6%88%e5%a4%b1" class="header-anchor"&gt;&lt;/a&gt;4. 重启后任务消失
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;MemorySaver&lt;/code&gt; 只在进程内。多实例部署需要共享持久化 Saver，同时工具仍要做幂等；二者缺一不可。&lt;/p&gt;
&lt;h3 id="5-最终回答说成功业务却没有记录"&gt;&lt;a href="#5-%e6%9c%80%e7%bb%88%e5%9b%9e%e7%ad%94%e8%af%b4%e6%88%90%e5%8a%9f%e4%b8%9a%e5%8a%a1%e5%8d%b4%e6%b2%a1%e6%9c%89%e8%ae%b0%e5%bd%95" class="header-anchor"&gt;&lt;/a&gt;5. 最终回答说成功，业务却没有记录
&lt;/h3&gt;&lt;p&gt;只信工具返回的结构化结果，不信模型自行生成的成功话术。建议工具返回包含 &lt;code&gt;status&lt;/code&gt;、&lt;code&gt;operationId&lt;/code&gt;、&lt;code&gt;errorCode&lt;/code&gt; 的对象，并在最终输出前增加确定性校验节点。&lt;/p&gt;
&lt;h2 id="九什么时候不用多-agent"&gt;&lt;a href="#%e4%b9%9d%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e4%b8%8d%e7%94%a8%e5%a4%9a-agent" class="header-anchor"&gt;&lt;/a&gt;九、什么时候不用多 Agent
&lt;/h2&gt;&lt;p&gt;如果任务只有一个模型、三个简单只读工具和一次调用，单个 &lt;code&gt;ReactAgent&lt;/code&gt; 往往更便宜、更快，也更容易测试。只有出现以下情况，多 Agent 才真正有价值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不同领域需要不同 Prompt、工具权限或模型；&lt;/li&gt;
&lt;li&gt;任务能拆成可独立测试的专业能力；&lt;/li&gt;
&lt;li&gt;子任务需要并行、路由或多阶段交接；&lt;/li&gt;
&lt;li&gt;某些 Agent 必须运行在隔离环境或独立服务中；&lt;/li&gt;
&lt;li&gt;团队希望分别维护日历、邮件、工单等能力。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要用 Agent 替代确定性代码。固定审批顺序、金额阈值和权限检查应该写进 Java 与工作流，而不是交给 LLM 临场决定。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;Spring AI Alibaba 的 Supervisor 模式可以概括为：&lt;strong&gt;上层 Agent 负责选择和协调，下层 Agent 负责专业推理，Java 工具负责可信执行，Saver 负责恢复上下文&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;真正决定系统能否进入生产的并不是 Agent 数量，而是边界是否清楚：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个专业 Agent 只看到必要工具；&lt;/li&gt;
&lt;li&gt;有副作用操作经过服务端审批和幂等保护；&lt;/li&gt;
&lt;li&gt;checkpoint、会话记忆与业务事实分开保存；&lt;/li&gt;
&lt;li&gt;版本通过 BOM 锁定，不混用稳定线和 milestone；&lt;/li&gt;
&lt;li&gt;最终成功状态由真实工具结果而不是模型话术决定。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在这个基础上，再引入 &lt;code&gt;ParallelAgent&lt;/code&gt;、&lt;code&gt;RoutingAgent&lt;/code&gt;、Graph 条件边、A2A 或 Nacos，复杂度才是可控增长，而不是把一个不可预测的大 Agent 拆成一群不可预测的小 Agent。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/alibaba/spring-ai-alibaba" target="_blank" rel="noopener"
 &gt;Spring AI Alibaba GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://java2ai.com/docs/overview" target="_blank" rel="noopener"
 &gt;Spring AI Alibaba 官方文档&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://github.com/alibaba/spring-ai-alibaba/tree/main/examples/multiagent-patterns/supervisor" target="_blank" rel="noopener"
 &gt;官方 Supervisor 示例&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://java2ai.com/docs/frameworks/agent-framework/tutorials/agents" target="_blank" rel="noopener"
 &gt;Agent Framework 教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.spring.io/spring-ai/reference/api/tools.html" target="_blank" rel="noopener"
 &gt;Spring AI Tool Calling&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Python asyncio 事件循环与 Task 取消：原理、API 与工程实践</title><link>https://blog.waihost.com/posts/python-asyncio-event-loop-task-cancellation/</link><pubDate>Mon, 20 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/python-asyncio-event-loop-task-cancellation/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/python-asyncio-event-loop-task-cancellation.svg" alt="Featured image of post Python asyncio 事件循环与 Task 取消：原理、API 与工程实践" /&gt;&lt;p&gt;高并发 HTTP 客户端、WebSocket 网关、爬虫调度、微服务侧车——Python 侧大量 IO 密集型场景会落到 &lt;strong&gt;asyncio&lt;/strong&gt;。写得不好时，常见症状是：协程“创建了却从不调度”、后台 Task 被 GC 悄悄干掉、超时后资源不释放、&lt;code&gt;CancelledError&lt;/code&gt; 被误吞导致 &lt;code&gt;TaskGroup&lt;/code&gt; / &lt;code&gt;timeout&lt;/code&gt; 行为异常。&lt;/p&gt;
&lt;p&gt;本文基于官方文档 &lt;em&gt;asyncio — Asynchronous I/O&lt;/em&gt;、&lt;em&gt;Coroutines and Tasks&lt;/em&gt;、&lt;em&gt;Runners&lt;/em&gt;、&lt;em&gt;Developing with asyncio&lt;/em&gt;，以及 PEP 492，把 &lt;strong&gt;事件循环协作调度、Task 生命周期、结构化并发、取消与超时&lt;/strong&gt; 讲清楚，并给出可复现的写法与排障清单。&lt;/p&gt;
&lt;h2 id="一问题背景并发不等于多线程"&gt;&lt;a href="#%e4%b8%80%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af%e5%b9%b6%e5%8f%91%e4%b8%8d%e7%ad%89%e4%ba%8e%e5%a4%9a%e7%ba%bf%e7%a8%8b" class="header-anchor"&gt;&lt;/a&gt;一、问题背景：并发不等于多线程
&lt;/h2&gt;&lt;p&gt;asyncio 的定位很明确：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;asyncio is a library to write concurrent code using the async/await syntax.&lt;br&gt;
asyncio is often a perfect fit for &lt;strong&gt;IO-bound&lt;/strong&gt; and high-level structured network code.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;它解决的是“大量等待网络/磁盘时如何让 CPU 去干别的活”，而不是用多核并行跑 CPU 密集计算。事件循环通常跑在 &lt;strong&gt;一个线程&lt;/strong&gt; 内：某个 Task 在执行同步代码时，&lt;strong&gt;同线程的其他 Task 无法推进&lt;/strong&gt;；只有执行到 &lt;code&gt;await&lt;/code&gt;，当前 Task 挂起，循环才调度下一个。&lt;/p&gt;
&lt;p&gt;若把 &lt;code&gt;time.sleep&lt;/code&gt;、同步 &lt;code&gt;requests&lt;/code&gt;、重型 JSON/CPU 计算直接塞进协程，等价于堵死整条事件循环。官方开发指南的原则是：阻塞工作放到线程池（&lt;code&gt;asyncio.to_thread&lt;/code&gt; / &lt;code&gt;loop.run_in_executor&lt;/code&gt;），循环线程只做非阻塞 IO 与调度。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;概念&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;工程后果&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;协程函数&lt;/strong&gt; &lt;code&gt;async def&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;调用后得到协程对象，&lt;strong&gt;不会自动运行&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;只写 &lt;code&gt;foo()&lt;/code&gt; 而不 &lt;code&gt;await&lt;/code&gt;/&lt;code&gt;create_task&lt;/code&gt; 会 RuntimeWarning&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Awaitable&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;可被 &lt;code&gt;await&lt;/code&gt; 的对象&lt;/td&gt;
					&lt;td&gt;三类主路径：协程、Task、Future&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Event loop&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;调度 Task/回调、做网络 IO、跑子进程&lt;/td&gt;
					&lt;td&gt;应用层优先用 &lt;code&gt;asyncio.run&lt;/code&gt;，少直接操作 loop&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Task&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;被调度执行的协程包装&lt;/td&gt;
					&lt;td&gt;可取消、可聚合、可命名/追踪&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="二核心模型协作式调度--弱引用-task"&gt;&lt;a href="#%e4%ba%8c%e6%a0%b8%e5%bf%83%e6%a8%a1%e5%9e%8b%e5%8d%8f%e4%bd%9c%e5%bc%8f%e8%b0%83%e5%ba%a6--%e5%bc%b1%e5%bc%95%e7%94%a8-task" class="header-anchor"&gt;&lt;/a&gt;二、核心模型：协作式调度 + 弱引用 Task
&lt;/h2&gt;&lt;h3 id="1-必须跑起来而不能只创建"&gt;&lt;a href="#1-%e5%bf%85%e9%a1%bb%e8%b7%91%e8%b5%b7%e6%9d%a5%e8%80%8c%e4%b8%8d%e8%83%bd%e5%8f%aa%e5%88%9b%e5%bb%ba" class="header-anchor"&gt;&lt;/a&gt;1. 必须“跑起来”，而不能只“创建”
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;asyncio&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;world&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;官方示例同时强调反例：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;coroutine&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;...&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;单纯调用协程 &lt;strong&gt;不会&lt;/strong&gt; 把它排进事件循环。入口应使用 &lt;code&gt;asyncio.run(coro)&lt;/code&gt;：它负责创建/关闭循环、收尾异步生成器、关闭默认 executor。&lt;strong&gt;同一线程已有运行中的 loop 时不能再调用 &lt;code&gt;asyncio.run&lt;/code&gt;&lt;/strong&gt;（嵌套场景用 &lt;code&gt;asyncio.Runner&lt;/code&gt; 或上层框架提供的生命周期）。&lt;/p&gt;
&lt;h3 id="2-并发来自-task不是多写几个-await"&gt;&lt;a href="#2-%e5%b9%b6%e5%8f%91%e6%9d%a5%e8%87%aa-task%e4%b8%8d%e6%98%af%e5%a4%9a%e5%86%99%e5%87%a0%e4%b8%aa-await" class="header-anchor"&gt;&lt;/a&gt;2. 并发来自 Task，不是“多写几个 await”
&lt;/h3&gt;&lt;p&gt;串行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;say_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;say_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;world&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 总耗时约 3s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;并发：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;task1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;say_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;task2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;say_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;world&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task2&lt;/span&gt; &lt;span class="c1"&gt;# 总耗时约 2s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;create_task&lt;/code&gt; 把协程包成 Task 并立即调度。一个极易踩坑的细节（官方明确写出）：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;The event loop only keeps &lt;strong&gt;weak references&lt;/strong&gt; to tasks. A task that isn’t referenced elsewhere may get garbage collected at any time, even before it’s done.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;“发后不管”的后台任务必须放进集合，并在 &lt;code&gt;done&lt;/code&gt; 回调里移除：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;background_tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;background_tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_done_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;background_tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;discard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="3-结构化并发优先-taskgroup311"&gt;&lt;a href="#3-%e7%bb%93%e6%9e%84%e5%8c%96%e5%b9%b6%e5%8f%91%e4%bc%98%e5%85%88-taskgroup311" class="header-anchor"&gt;&lt;/a&gt;3. 结构化并发：优先 &lt;code&gt;TaskGroup&lt;/code&gt;（3.11+）
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;asyncio.TaskGroup&lt;/code&gt; 把“创建 + 等待全部结束”绑在异步上下文管理器里：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TaskGroup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;say_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;say_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;world&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 退出 with 时，组内任务均已结束&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;组内若有任务抛出非 &lt;code&gt;CancelledError&lt;/code&gt; 的异常，会汇总为 &lt;code&gt;ExceptionGroup&lt;/code&gt; / &lt;code&gt;BaseExceptionGroup&lt;/code&gt; 再抛出。相对“手写 &lt;code&gt;create_task&lt;/code&gt; + 忘记 &lt;code&gt;await&lt;/code&gt;”，&lt;code&gt;TaskGroup&lt;/code&gt; 更不容易留下孤儿任务。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;API&lt;/th&gt;
					&lt;th&gt;典型用途&lt;/th&gt;
					&lt;th&gt;注意点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;create_task&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;灵活并发、需要 Task 句柄&lt;/td&gt;
					&lt;td&gt;保持强引用；自行处理汇合&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;TaskGroup&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;结构化并发、统一收口&lt;/td&gt;
					&lt;td&gt;异常变为 ExceptionGroup；依赖取消语义正确&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;gather(*aws, return_exceptions=False)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;固定一批 awaitable 一起等&lt;/td&gt;
					&lt;td&gt;默认某任务异常会影响整体策略；结果顺序与入参一致&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;wait(aws, return_when=...)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;需要 &lt;code&gt;done/pending&lt;/code&gt; 集合或 FIRST_COMPLETED&lt;/td&gt;
					&lt;td&gt;入参必须是 Future/Task 集合，不能是裸协程习惯用法&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;gather&lt;/code&gt; 适合“固定 N 路、要按入参顺序拿结果”：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;return_exceptions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# 单个失败不中断其余；结果里可能是 Exception 实例&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;return_exceptions=False&lt;/code&gt;（默认）时，任一 awaitable 以异常结束，&lt;code&gt;gather&lt;/code&gt; 会把该异常传播给调用方（其余任务的收尾策略仍需按版本与场景理解，工程上更稳妥的是用 &lt;code&gt;TaskGroup&lt;/code&gt; 明确“有错就结构化失败”）。&lt;code&gt;wait&lt;/code&gt; 则返回 &lt;code&gt;(done, pending)&lt;/code&gt;，并可用 &lt;code&gt;FIRST_COMPLETED&lt;/code&gt; / &lt;code&gt;FIRST_EXCEPTION&lt;/code&gt; / &lt;code&gt;ALL_COMPLETED&lt;/code&gt; 控制返回时机，适合“谁先完成谁处理、其余取消”的竞态选路。&lt;/p&gt;
&lt;h2 id="三取消与超时cancellederror-是协作信号"&gt;&lt;a href="#%e4%b8%89%e5%8f%96%e6%b6%88%e4%b8%8e%e8%b6%85%e6%97%b6cancellederror-%e6%98%af%e5%8d%8f%e4%bd%9c%e4%bf%a1%e5%8f%b7" class="header-anchor"&gt;&lt;/a&gt;三、取消与超时：CancelledError 是协作信号
&lt;/h2&gt;&lt;h3 id="1-取消发生在下一次机会"&gt;&lt;a href="#1-%e5%8f%96%e6%b6%88%e5%8f%91%e7%94%9f%e5%9c%a8%e4%b8%8b%e4%b8%80%e6%ac%a1%e6%9c%ba%e4%bc%9a" class="header-anchor"&gt;&lt;/a&gt;1. 取消发生在“下一次机会”
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;task.cancel()&lt;/code&gt; 请求取消后，目标 Task 会在 &lt;strong&gt;下一次可取消点&lt;/strong&gt;（通常是 &lt;code&gt;await&lt;/code&gt;）注入 &lt;code&gt;asyncio.CancelledError&lt;/code&gt;。官方建议：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;用 &lt;code&gt;try/finally&lt;/code&gt; 做资源清理（关连接、放回连接池、落盘状态）；&lt;/li&gt;
&lt;li&gt;若显式 &lt;code&gt;except CancelledError&lt;/code&gt;，清理完成后 &lt;strong&gt;一般应继续抛出&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CancelledError&lt;/code&gt; &lt;strong&gt;直接继承 &lt;code&gt;BaseException&lt;/code&gt;&lt;/strong&gt;，因此普通 &lt;code&gt;except Exception&lt;/code&gt; 抓不到它——这是刻意设计。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;吞掉取消会破坏结构化并发组件：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;asyncio.TaskGroup and asyncio.timeout() are implemented using cancellation internally and might misbehave if a coroutine swallows asyncio.CancelledError.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="2-超时wait_for-与-timeout-上下文"&gt;&lt;a href="#2-%e8%b6%85%e6%97%b6wait_for-%e4%b8%8e-timeout-%e4%b8%8a%e4%b8%8b%e6%96%87" class="header-anchor"&gt;&lt;/a&gt;2. 超时：&lt;code&gt;wait_for&lt;/code&gt; 与 &lt;code&gt;timeout&lt;/code&gt; 上下文
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 超时后取消 awaitable，并抛 TimeoutError&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;long_job&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;wait_for&lt;/code&gt; 在超时路径会 &lt;strong&gt;取消&lt;/strong&gt; 目标任务；为避免取消可包 &lt;code&gt;shield&lt;/code&gt;（见下）。函数会等到取消真正完成，因此总等待可能略大于 &lt;code&gt;timeout&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;3.11+ 更推荐上下文管理器写法：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;long_running_task&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# TimeoutError 只能在 with 之外捕获&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;timeout&lt;/code&gt; 会取消 &lt;strong&gt;当前 Task&lt;/strong&gt;，内部把 &lt;code&gt;CancelledError&lt;/code&gt; 转成 &lt;code&gt;TimeoutError&lt;/code&gt;。&lt;code&gt;timeout_at(when)&lt;/code&gt; 用绝对 deadline，适合与外部截止时间对齐。&lt;/p&gt;
&lt;h3 id="3-shield挡外层取消不挡内层自杀"&gt;&lt;a href="#3-shield%e6%8c%a1%e5%a4%96%e5%b1%82%e5%8f%96%e6%b6%88%e4%b8%8d%e6%8c%a1%e5%86%85%e5%b1%82%e8%87%aa%e6%9d%80" class="header-anchor"&gt;&lt;/a&gt;3. &lt;code&gt;shield&lt;/code&gt;：挡外层取消，不挡内层自杀
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shield&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;critical_section&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;外层 Task 被取消时，&lt;code&gt;critical_section&lt;/code&gt; 对应 Task &lt;strong&gt;不一定&lt;/strong&gt; 被取消；但从调用方看，&lt;code&gt;await&lt;/code&gt; 仍会抛 &lt;code&gt;CancelledError&lt;/code&gt;。若 &lt;code&gt;critical_section&lt;/code&gt; 自己取消自己，&lt;code&gt;shield&lt;/code&gt; 挡不住。完全“无视取消”通常不推荐——多数场景应清理后退出。&lt;/p&gt;
&lt;h3 id="4-可复现小实验串行-vs-并发-vs-超时取消"&gt;&lt;a href="#4-%e5%8f%af%e5%a4%8d%e7%8e%b0%e5%b0%8f%e5%ae%9e%e9%aa%8c%e4%b8%b2%e8%a1%8c-vs-%e5%b9%b6%e5%8f%91-vs-%e8%b6%85%e6%97%b6%e5%8f%96%e6%b6%88" class="header-anchor"&gt;&lt;/a&gt;4. 可复现小实验：串行 vs 并发 vs 超时取消
&lt;/h3&gt;&lt;p&gt;把下面脚本存为 &lt;code&gt;asyncio_lab.py&lt;/code&gt;，用本机 Python 3.11+ 直接跑，对照耗时与异常类型：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;asyncio&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;time&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: start&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: done&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: cleanup&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;demo_serial&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;A&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;B&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;serial_sec=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;demo_concurrent&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TaskGroup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;A&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;B&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;concurrent_sec=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;demo_timeout_cancel&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;work&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;slow&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;got TimeoutError as expected&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;demo_swallow_cancel_bad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CancelledError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;swallowed cancel — TaskGroup/timeout 可能异常&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 反例：清理后应 re-raise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;oops&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bad_result=&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_exceptions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;demo_serial&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;demo_concurrent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;demo_timeout_cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;demo_swallow_cancel_bad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;预期现象：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;串行约 2s，并发约 1s，说明 &lt;strong&gt;await 串接 ≠ 并发&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;超时路径打印 &lt;code&gt;cleanup&lt;/code&gt; 后得到 &lt;code&gt;TimeoutError&lt;/code&gt;（&lt;code&gt;timeout&lt;/code&gt; 上下文在内部完成 &lt;code&gt;CancelledError&lt;/code&gt; 转换）；&lt;/li&gt;
&lt;li&gt;吞掉 &lt;code&gt;CancelledError&lt;/code&gt; 会让任务“以普通返回值结束”，外层若误以为已取消，容易泄漏后续逻辑。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="四工程实践从入口到排障"&gt;&lt;a href="#%e5%9b%9b%e5%b7%a5%e7%a8%8b%e5%ae%9e%e8%b7%b5%e4%bb%8e%e5%85%a5%e5%8f%a3%e5%88%b0%e6%8e%92%e9%9a%9c" class="header-anchor"&gt;&lt;/a&gt;四、工程实践：从入口到排障
&lt;/h2&gt;&lt;h3 id="1-入口与生命周期"&gt;&lt;a href="#1-%e5%85%a5%e5%8f%a3%e4%b8%8e%e7%94%9f%e5%91%bd%e5%91%a8%e6%9c%9f" class="header-anchor"&gt;&lt;/a&gt;1. 入口与生命周期
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 创建共享资源：连接池、httpx.AsyncClient 等&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 开发可 debug=True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;需要在同一 loop 多次跑顶层协程时，用 &lt;code&gt;asyncio.Runner&lt;/code&gt; 上下文，而不是嵌套 &lt;code&gt;asyncio.run&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="2-阻塞调用迁出事件循环"&gt;&lt;a href="#2-%e9%98%bb%e5%a1%9e%e8%b0%83%e7%94%a8%e8%bf%81%e5%87%ba%e4%ba%8b%e4%bb%b6%e5%be%aa%e7%8e%af" class="header-anchor"&gt;&lt;/a&gt;2. 阻塞调用迁出事件循环
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;blocking_io&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 同步 SDK / 磁盘 / 旧库&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/etc/hosts&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocking_io&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;to_thread&lt;/code&gt; 会传播 &lt;code&gt;contextvars&lt;/code&gt;，适合 IO 型阻塞；CPU 密集可考虑进程池 executor。&lt;/p&gt;
&lt;h3 id="3-取消友好的业务骨架"&gt;&lt;a href="#3-%e5%8f%96%e6%b6%88%e5%8f%8b%e5%a5%bd%e7%9a%84%e4%b8%9a%e5%8a%a1%e9%aa%a8%e6%9e%b6" class="header-anchor"&gt;&lt;/a&gt;3. 取消友好的业务骨架
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_with_cleanup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;aclose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;原则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有可能长时间 &lt;code&gt;await&lt;/code&gt; 的路径都假定可被取消；&lt;/li&gt;
&lt;li&gt;持有的锁、文件、连接在 &lt;code&gt;finally&lt;/code&gt; 释放；&lt;/li&gt;
&lt;li&gt;不要用裸 &lt;code&gt;except Exception: pass&lt;/code&gt; 包住整段协程。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4-并发扇出模板"&gt;&lt;a href="#4-%e5%b9%b6%e5%8f%91%e6%89%87%e5%87%ba%e6%a8%a1%e6%9d%bf" class="header-anchor"&gt;&lt;/a&gt;4. 并发扇出模板
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fan_out&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TaskGroup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;限流用 &lt;code&gt;Semaphore&lt;/code&gt;，汇合用 &lt;code&gt;TaskGroup&lt;/code&gt;，超时可在外层 &lt;code&gt;async with asyncio.timeout(...)&lt;/code&gt; 包住整组或单个 &lt;code&gt;one&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="5-调试开关"&gt;&lt;a href="#5-%e8%b0%83%e8%af%95%e5%bc%80%e5%85%b3" class="header-anchor"&gt;&lt;/a&gt;5. 调试开关
&lt;/h3&gt;&lt;p&gt;开发期打开 debug mode（任选其一）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;环境变量 &lt;code&gt;PYTHONASYNCIODEBUG=1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;asyncio.run(..., debug=True)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loop.set_debug(True)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;debug 能更清楚地指出 &lt;strong&gt;never-awaited coroutine&lt;/strong&gt; 的创建栈，并帮助发现慢回调。配合：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;logging&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEBUG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="6-与多线程协作时的边界"&gt;&lt;a href="#6-%e4%b8%8e%e5%a4%9a%e7%ba%bf%e7%a8%8b%e5%8d%8f%e4%bd%9c%e6%97%b6%e7%9a%84%e8%be%b9%e7%95%8c" class="header-anchor"&gt;&lt;/a&gt;6. 与多线程协作时的边界
&lt;/h3&gt;&lt;p&gt;官方 &lt;em&gt;Developing with asyncio&lt;/em&gt; 写得很清楚：事件循环在一个线程里跑所有回调与 Task；&lt;strong&gt;几乎所有 asyncio 对象都不是线程安全的&lt;/strong&gt;。从其他 OS 线程调度回调必须用 &lt;code&gt;loop.call_soon_threadsafe(...)&lt;/code&gt;；从其他线程提交协程用 &lt;code&gt;asyncio.run_coroutine_threadsafe(coro, loop)&lt;/code&gt;，它返回 &lt;code&gt;concurrent.futures.Future&lt;/code&gt;。反向地，若只是要把阻塞函数丢出循环线程，优先 &lt;code&gt;asyncio.to_thread&lt;/code&gt;，而不是自己裸开线程再回调 loop。&lt;/p&gt;
&lt;p&gt;工程约束可以记成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;一个 loop 绑定一个线程&lt;/strong&gt;（通常是主线程）；&lt;/li&gt;
&lt;li&gt;跨线程只走 threadsafe API；&lt;/li&gt;
&lt;li&gt;业务协程里不要 &lt;code&gt;threading.Lock&lt;/code&gt; 长时间占着不 &lt;code&gt;await&lt;/code&gt;——锁竞争会放大“假死”表象。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="7-http-扇出--首个成功取消其余"&gt;&lt;a href="#7-http-%e6%89%87%e5%87%ba--%e9%a6%96%e4%b8%aa%e6%88%90%e5%8a%9f%e5%8f%96%e6%b6%88%e5%85%b6%e4%bd%99" class="header-anchor"&gt;&lt;/a&gt;7. HTTP 扇出 + 首个成功取消其余
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;first_success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coros&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;coros&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_when&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIRST_COMPLETED&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 收尾 pending，避免警告&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_exceptions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;all failed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这是“多副本查询取最快”的常见模式：与 Go 里 fan-out + context cancel 同构，只是取消载体从 &lt;code&gt;context.Context&lt;/code&gt; 换成了 &lt;strong&gt;Task.cancel / CancelledError&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="五常见坑与排查清单"&gt;&lt;a href="#%e4%ba%94%e5%b8%b8%e8%a7%81%e5%9d%91%e4%b8%8e%e6%8e%92%e6%9f%a5%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;五、常见坑与排查清单
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;症状&lt;/th&gt;
					&lt;th&gt;可能根因&lt;/th&gt;
					&lt;th&gt;排查/修复&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;RuntimeWarning: coroutine 'x' was never awaited&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;写了 &lt;code&gt;x()&lt;/code&gt; 却未 &lt;code&gt;await&lt;/code&gt;/&lt;code&gt;create_task&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;打开 debug；全文搜未 await 的调用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;后台任务“跑一半消失”&lt;/td&gt;
					&lt;td&gt;只 &lt;code&gt;create_task&lt;/code&gt; 未保存强引用&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;set&lt;/code&gt; + &lt;code&gt;add_done_callback(discard)&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;整体 QPS 骤降、延迟抖动&lt;/td&gt;
					&lt;td&gt;协程内同步阻塞&lt;/td&gt;
					&lt;td&gt;火焰图/&lt;code&gt;asyncio&lt;/code&gt; 慢回调日志；改 &lt;code&gt;to_thread&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;超时后连接仍占满&lt;/td&gt;
					&lt;td&gt;取消路径无 &lt;code&gt;finally&lt;/code&gt; 清理&lt;/td&gt;
					&lt;td&gt;统一 async context manager&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;TaskGroup&lt;/code&gt; / &lt;code&gt;timeout&lt;/code&gt; 行为怪异&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;except CancelledError: pass&lt;/code&gt; 吞取消&lt;/td&gt;
					&lt;td&gt;清理后 re-raise；勿滥用 &lt;code&gt;uncancel&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Jupyter/已有 loop 报错&lt;/td&gt;
					&lt;td&gt;嵌套 &lt;code&gt;asyncio.run&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;用框架提供的 loop / &lt;code&gt;Runner&lt;/code&gt; / &lt;code&gt;await&lt;/code&gt; 直接写&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;取消不及时&lt;/td&gt;
					&lt;td&gt;长段纯计算无 &lt;code&gt;await&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;拆分、主动 &lt;code&gt;await asyncio.sleep(0)&lt;/code&gt; 让出（治标）或迁线程/进程（治本）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;快速自检脚本思路：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 1) 开发模式跑入口&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;PYTHONASYNCIODEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; python app.py
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 2) 确认没有 never-awaited&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 3) 压测时观察是否仅单核打满且无网络等待（疑似阻塞循环）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="六总结"&gt;&lt;a href="#%e5%85%ad%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;六、总结
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;asyncio 是单线程协作式并发&lt;/strong&gt;：&lt;code&gt;await&lt;/code&gt; 是让出点；阻塞代码会冻住所有 Task。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协程对象 ≠ 已调度任务&lt;/strong&gt;：入口用 &lt;code&gt;asyncio.run&lt;/code&gt;，并发用 &lt;code&gt;create_task&lt;/code&gt; / &lt;code&gt;TaskGroup&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task 只有弱引用&lt;/strong&gt;：fire-and-forget 必须自管强引用集合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;取消是协作语义&lt;/strong&gt;：&lt;code&gt;CancelledError&lt;/code&gt; 属 &lt;code&gt;BaseException&lt;/code&gt;；清理后应继续传播，否则破坏 &lt;code&gt;TaskGroup&lt;/code&gt;/&lt;code&gt;timeout&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;超时优先结构化 API&lt;/strong&gt;：&lt;code&gt;asyncio.timeout&lt;/code&gt; / &lt;code&gt;wait_for&lt;/code&gt;；关键段可 &lt;code&gt;shield&lt;/code&gt;，但不要“永远不退出”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;阻塞 IO 迁出循环&lt;/strong&gt;：&lt;code&gt;to_thread&lt;/code&gt; / executor；开发期打开 debug 抓 never-awaited 与慢回调。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;掌握“调度模型 + 取消契约 + 结构化汇合”，比背一长串 API 更能写出可维护的异步服务。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.python.org/3/library/asyncio.html" target="_blank" rel="noopener"
 &gt;asyncio — Asynchronous I/O（Python 官方文档）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.python.org/3/library/asyncio-task.html" target="_blank" rel="noopener"
 &gt;Coroutines and Tasks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.python.org/3/library/asyncio-eventloop.html" target="_blank" rel="noopener"
 &gt;Event Loop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.python.org/3/library/asyncio-runner.html" target="_blank" rel="noopener"
 &gt;Runners（asyncio.run / Runner）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://docs.python.org/3/library/asyncio-dev.html" target="_blank" rel="noopener"
 &gt;Developing with asyncio（Debug Mode / 多线程 / never-awaited）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://peps.python.org/pep-0492/" target="_blank" rel="noopener"
 &gt;PEP 492 – Coroutines with async and await syntax&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>Go context 取消传播与超时控制：原理、API 与工程实践</title><link>https://blog.waihost.com/posts/go-context-cancellation-timeout-practice/</link><pubDate>Sat, 18 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/go-context-cancellation-timeout-practice/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/go-context-cancellation-timeout-practice.svg" alt="Featured image of post Go context 取消传播与超时控制：原理、API 与工程实践" /&gt;&lt;p&gt;HTTP 请求被客户端断开、下游 RPC 超时、批量任务需要“先完成的取消其余副本”——这些场景的共同点是：&lt;strong&gt;一组协作的 goroutine 必须能收到“该停了”的信号，并在约定时间内释放资源&lt;/strong&gt;。Go 标准库的 &lt;code&gt;context&lt;/code&gt; 包，就是为跨 API 边界传递 &lt;strong&gt;截止时间（deadline）&lt;/strong&gt;、&lt;strong&gt;取消信号&lt;/strong&gt; 与 &lt;strong&gt;请求级值&lt;/strong&gt; 而设计的。&lt;/p&gt;
&lt;p&gt;本文基于 &lt;code&gt;context&lt;/code&gt; 包源码注释、&lt;code&gt;pkg.go.dev/context&lt;/code&gt; 文档，以及 Go 官方博客 &lt;em&gt;Go Concurrency Patterns: Context&lt;/em&gt;、&lt;em&gt;Contexts and structs&lt;/em&gt;，把取消树、超时、&lt;code&gt;CancelFunc&lt;/code&gt; 泄漏、&lt;code&gt;WithValue&lt;/code&gt; 边界与常见工程坑讲清楚，并给出可落地的写法与排查清单。&lt;/p&gt;
&lt;h2 id="一问题背景为什么需要-context"&gt;&lt;a href="#%e4%b8%80%e9%97%ae%e9%a2%98%e8%83%8c%e6%99%af%e4%b8%ba%e4%bb%80%e4%b9%88%e9%9c%80%e8%a6%81-context" class="header-anchor"&gt;&lt;/a&gt;一、问题背景：为什么需要 Context
&lt;/h2&gt;&lt;p&gt;没有统一取消通道时，常见反模式是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个函数自建 &lt;code&gt;done chan struct{}&lt;/code&gt;，调用链一长就无法串联；&lt;/li&gt;
&lt;li&gt;超时只写在 HTTP Client 上，业务层仍继续跑昂贵计算；&lt;/li&gt;
&lt;li&gt;子 goroutine 不知道请求已结束，继续访问已关闭的连接，造成泄漏或写日志噪音。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;官方文档的定位非常明确：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;工程上可以记三句话：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;能力&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;典型触发&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Deadline&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;工作最晚何时必须结束&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;WithTimeout&lt;/code&gt; / &lt;code&gt;WithDeadline&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Cancellation&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;调用方主动要求放弃&lt;/td&gt;
					&lt;td&gt;客户端断开、&lt;code&gt;cancel()&lt;/code&gt;、父 context 取消&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Request-scoped values&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;跨边界透传的请求元数据&lt;/td&gt;
					&lt;td&gt;trace id、鉴权主体（慎用）&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;服务入口应为入站请求创建 Context；出站调用应接收并传播同一个（或派生的）Context。中间调用链 &lt;strong&gt;必须向下传递&lt;/strong&gt;，必要时用 &lt;code&gt;WithCancel&lt;/code&gt; / &lt;code&gt;WithTimeout&lt;/code&gt; / &lt;code&gt;WithValue&lt;/code&gt; 派生子 Context。&lt;/p&gt;
&lt;h2 id="二核心模型取消树--done-通道"&gt;&lt;a href="#%e4%ba%8c%e6%a0%b8%e5%bf%83%e6%a8%a1%e5%9e%8b%e5%8f%96%e6%b6%88%e6%a0%91--done-%e9%80%9a%e9%81%93" class="header-anchor"&gt;&lt;/a&gt;二、核心模型：取消树 + Done 通道
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;Context&lt;/code&gt; 是接口，核心方法包括 &lt;code&gt;Done&lt;/code&gt;、&lt;code&gt;Err&lt;/code&gt;、&lt;code&gt;Deadline&lt;/code&gt;、&lt;code&gt;Value&lt;/code&gt;。理解取消的关键是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Done() &amp;lt;-chan struct{}&lt;/code&gt;&lt;/strong&gt;：通道关闭即“应停止工作”的信号；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Err()&lt;/code&gt;&lt;/strong&gt;：在 Done 关闭后说明原因——&lt;code&gt;context.Canceled&lt;/code&gt; 或 &lt;code&gt;context.DeadlineExceeded&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;派生形成树&lt;/strong&gt;：父 Context 取消时，&lt;strong&gt;所有子 Context 一并取消&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接收方不能取消父方&lt;/strong&gt;：Context 没有 &lt;code&gt;Cancel&lt;/code&gt; 方法，Done 只读，避免子操作反过来取消父操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;官方博客强调：父操作启动子操作时，子操作不应有能力取消父操作；取消权通过 &lt;code&gt;WithCancel&lt;/code&gt; 返回的 &lt;code&gt;CancelFunc&lt;/code&gt; 交给“拥有者”。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Background / request root
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─ WithTimeout(2s) ──► handler 工作树
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ ├─ WithCancel ──► 并行查询 A
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ └─ WithCancel ──► 并行查询 B
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─ 根取消 / 超时 / cancel()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ⇒ 整棵子树 Done 关闭
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Background()&lt;/code&gt; 返回永不取消、无值、无 deadline 的空 Context，适合 &lt;code&gt;main&lt;/code&gt;、初始化、测试，以及入站请求的顶层根。&lt;code&gt;TODO()&lt;/code&gt; 同样是空 Context，用于“暂时不清楚该用哪个 Context”的占位；&lt;strong&gt;不要传 &lt;code&gt;nil&lt;/code&gt;&lt;/strong&gt;——文档明确要求：即使函数允许 nil，也请传 &lt;code&gt;context.TODO()&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="三api-速查从根到超时"&gt;&lt;a href="#%e4%b8%89api-%e9%80%9f%e6%9f%a5%e4%bb%8e%e6%a0%b9%e5%88%b0%e8%b6%85%e6%97%b6" class="header-anchor"&gt;&lt;/a&gt;三、API 速查：从根到超时
&lt;/h2&gt;&lt;h3 id="1-取消withcancel--withcancelcause"&gt;&lt;a href="#1-%e5%8f%96%e6%b6%88withcancel--withcancelcause" class="header-anchor"&gt;&lt;/a&gt;1. 取消：&lt;code&gt;WithCancel&lt;/code&gt; / &lt;code&gt;WithCancelCause&lt;/code&gt;
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithCancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 工作结束后尽快调用，释放子树与定时器资源&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;语义要点（源码注释）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;子 Context 的 Done 在 &lt;strong&gt;&lt;code&gt;cancel()&lt;/code&gt; 被调用&lt;/strong&gt; 或 &lt;strong&gt;父 Done 关闭&lt;/strong&gt; 时关闭，谁先发生谁生效；&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;cancel&lt;/code&gt; 会取消子及其后代，并断开父对子的引用、停止相关 timer；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不调用 &lt;code&gt;CancelFunc&lt;/code&gt; 会泄漏&lt;/strong&gt;：子 Context 及其后代会一直挂到父被取消；&lt;code&gt;go vet&lt;/code&gt; 会检查所有控制流路径是否使用了 CancelFunc；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CancelFunc&lt;/code&gt; &lt;strong&gt;不等待&lt;/strong&gt;工作真正停下；可被多 goroutine 并发调用，首次之后为 no-op。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go 1.20+ 的 cause 系列更利于排障：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithCancelCause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;client disconnected: %w&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// ctx.Err() 仍为 context.Canceled&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// context.Cause(ctx) 可取到你记录的 cause&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;Cause(ctx)&lt;/code&gt;：若通过 &lt;code&gt;CancelCauseFunc(err)&lt;/code&gt; 取消则返回该 err；否则与 &lt;code&gt;ctx.Err()&lt;/code&gt; 相同；未取消时返回 &lt;code&gt;nil&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="2-超时与截止时间withtimeout--withdeadline"&gt;&lt;a href="#2-%e8%b6%85%e6%97%b6%e4%b8%8e%e6%88%aa%e6%ad%a2%e6%97%b6%e9%97%b4withtimeout--withdeadline" class="header-anchor"&gt;&lt;/a&gt;2. 超时与截止时间：&lt;code&gt;WithTimeout&lt;/code&gt; / &lt;code&gt;WithDeadline&lt;/code&gt;
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// WithTimeout ≡ WithDeadline(parent, time.Now().Add(timeout))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Millisecond&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;slowOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;语义要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;子 deadline 取 &lt;strong&gt;parent deadline 与新 deadline 中更早者&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;Done 在 &lt;strong&gt;超时 / cancel / 父取消&lt;/strong&gt; 三者中最先发生时关闭；&lt;/li&gt;
&lt;li&gt;超时后 &lt;code&gt;ctx.Err()&lt;/code&gt; 为 &lt;code&gt;context.DeadlineExceeded&lt;/code&gt;；主动 &lt;code&gt;cancel&lt;/code&gt; 一般为 &lt;code&gt;context.Canceled&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;同样必须 &lt;code&gt;defer cancel()&lt;/code&gt;，即使操作在超时前完成，也要释放 timer 等资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-值withvalue窄场景"&gt;&lt;a href="#3-%e5%80%bcwithvalue%e7%aa%84%e5%9c%ba%e6%99%af" class="header-anchor"&gt;&lt;/a&gt;3. 值：&lt;code&gt;WithValue&lt;/code&gt;（窄场景）
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;traceIDKey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;traceIDKey&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;req-42&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;官方硬规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;只&lt;/strong&gt;用于跨进程/API 传递的请求级数据，&lt;strong&gt;不要&lt;/strong&gt;把可选函数参数塞进 Context；&lt;/li&gt;
&lt;li&gt;key 必须 comparable，且 &lt;strong&gt;不要用内置 string 等类型当 key&lt;/strong&gt;，避免包间碰撞；推荐自定义未导出类型，如 &lt;code&gt;struct{}&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4-进阶withoutcancel-与-afterfuncgo-121"&gt;&lt;a href="#4-%e8%bf%9b%e9%98%b6withoutcancel-%e4%b8%8e-afterfuncgo-121" class="header-anchor"&gt;&lt;/a&gt;4. 进阶：&lt;code&gt;WithoutCancel&lt;/code&gt; 与 &lt;code&gt;AfterFunc&lt;/code&gt;（Go 1.21+）
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;API&lt;/th&gt;
					&lt;th&gt;用途&lt;/th&gt;
					&lt;th&gt;注意&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;WithoutCancel(parent)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;派生一个 &lt;strong&gt;不随父取消&lt;/strong&gt; 的 Context；无 Deadline/Err，Done 为 nil&lt;/td&gt;
					&lt;td&gt;父 nil 会 panic；适合“请求已结束但仍要落盘审计”的短尾工作，且必须自建超时&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;AfterFunc(ctx, f)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;ctx 取消后在 &lt;strong&gt;独立 goroutine&lt;/strong&gt; 跑 &lt;code&gt;f&lt;/code&gt;；已取消则立即调度&lt;/td&gt;
					&lt;td&gt;返回 &lt;code&gt;stop&lt;/code&gt;；&lt;code&gt;stop&lt;/code&gt; 不等待 &lt;code&gt;f&lt;/code&gt; 结束；多次 &lt;code&gt;AfterFunc&lt;/code&gt; 彼此独立&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="四实践http-处理中的超时与协作取消"&gt;&lt;a href="#%e5%9b%9b%e5%ae%9e%e8%b7%b5http-%e5%a4%84%e7%90%86%e4%b8%ad%e7%9a%84%e8%b6%85%e6%97%b6%e4%b8%8e%e5%8d%8f%e4%bd%9c%e5%8f%96%e6%b6%88" class="header-anchor"&gt;&lt;/a&gt;四、实践：HTTP 处理中的超时与协作取消
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;handleSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 入站请求：以 r.Context() 为根（客户端断开时通常会取消）&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 业务总预算：2s（与网关/客户端超时对齐，略留余量）&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;q&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;searchBackends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DeadlineExceeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;search timeout&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusGatewayTimeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Canceled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 客户端已走，通常无需再写完整响应&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusBadGateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ... 写响应&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;searchBackends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 扇出：任一成功可取消其余；或全部失败返回&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithCancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;chan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;a.example&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;b.example&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ep&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;go&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;callOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 内部必须监听 ctx.Done()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Cause&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 或 ctx.Err()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 提前结束兄弟 goroutine&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;callOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MethodGet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;https://&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;/s?q=&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;QueryEscape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DefaultClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ... 读 body，同样尊重 ctx&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;ok&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;协作取消的正确姿势：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;叶子操作&lt;/strong&gt;用支持 Context 的 API（&lt;code&gt;NewRequestWithContext&lt;/code&gt;、&lt;code&gt;QueryContext&lt;/code&gt;、gRPC ctx 等）；&lt;/li&gt;
&lt;li&gt;纯计算循环用 &lt;code&gt;select&lt;/code&gt; 或周期性检查 &lt;code&gt;ctx.Err()&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;取消是 &lt;strong&gt;建议性（advisory）&lt;/strong&gt; 的：库必须真正监听 Done，否则 &lt;code&gt;cancel()&lt;/code&gt; 只是“通知了空气”。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="五不要把-context-塞进结构体"&gt;&lt;a href="#%e4%ba%94%e4%b8%8d%e8%a6%81%e6%8a%8a-context-%e5%a1%9e%e8%bf%9b%e7%bb%93%e6%9e%84%e4%bd%93" class="header-anchor"&gt;&lt;/a&gt;五、不要把 Context 塞进结构体
&lt;/h2&gt;&lt;p&gt;官方文档与 &lt;em&gt;Contexts and structs&lt;/em&gt; 博客一致：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named &lt;code&gt;ctx&lt;/code&gt;.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;推荐：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* 无 ctx 字段 */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Work&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;work&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;Work&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;反例把 &lt;code&gt;ctx&lt;/code&gt; 存进 &lt;code&gt;Worker&lt;/code&gt; 后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用方无法为单次 &lt;code&gt;Fetch&lt;/code&gt; 单独设超时；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;New(ctx)&lt;/code&gt; 的生命周期与后续方法混在一起，API 语义模糊；&lt;/li&gt;
&lt;li&gt;两个方法被迫共享同一取消域，容易误取消或该取消却取不消。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;极少数场景（如长期运行、自身定义生命周期的对象）才考虑在结构体内持有 Context，且必须文档化；日常业务 Handler / Service / Repository &lt;strong&gt;按参数传递&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="六常见坑与排查"&gt;&lt;a href="#%e5%85%ad%e5%b8%b8%e8%a7%81%e5%9d%91%e4%b8%8e%e6%8e%92%e6%9f%a5" class="header-anchor"&gt;&lt;/a&gt;六、常见坑与排查
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;症状&lt;/th&gt;
					&lt;th&gt;可能原因&lt;/th&gt;
					&lt;th&gt;处理&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;goroutine / timer 泄漏&lt;/td&gt;
					&lt;td&gt;创建了 &lt;code&gt;WithCancel&lt;/code&gt;/&lt;code&gt;WithTimeout&lt;/code&gt; 却没 &lt;code&gt;cancel&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;立刻 &lt;code&gt;defer cancel()&lt;/code&gt;；跑 &lt;code&gt;go vet&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;超时不生效&lt;/td&gt;
					&lt;td&gt;底层调用未绑定 ctx（旧 &lt;code&gt;http.Get&lt;/code&gt;、忽略 Done 的循环）&lt;/td&gt;
					&lt;td&gt;换 &lt;code&gt;*Context&lt;/code&gt; API；手写 &lt;code&gt;select&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;Err&lt;/code&gt; 总是 Canceled 难排查&lt;/td&gt;
					&lt;td&gt;未区分超时与主动取消&lt;/td&gt;
					&lt;td&gt;用 &lt;code&gt;errors.Is&lt;/code&gt;；需要细节时用 Cause 系列&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;请求结束后仍用原 ctx 写库&lt;/td&gt;
					&lt;td&gt;父 ctx 已取消导致落库失败&lt;/td&gt;
					&lt;td&gt;短尾任务用 &lt;strong&gt;新的&lt;/strong&gt; &lt;code&gt;WithTimeout(context.Background(), …)&lt;/code&gt; 或审慎使用 &lt;code&gt;WithoutCancel&lt;/code&gt; + 自有超时&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;值取不到 / 键冲突&lt;/td&gt;
					&lt;td&gt;string key 跨包碰撞或类型断言错误&lt;/td&gt;
					&lt;td&gt;私有 key 类型 + 统一 accessor&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;把可选参数塞进 Value&lt;/td&gt;
					&lt;td&gt;隐式依赖、难测&lt;/td&gt;
					&lt;td&gt;显式函数参数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;传递 &lt;code&gt;nil&lt;/code&gt; context&lt;/td&gt;
					&lt;td&gt;派生时 panic 或行为未定义&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;TODO()&lt;/code&gt; / &lt;code&gt;Background()&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最小实验建议：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;A. WithTimeout(50ms) + time.Sleep(200ms) 并 select ctx.Done()
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 期望：DeadlineExceeded
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;B. 父 WithCancel，子 WithTimeout(1s)，父先 cancel
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 期望：子立刻 Done，Err 为 Canceled（父取消优先于子 timer）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;C. WithTimeout 成功路径忘记 defer cancel
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 观察：高频调用下 timer/子节点滞留（pprof / 泄漏检测）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;D. Worker 存 ctx vs 每方法传 ctx
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 对比：后者可为单次调用设更短 deadline
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="七工程约定可直接用于-code-review"&gt;&lt;a href="#%e4%b8%83%e5%b7%a5%e7%a8%8b%e7%ba%a6%e5%ae%9a%e5%8f%af%e7%9b%b4%e6%8e%a5%e7%94%a8%e4%ba%8e-code-review" class="header-anchor"&gt;&lt;/a&gt;七、工程约定（可直接用于 Code Review）
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;ctx 永远是第一个参数&lt;/strong&gt;，命名 &lt;code&gt;ctx&lt;/code&gt;；不放结构体字段。&lt;/li&gt;
&lt;li&gt;入站用框架/request 的 Context；出站必须向下传，禁止“半路换成 Background”除非有书面理由（如与请求脱钩的异步任务）。&lt;/li&gt;
&lt;li&gt;每个 &lt;code&gt;WithCancel&lt;/code&gt; / &lt;code&gt;WithTimeout&lt;/code&gt; / &lt;code&gt;WithDeadline&lt;/code&gt; &lt;strong&gt;成对 &lt;code&gt;defer cancel()&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;超时预算 &lt;strong&gt;自上而下递减&lt;/strong&gt;：网关 3s → 服务 2s → 下游 RPC 800ms，避免子超时大于父。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WithValue&lt;/code&gt; 仅 trace/auth 等横切元数据；业务输入走参数。&lt;/li&gt;
&lt;li&gt;错误判断用 &lt;code&gt;errors.Is(err, context.Canceled)&lt;/code&gt; / &lt;code&gt;DeadlineExceeded&lt;/code&gt;，不要比字符串。&lt;/li&gt;
&lt;li&gt;取消后仍需完成的短工作：新建带超时的独立 Context，并限队列，防止雪崩。&lt;/li&gt;
&lt;li&gt;并发扇出时明确策略：全部完成 / 先胜出取消其余 / 错误阈值，并保证兄弟 goroutine 能收到取消。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="八总结"&gt;&lt;a href="#%e5%85%ab%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;八、总结
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;context&lt;/code&gt; 不是“多传一个参数”的仪式，而是 Go 并发里 &lt;strong&gt;跨边界的截止时间与取消协议&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;取消沿 &lt;strong&gt;父子树&lt;/strong&gt; 向下传播；子不能取消父；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WithTimeout&lt;/code&gt; / &lt;code&gt;WithDeadline&lt;/code&gt; 用更紧的 deadline 保护尾延迟；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CancelFunc&lt;/code&gt; 必须调用，否则泄漏到父取消为止；&lt;/li&gt;
&lt;li&gt;协作取消要求叶子真正监听 &lt;code&gt;Done&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;Context 作参数传递，不进结构体；Value 只承载请求级元数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把“谁拥有 cancel、超时预算如何切分、叶子是否尊重 Done”三件事写进设计与 Code Review，线上超时与 goroutine 泄漏类问题会少一个数量级。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Go 标准库文档 — &lt;a class="link" href="https://pkg.go.dev/context" target="_blank" rel="noopener"
 &gt;context package&lt;/a&gt;（Overview、&lt;code&gt;WithCancel&lt;/code&gt;/&lt;code&gt;WithTimeout&lt;/code&gt;/&lt;code&gt;WithValue&lt;/code&gt;/&lt;code&gt;WithoutCancel&lt;/code&gt;/&lt;code&gt;AfterFunc&lt;/code&gt;/&lt;code&gt;Cause&lt;/code&gt;、编程约定）&lt;/li&gt;
&lt;li&gt;Go 源码 — &lt;a class="link" href="https://raw.githubusercontent.com/golang/go/master/src/context/context.go" target="_blank" rel="noopener"
 &gt;src/context/context.go&lt;/a&gt;（包注释与 API 语义、&lt;code&gt;Canceled&lt;/code&gt;/&lt;code&gt;DeadlineExceeded&lt;/code&gt;、CancelFunc 行为）&lt;/li&gt;
&lt;li&gt;Go Blog — &lt;a class="link" href="https://go.dev/blog/context" target="_blank" rel="noopener"
 &gt;Go Concurrency Patterns: Context&lt;/a&gt;（Context 接口、派生树、Background、服务端示例动机）&lt;/li&gt;
&lt;li&gt;Go Blog — &lt;a class="link" href="https://go.dev/blog/context-and-structs" target="_blank" rel="noopener"
 &gt;Contexts and structs&lt;/a&gt;（为何不要把 Context 存进结构体、按调用传参的生命周期清晰性）&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>OceanBase 从零开始：核心概念、上手路径，以及和 MySQL 的关键差异</title><link>https://blog.waihost.com/posts/oceanbase-beginner-mysql-comparison/</link><pubDate>Fri, 17 Jul 2026 01:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/oceanbase-beginner-mysql-comparison/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/oceanbase-beginner-mysql-comparison.svg" alt="Featured image of post OceanBase 从零开始：核心概念、上手路径，以及和 MySQL 的关键差异" /&gt;&lt;p&gt;如果你已经会 MySQL，第一次接触 OceanBase 时，最容易卡住的不是“SQL 怎么写”，而是：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;它看起来很像 MySQL，为什么连接方式、资源模型、高可用和运维概念完全不是一回事？&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;本文面向零基础到 MySQL 初中级同学，用一条从概念到上手的路径讲清楚：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;OceanBase 是什么&lt;/li&gt;
&lt;li&gt;和 MySQL 在架构、租户、连接、兼容性上的核心差异&lt;/li&gt;
&lt;li&gt;从连接、建库建表到基础 SQL 的最小实践&lt;/li&gt;
&lt;li&gt;什么场景该把 OceanBase 当“更强的 MySQL”，什么场景不能这么想&lt;/li&gt;
&lt;/ol&gt;

 &lt;blockquote&gt;
 &lt;p&gt;说明：本文以 &lt;strong&gt;OceanBase 社区版（MySQL 模式）&lt;/strong&gt; 为主。官方文档明确：&lt;strong&gt;社区版仅提供 MySQL 模式&lt;/strong&gt;；企业版还支持 Oracle 模式。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="一先建立正确心智模型"&gt;&lt;a href="#%e4%b8%80%e5%85%88%e5%bb%ba%e7%ab%8b%e6%ad%a3%e7%a1%ae%e5%bf%83%e6%99%ba%e6%a8%a1%e5%9e%8b" class="header-anchor"&gt;&lt;/a&gt;一、先建立正确心智模型
&lt;/h2&gt;&lt;h3 id="1-mysql-的常见心智模型"&gt;&lt;a href="#1-mysql-%e7%9a%84%e5%b8%b8%e8%a7%81%e5%bf%83%e6%99%ba%e6%a8%a1%e5%9e%8b" class="header-anchor"&gt;&lt;/a&gt;1. MySQL 的常见心智模型
&lt;/h3&gt;&lt;p&gt;很多人对 MySQL 的默认理解是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;一台机器 / 一个 mysqld 实例
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 多个 database
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 多个 table
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;连接通常很直接：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mysql -h127.0.0.1 -P3306 -uroot -p
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;你连上的，基本就是“这个实例本身”。&lt;/p&gt;
&lt;h3 id="2-oceanbase-的心智模型"&gt;&lt;a href="#2-oceanbase-%e7%9a%84%e5%bf%83%e6%99%ba%e6%a8%a1%e5%9e%8b" class="header-anchor"&gt;&lt;/a&gt;2. OceanBase 的心智模型
&lt;/h3&gt;&lt;p&gt;OceanBase 更接近：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;一个集群（Cluster）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 多个节点（OBServer）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 多个可用区（Zone）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 多个租户（Tenant） ← 应用视角下更像“数据库实例”
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 多个 database
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 多个 table / 分区
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;关键变化只有一句话：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;在 OceanBase 里，应用通常不是“连一台 MySQL 机器”，而是“连某个租户”。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;官方文档把租户定义为：集群内互相隔离的数据库“实例”。对应用来说，一个租户近似一个独立数据库实例；租户之间数据、权限、资源隔离。&lt;/p&gt;
&lt;h2 id="二oceanbase-是什么"&gt;&lt;a href="#%e4%ba%8coceanbase-%e6%98%af%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;二、OceanBase 是什么
&lt;/h2&gt;&lt;p&gt;OceanBase 是蚂蚁集团开源的&lt;strong&gt;分布式关系型数据库&lt;/strong&gt;，基于 Paxos 类共识协议与分布式架构，提供高可用和水平扩展能力，可运行在普通服务器集群上。&lt;/p&gt;
&lt;p&gt;官方强调的关键能力包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;水平扩展&lt;/li&gt;
&lt;li&gt;高可用（RPO=0、RTO 秒级恢复目标）&lt;/li&gt;
&lt;li&gt;MySQL 兼容，便于迁移&lt;/li&gt;
&lt;li&gt;HTAP（同一套系统同时服务事务与分析负载）&lt;/li&gt;
&lt;li&gt;多租户资源隔离&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以把“上手阶段”理解成两层：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;层&lt;/th&gt;
					&lt;th&gt;你感受到的&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;SQL 层&lt;/td&gt;
					&lt;td&gt;很像 MySQL：&lt;code&gt;CREATE DATABASE&lt;/code&gt; / &lt;code&gt;CREATE TABLE&lt;/code&gt; / &lt;code&gt;SELECT&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;系统层&lt;/td&gt;
					&lt;td&gt;更像分布式数据库：集群、Zone、租户、Unit、副本、OBProxy&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="三和-mysql-的核心差异总览"&gt;&lt;a href="#%e4%b8%89%e5%92%8c-mysql-%e7%9a%84%e6%a0%b8%e5%bf%83%e5%b7%ae%e5%bc%82%e6%80%bb%e8%a7%88" class="header-anchor"&gt;&lt;/a&gt;三、和 MySQL 的核心差异总览
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;MySQL（常见单机/主从）&lt;/th&gt;
					&lt;th&gt;OceanBase&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;基本部署单元&lt;/td&gt;
					&lt;td&gt;实例（mysqld）&lt;/td&gt;
					&lt;td&gt;集群 + 租户&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;应用连接对象&lt;/td&gt;
					&lt;td&gt;实例&lt;/td&gt;
					&lt;td&gt;租户（经直连或 OBProxy）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;默认端口（常见）&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;3306&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;直连 &lt;code&gt;2881&lt;/code&gt;，经 ODP/OBProxy 常见 &lt;code&gt;2883&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;用户写法&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;root&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;root@tenant&lt;/code&gt; 或 &lt;code&gt;user@tenant#cluster&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;扩展方式&lt;/td&gt;
					&lt;td&gt;垂直扩容 / 读写分离 / 分库分表&lt;/td&gt;
					&lt;td&gt;原生分区、多节点、副本扩展&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;高可用机制&lt;/td&gt;
					&lt;td&gt;主从复制、MGR 等&lt;/td&gt;
					&lt;td&gt;基于 Multi-Paxos 的多副本日志流&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;多业务隔离&lt;/td&gt;
					&lt;td&gt;多实例 / 多库 + 权限&lt;/td&gt;
					&lt;td&gt;原生多租户 + 资源池&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;存储引擎模型&lt;/td&gt;
					&lt;td&gt;InnoDB 等可插拔引擎&lt;/td&gt;
					&lt;td&gt;自研分布式存储与事务体系&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;SQL 兼容&lt;/td&gt;
					&lt;td&gt;原生 MySQL&lt;/td&gt;
					&lt;td&gt;MySQL 模式兼容 5.7/8.0 大部分能力，但非 100%&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;社区版模式&lt;/td&gt;
					&lt;td&gt;MySQL&lt;/td&gt;
					&lt;td&gt;仅 MySQL 模式&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一句话：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;MySQL 更像“一台关系库”；OceanBase 更像“一个可多租户的分布式数据库平台”。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="四架构差异为什么它不像单机-mysql"&gt;&lt;a href="#%e5%9b%9b%e6%9e%b6%e6%9e%84%e5%b7%ae%e5%bc%82%e4%b8%ba%e4%bb%80%e4%b9%88%e5%ae%83%e4%b8%8d%e5%83%8f%e5%8d%95%e6%9c%ba-mysql" class="header-anchor"&gt;&lt;/a&gt;四、架构差异：为什么它不像单机 MySQL
&lt;/h2&gt;&lt;h3 id="1-shared-nothing-集群"&gt;&lt;a href="#1-shared-nothing-%e9%9b%86%e7%be%a4" class="header-anchor"&gt;&lt;/a&gt;1. Shared-Nothing 集群
&lt;/h3&gt;&lt;p&gt;官方系统架构文档指出，OceanBase 常用无共享（Shared-Nothing）模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;节点对等&lt;/li&gt;
&lt;li&gt;每个节点都有自己的 SQL / 存储 / 事务能力&lt;/li&gt;
&lt;li&gt;表可水平拆分为多个分区（Partition）&lt;/li&gt;
&lt;li&gt;分区数据落在 Tablet，修改通过日志流（Log Stream）持久化&lt;/li&gt;
&lt;li&gt;主从副本通过 Multi-Paxos 保持一致性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这意味着：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据天然可分布到多节点&lt;/li&gt;
&lt;li&gt;故障切换依赖副本选举，而不是简单“切到另一台 mysqld”&lt;/li&gt;
&lt;li&gt;建表时的分区设计，会直接影响数据分布与扩展性&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="2-多租户最容易被-mysql-同学忽略的概念"&gt;&lt;a href="#2-%e5%a4%9a%e7%a7%9f%e6%88%b7%e6%9c%80%e5%ae%b9%e6%98%93%e8%a2%ab-mysql-%e5%90%8c%e5%ad%a6%e5%bf%bd%e7%95%a5%e7%9a%84%e6%a6%82%e5%bf%b5" class="header-anchor"&gt;&lt;/a&gt;2. 多租户：最容易被 MySQL 同学忽略的概念
&lt;/h3&gt;&lt;p&gt;在 OceanBase 中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;集群初始化后会有系统租户 &lt;strong&gt;&lt;code&gt;sys&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys&lt;/code&gt; 保存集群元数据，本身也是 MySQL 兼容模式租户&lt;/li&gt;
&lt;li&gt;业务一般应在&lt;strong&gt;普通租户&lt;/strong&gt;里建库建表，而不是把所有业务都堆在 &lt;code&gt;sys&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建业务租户的典型顺序是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Unit Config（资源规格）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Resource Pool（资源池）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → Tenant（租户）
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这和 MySQL 里“直接 &lt;code&gt;CREATE DATABASE&lt;/code&gt;”完全不同。&lt;br&gt;
MySQL 的 database 更像命名空间；OceanBase 的 tenant 更像独立实例边界。&lt;/p&gt;
&lt;h3 id="3-obproxy--odp应用如何像访问单机库一样访问分布式库"&gt;&lt;a href="#3-obproxy--odp%e5%ba%94%e7%94%a8%e5%a6%82%e4%bd%95%e5%83%8f%e8%ae%bf%e9%97%ae%e5%8d%95%e6%9c%ba%e5%ba%93%e4%b8%80%e6%a0%b7%e8%ae%bf%e9%97%ae%e5%88%86%e5%b8%83%e5%bc%8f%e5%ba%93" class="header-anchor"&gt;&lt;/a&gt;3. OBProxy / ODP：应用如何“像访问单机库一样”访问分布式库
&lt;/h3&gt;&lt;p&gt;为了让应用尽量少感知分区和副本分布，OceanBase 提供 &lt;strong&gt;ODP（OceanBase Database Proxy，又称 OBProxy）&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;应用通常连代理&lt;/li&gt;
&lt;li&gt;代理把 SQL 路由到合适节点&lt;/li&gt;
&lt;li&gt;对应用更接近“连一个入口地址”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这对应 MySQL 生态里的 proxy / 中间件角色，但在 OceanBase 里它更常作为标准访问路径出现。&lt;/p&gt;
&lt;h2 id="五连接差异最容易踩坑的地方"&gt;&lt;a href="#%e4%ba%94%e8%bf%9e%e6%8e%a5%e5%b7%ae%e5%bc%82%e6%9c%80%e5%ae%b9%e6%98%93%e8%b8%a9%e5%9d%91%e7%9a%84%e5%9c%b0%e6%96%b9" class="header-anchor"&gt;&lt;/a&gt;五、连接差异：最容易踩坑的地方
&lt;/h2&gt;&lt;h3 id="1-docker-体验环境最简单"&gt;&lt;a href="#1-docker-%e4%bd%93%e9%aa%8c%e7%8e%af%e5%a2%83%e6%9c%80%e7%ae%80%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;1. Docker 体验环境（最简单）
&lt;/h3&gt;&lt;p&gt;快速体验可用官方镜像：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker run -p 2881:2881 --name oceanbase-ce -e &lt;span class="nv"&gt;MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;mini -d oceanbase/oceanbase-ce
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker logs oceanbase-ce &lt;span class="p"&gt;|&lt;/span&gt; tail -1 &lt;span class="c1"&gt;# 期望 boot success!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;连接示例：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 系统租户 root&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; -it oceanbase-ce obclient -h127.0.0.1 -P2881 -uroot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 或宿主机 mysql 客户端&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mysql -h127.0.0.1 -P2881 -uroot
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mysql -h127.0.0.1 -P2881 -uroot@test
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-正式连接写法用户名里有租户"&gt;&lt;a href="#2-%e6%ad%a3%e5%bc%8f%e8%bf%9e%e6%8e%a5%e5%86%99%e6%b3%95%e7%94%a8%e6%88%b7%e5%90%8d%e9%87%8c%e6%9c%89%e7%a7%9f%e6%88%b7" class="header-anchor"&gt;&lt;/a&gt;2. 正式连接写法：用户名里有租户
&lt;/h3&gt;&lt;p&gt;官方文档给出两类常见连接：&lt;/p&gt;
&lt;h4 id="直连-observer默认端口常为-2881"&gt;&lt;a href="#%e7%9b%b4%e8%bf%9e-observer%e9%bb%98%e8%ae%a4%e7%ab%af%e5%8f%a3%e5%b8%b8%e4%b8%ba-2881" class="header-anchor"&gt;&lt;/a&gt;直连 OBServer（默认端口常为 2881）
&lt;/h4&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mysql -h&amp;lt;observer_ip&amp;gt; -P2881 -u用户名@租户名 -p
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直连时 &lt;code&gt;-u&lt;/code&gt; 应是 &lt;code&gt;用户名@租户名&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要在直连用户名里带集群名&lt;/strong&gt;，否则可能报错&lt;/li&gt;
&lt;li&gt;普通租户直连时，目标节点上需要有该租户资源&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="通过-odpobproxy默认端口常为-2883"&gt;&lt;a href="#%e9%80%9a%e8%bf%87-odpobproxy%e9%bb%98%e8%ae%a4%e7%ab%af%e5%8f%a3%e5%b8%b8%e4%b8%ba-2883" class="header-anchor"&gt;&lt;/a&gt;通过 ODP/OBProxy（默认端口常为 2883）
&lt;/h4&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mysql -h&amp;lt;odp_ip&amp;gt; -P2883 -u用户名@租户名#集群名 -p
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;用户名还可能写成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;集群名:租户名:用户名&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;集群名-租户名-用户名&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;集群名.租户名.用户名&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这和 MySQL 的 &lt;code&gt;root@'%'&lt;/code&gt; 完全不是同一套语义。&lt;/p&gt;
&lt;h3 id="3-客户端选择"&gt;&lt;a href="#3-%e5%ae%a2%e6%88%b7%e7%ab%af%e9%80%89%e6%8b%a9" class="header-anchor"&gt;&lt;/a&gt;3. 客户端选择
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;可用 &lt;code&gt;obclient&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;也可用 MySQL 客户端连接 MySQL 模式租户&lt;br&gt;
官方文档提到当前支持的 MySQL 客户端版本包括 5.5 / 5.6 / 5.7（以你使用版本的文档为准）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="六从零上手mysql-同学-10-分钟路径"&gt;&lt;a href="#%e5%85%ad%e4%bb%8e%e9%9b%b6%e4%b8%8a%e6%89%8bmysql-%e5%90%8c%e5%ad%a6-10-%e5%88%86%e9%92%9f%e8%b7%af%e5%be%84" class="header-anchor"&gt;&lt;/a&gt;六、从零上手：MySQL 同学 10 分钟路径
&lt;/h2&gt;&lt;p&gt;下面假设你已经能连上一个 MySQL 模式租户。&lt;/p&gt;
&lt;h3 id="1-看有哪些库"&gt;&lt;a href="#1-%e7%9c%8b%e6%9c%89%e5%93%aa%e4%ba%9b%e5%ba%93" class="header-anchor"&gt;&lt;/a&gt;1. 看有哪些库
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DATABASES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;体验环境里常见会看到 &lt;code&gt;oceanbase&lt;/code&gt;、&lt;code&gt;test&lt;/code&gt;、&lt;code&gt;information_schema&lt;/code&gt;、&lt;code&gt;mysql&lt;/code&gt; 等。&lt;/p&gt;
&lt;h3 id="2-创建业务库"&gt;&lt;a href="#2-%e5%88%9b%e5%bb%ba%e4%b8%9a%e5%8a%a1%e5%ba%93" class="header-anchor"&gt;&lt;/a&gt;2. 创建业务库
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DATABASE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;demo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DEFAULT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;CHARACTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;utf8mb4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;READ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WRITE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;USE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;demo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这和 MySQL 非常像。&lt;/p&gt;
&lt;h3 id="3-建表写入查询"&gt;&lt;a href="#3-%e5%bb%ba%e8%a1%a8%e5%86%99%e5%85%a5%e6%9f%a5%e8%af%a2" class="header-anchor"&gt;&lt;/a&gt;3. 建表、写入、查询
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DATETIME&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DEFAULT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alice&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;bob&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alice2&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;官方 MySQL 模式基础文档也覆盖了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CREATE DATABASE&lt;/code&gt; / &lt;code&gt;SHOW DATABASES&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CREATE TABLE&lt;/code&gt; / &lt;code&gt;SHOW TABLES&lt;/code&gt; / &lt;code&gt;SHOW CREATE TABLE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ALTER TABLE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;INSERT&lt;/code&gt; / &lt;code&gt;UPDATE&lt;/code&gt; / &lt;code&gt;DELETE&lt;/code&gt; / &lt;code&gt;TRUNCATE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;多表查询、聚合、&lt;code&gt;EXPLAIN&lt;/code&gt; 等&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="4-看建表细节时的oceanbase-味道"&gt;&lt;a href="#4-%e7%9c%8b%e5%bb%ba%e8%a1%a8%e7%bb%86%e8%8a%82%e6%97%b6%e7%9a%84oceanbase-%e5%91%b3%e9%81%93" class="header-anchor"&gt;&lt;/a&gt;4. 看建表细节时的“OceanBase 味道”
&lt;/h3&gt;&lt;p&gt;在 OceanBase 里执行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;G&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;你可能会看到比 MySQL 更“重”的表属性，例如副本数、压缩、Tablet 相关参数等。&lt;br&gt;
这说明：&lt;strong&gt;SQL 表面兼容，元数据与存储参数并不等同于 InnoDB。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="七兼容性像-mysql但不是-100-mysql"&gt;&lt;a href="#%e4%b8%83%e5%85%bc%e5%ae%b9%e6%80%a7%e5%83%8f-mysql%e4%bd%86%e4%b8%8d%e6%98%af-100-mysql" class="header-anchor"&gt;&lt;/a&gt;七、兼容性：像 MySQL，但不是 100% MySQL
&lt;/h2&gt;&lt;p&gt;官方《与 MySQL 兼容性对比》说明：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;OceanBase 的 MySQL 模式兼容 MySQL 5.7/8.0 的&lt;strong&gt;绝大部分&lt;/strong&gt;功能和语法；因架构差异或需求优先级，部分功能未支持。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="1-相似度高的部分"&gt;&lt;a href="#1-%e7%9b%b8%e4%bc%bc%e5%ba%a6%e9%ab%98%e7%9a%84%e9%83%a8%e5%88%86" class="header-anchor"&gt;&lt;/a&gt;1. 相似度高的部分
&lt;/h3&gt;&lt;p&gt;通常对应用最友好的部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;常见数值 / 时间 / 字符 / JSON 等数据类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SELECT&lt;/code&gt; / &lt;code&gt;INSERT&lt;/code&gt; / &lt;code&gt;UPDATE&lt;/code&gt; / &lt;code&gt;DELETE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;多表连接、子查询、聚合分组&lt;/li&gt;
&lt;li&gt;存储过程、函数、触发器等大部分 PL 能力&lt;/li&gt;
&lt;li&gt;&lt;code&gt;information_schema&lt;/code&gt;、&lt;code&gt;mysql&lt;/code&gt; 中的大量视图（但不是逐列完全等价）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;很多基于 MySQL 协议的驱动可直接连&lt;/li&gt;
&lt;li&gt;简单 CRUD 业务迁移门槛较低&lt;/li&gt;
&lt;li&gt;你会有“这不就是 MySQL 吗”的错觉&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-必须注意的差异"&gt;&lt;a href="#2-%e5%bf%85%e9%a1%bb%e6%b3%a8%e6%84%8f%e7%9a%84%e5%b7%ae%e5%bc%82" class="header-anchor"&gt;&lt;/a&gt;2. 必须注意的差异
&lt;/h3&gt;&lt;p&gt;官方明确提到的差异方向包括：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;类别&lt;/th&gt;
					&lt;th&gt;差异点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;SQL 语法&lt;/td&gt;
					&lt;td&gt;例如不支持 &lt;code&gt;SELECT ... FOR SHARE ...&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;TRUNCATE&lt;/td&gt;
					&lt;td&gt;不支持在事务处理与表锁定过程中操作&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;系统视图&lt;/td&gt;
					&lt;td&gt;不保证所有视图/列含义都与 MySQL 相同&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;存储引擎&lt;/td&gt;
					&lt;td&gt;不是 InnoDB 插件体系，而是自研分布式存储事务体系&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;优化器&lt;/td&gt;
					&lt;td&gt;执行计划与优化策略不同&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;分区 / 备份恢复&lt;/td&gt;
					&lt;td&gt;能力模型与操作路径不同&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;架构相关功能&lt;/td&gt;
					&lt;td&gt;因分布式架构，部分单机 MySQL 习惯不能照搬&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="3-实战迁移建议"&gt;&lt;a href="#3-%e5%ae%9e%e6%88%98%e8%bf%81%e7%a7%bb%e5%bb%ba%e8%ae%ae" class="header-anchor"&gt;&lt;/a&gt;3. 实战迁移建议
&lt;/h3&gt;&lt;p&gt;从 MySQL 迁到 OceanBase 时，不要只做“能不能连上”：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先验证协议层&lt;/strong&gt;：驱动、连接池、ORM&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;再验证 SQL 层&lt;/strong&gt;：慢 SQL、锁、事务边界、分页、HINT&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;再验证运维层&lt;/strong&gt;：备份恢复、监控指标、扩缩容、租户资源&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最后验证边界语法&lt;/strong&gt;：冷门函数、系统表、复制相关特性、特定锁语法&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;一句话：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;兼容的是“MySQL 使用体验”，不是“MySQL 内核实现”。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="八对象模型对比database-相同边界不同"&gt;&lt;a href="#%e5%85%ab%e5%af%b9%e8%b1%a1%e6%a8%a1%e5%9e%8b%e5%af%b9%e6%af%94database-%e7%9b%b8%e5%90%8c%e8%be%b9%e7%95%8c%e4%b8%8d%e5%90%8c" class="header-anchor"&gt;&lt;/a&gt;八、对象模型对比：database 相同，边界不同
&lt;/h2&gt;&lt;h3 id="mysql"&gt;&lt;a href="#mysql" class="header-anchor"&gt;&lt;/a&gt;MySQL
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Instance
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Database
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Table / View / Procedure
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;权限、连接、资源，常常都围绕“实例 + 账号”展开。&lt;/p&gt;
&lt;h3 id="oceanbase"&gt;&lt;a href="#oceanbase" class="header-anchor"&gt;&lt;/a&gt;OceanBase
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Cluster
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Tenant（资源与权限边界更强）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Database
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── Table / Index / Procedure
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── sys 租户（集群元数据）
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对 MySQL 同学最实用的迁移记忆：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;你在 MySQL 想做的事&lt;/th&gt;
					&lt;th&gt;在 OceanBase 更常见的对应&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;新建一个业务实例&lt;/td&gt;
					&lt;td&gt;新建一个业务租户&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;在实例里建库&lt;/td&gt;
					&lt;td&gt;在租户里 &lt;code&gt;CREATE DATABASE&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;给业务分配 CPU/内存&lt;/td&gt;
					&lt;td&gt;调整 Unit Config / Resource Pool&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;连业务库&lt;/td&gt;
					&lt;td&gt;连 &lt;code&gt;user@tenant&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;做高可用&lt;/td&gt;
					&lt;td&gt;规划 Zone / 副本 / 主备切换，而不是只配主从&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="九一个最小对比实验帮助真正理解差异"&gt;&lt;a href="#%e4%b9%9d%e4%b8%80%e4%b8%aa%e6%9c%80%e5%b0%8f%e5%af%b9%e6%af%94%e5%ae%9e%e9%aa%8c%e5%b8%ae%e5%8a%a9%e7%9c%9f%e6%ad%a3%e7%90%86%e8%a7%a3%e5%b7%ae%e5%bc%82" class="header-anchor"&gt;&lt;/a&gt;九、一个最小对比实验（帮助真正理解差异）
&lt;/h2&gt;&lt;h3 id="实验-a同一套-crud"&gt;&lt;a href="#%e5%ae%9e%e9%aa%8c-a%e5%90%8c%e4%b8%80%e5%a5%97-crud" class="header-anchor"&gt;&lt;/a&gt;实验 A：同一套 CRUD
&lt;/h3&gt;&lt;p&gt;在 OceanBase MySQL 模式租户执行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DATABASE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DEFAULT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;CHARACTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;utf8mb4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;USE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;shop&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;DECIMAL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PAID&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;39&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;CREATED&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PAID&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;你会发现：&lt;br&gt;
&lt;strong&gt;如果只看 SQL，这和 MySQL 几乎没区别。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="实验-b连接串差异"&gt;&lt;a href="#%e5%ae%9e%e9%aa%8c-b%e8%bf%9e%e6%8e%a5%e4%b8%b2%e5%b7%ae%e5%bc%82" class="header-anchor"&gt;&lt;/a&gt;实验 B：连接串差异
&lt;/h3&gt;&lt;p&gt;MySQL：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mysql -h127.0.0.1 -P3306 -uroot -p shop
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;OceanBase 直连某租户：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mysql -h127.0.0.1 -P2881 -uroot@test -p shop
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;OceanBase 经 ODP：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mysql -h127.0.0.1 -P2883 -uroot@test#cluster_name -p shop
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;你会发现：&lt;br&gt;
&lt;strong&gt;真正拉开差距的，是“你连到了哪里”和“资源边界在哪里”。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id="实验-c多租户隔离"&gt;&lt;a href="#%e5%ae%9e%e9%aa%8c-c%e5%a4%9a%e7%a7%9f%e6%88%b7%e9%9a%94%e7%a6%bb" class="header-anchor"&gt;&lt;/a&gt;实验 C：多租户隔离
&lt;/h3&gt;&lt;p&gt;在 &lt;code&gt;sys&lt;/code&gt; 中创建两个业务租户后：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;root@tenant1&lt;/code&gt; 里建表&lt;/li&gt;
&lt;li&gt;&lt;code&gt;root@tenant2&lt;/code&gt; 里看不到那张表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;官方多租户示例正是用这种方式证明：&lt;br&gt;
&lt;strong&gt;租户之间资源、数据、权限隔离。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这比 MySQL 里“两个 database 互相授权”要更接近“两个实例”。&lt;/p&gt;
&lt;h2 id="十什么时候适合用-oceanbase什么时候继续-mysql"&gt;&lt;a href="#%e5%8d%81%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e9%80%82%e5%90%88%e7%94%a8-oceanbase%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e7%bb%a7%e7%bb%ad-mysql" class="header-anchor"&gt;&lt;/a&gt;十、什么时候适合用 OceanBase，什么时候继续 MySQL
&lt;/h2&gt;&lt;h3 id="更适合考虑-oceanbase-的场景"&gt;&lt;a href="#%e6%9b%b4%e9%80%82%e5%90%88%e8%80%83%e8%99%91-oceanbase-%e7%9a%84%e5%9c%ba%e6%99%af" class="header-anchor"&gt;&lt;/a&gt;更适合考虑 OceanBase 的场景
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;需要水平扩展，单机 MySQL + 分库分表成本高&lt;/li&gt;
&lt;li&gt;需要更强的金融级高可用与多副本一致性&lt;/li&gt;
&lt;li&gt;希望一套系统兼顾 TP 与部分 AP&lt;/li&gt;
&lt;li&gt;多业务要强隔离，但又想统一运维平台（多租户）&lt;/li&gt;
&lt;li&gt;已有大量 MySQL 协议应用，希望降低改造成本&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="更适合继续-mysql-的场景"&gt;&lt;a href="#%e6%9b%b4%e9%80%82%e5%90%88%e7%bb%a7%e7%bb%ad-mysql-%e7%9a%84%e5%9c%ba%e6%99%af" class="header-anchor"&gt;&lt;/a&gt;更适合继续 MySQL 的场景
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;单机或轻量业务，数据量和可用性要求不高&lt;/li&gt;
&lt;li&gt;团队只熟悉传统 mysqld 运维，短期不想引入分布式复杂度&lt;/li&gt;
&lt;li&gt;深度依赖某些 MySQL 专有行为 / 插件 / 生态工具且未验证兼容&lt;/li&gt;
&lt;li&gt;本地开发只需要极轻量依赖（虽然也可用 OceanBase Docker，但 MySQL 更轻）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="十一学习路线建议"&gt;&lt;a href="#%e5%8d%81%e4%b8%80%e5%ad%a6%e4%b9%a0%e8%b7%af%e7%ba%bf%e5%bb%ba%e8%ae%ae" class="header-anchor"&gt;&lt;/a&gt;十一、学习路线建议
&lt;/h2&gt;&lt;p&gt;如果你是 MySQL 背景，建议按这个顺序学：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先跑起来&lt;/strong&gt;：Docker / &lt;code&gt;obd demo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先会连&lt;/strong&gt;：&lt;code&gt;user@tenant&lt;/code&gt;、&lt;code&gt;2881&lt;/code&gt; / &lt;code&gt;2883&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先当 MySQL 用&lt;/strong&gt;：建库建表 CRUD&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;再学租户与资源&lt;/strong&gt;：Unit / Resource Pool / sys 与普通租户&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;再学分布式&lt;/strong&gt;：分区、副本、Zone、OBProxy 路由&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最后做迁移评估&lt;/strong&gt;：兼容性清单 + 压测 + 备份恢复演练&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;配套阅读：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本站另一篇：&lt;a class="link" href="https://blog.waihost.com/posts/oceanbase-multi-platform-setup-guide/" &gt;OceanBase 多平台搭建教程&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;官方 MySQL 兼容性文档&lt;/li&gt;
&lt;li&gt;官方系统架构与多租户文档&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="十二速查对照表"&gt;&lt;a href="#%e5%8d%81%e4%ba%8c%e9%80%9f%e6%9f%a5%e5%af%b9%e7%85%a7%e8%a1%a8" class="header-anchor"&gt;&lt;/a&gt;十二、速查对照表
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;主题&lt;/th&gt;
					&lt;th&gt;MySQL&lt;/th&gt;
					&lt;th&gt;OceanBase&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;默认连接&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;root@host:3306&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;root@tenant@host:2881&lt;/code&gt; 或经 ODP &lt;code&gt;2883&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;业务隔离&lt;/td&gt;
					&lt;td&gt;database / 多实例&lt;/td&gt;
					&lt;td&gt;tenant&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;管理入口&lt;/td&gt;
					&lt;td&gt;实例管理员&lt;/td&gt;
					&lt;td&gt;常先登 &lt;code&gt;sys&lt;/code&gt; 做集群与租户管理&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;扩展&lt;/td&gt;
					&lt;td&gt;加配 / 分库分表&lt;/td&gt;
					&lt;td&gt;分区 + 多节点 + 资源单元&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;高可用&lt;/td&gt;
					&lt;td&gt;复制拓扑&lt;/td&gt;
					&lt;td&gt;Multi-Paxos 多副本&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;SQL 习惯&lt;/td&gt;
					&lt;td&gt;原生 MySQL&lt;/td&gt;
					&lt;td&gt;高度兼容，但要做差异验证&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;社区版模式&lt;/td&gt;
					&lt;td&gt;MySQL&lt;/td&gt;
					&lt;td&gt;仅 MySQL 模式&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;OceanBase 对 MySQL 同学并不“难在 SQL”，而难在&lt;strong&gt;换模型&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把“实例”理解升级成“集群 + 租户”&lt;/li&gt;
&lt;li&gt;把“连上 mysqld”升级成“连上正确的租户入口”&lt;/li&gt;
&lt;li&gt;把“兼容 MySQL”理解成“协议与常用 SQL 友好，不等于内核相同”&lt;/li&gt;
&lt;li&gt;先用 CRUD 建立信心，再用租户、分区、副本理解它的分布式能力&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;掌握这四步后，你就不会再把 OceanBase 误当成“换了皮的 MySQL”，也能更准确判断它该用在什么地方。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;OceanBase 官方仓库 README（中英文，MySQL 兼容与快速开始）：https://github.com/oceanbase/oceanbase&lt;/li&gt;
&lt;li&gt;OceanBase 官方文档：系统架构（&lt;code&gt;oceanbase-doc&lt;/code&gt; / V4.3.5）&lt;/li&gt;
&lt;li&gt;OceanBase 官方文档：与 MySQL 兼容性对比&lt;/li&gt;
&lt;li&gt;OceanBase 官方文档：SQL 基础操作（MySQL 模式）&lt;/li&gt;
&lt;li&gt;OceanBase 官方文档：体验多租户特性&lt;/li&gt;
&lt;li&gt;OceanBase 官方文档：通过 MySQL 客户端连接 OceanBase 租户&lt;/li&gt;
&lt;li&gt;OceanBase Docker 镜像：https://hub.docker.com/r/oceanbase/oceanbase-ce&lt;/li&gt;
&lt;li&gt;OceanBase 中文文档中心：https://www.oceanbase.com/docs/oceanbase-database-cn&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Spring 事务传播机制：REQUIRED、REQUIRES_NEW 与嵌套事务工程实践</title><link>https://blog.waihost.com/posts/spring-transaction-propagation-practice/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/spring-transaction-propagation-practice/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/spring-transaction-propagation-practice.svg" alt="Featured image of post Spring 事务传播机制：REQUIRED、REQUIRES_NEW 与嵌套事务工程实践" /&gt;&lt;p&gt;业务代码里最容易“看起来对、跑起来却不对”的一类问题，就是 &lt;strong&gt;事务边界&lt;/strong&gt;。方法上贴了 &lt;code&gt;@Transactional&lt;/code&gt;，并不等于“想回滚就一定回滚、想独立提交就一定独立提交”。真正决定行为的，是 &lt;strong&gt;传播行为（propagation）&lt;/strong&gt;、&lt;strong&gt;物理事务 vs 逻辑事务&lt;/strong&gt;、以及 Spring 默认的 &lt;strong&gt;代理拦截模型&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;本文基于 Spring Framework 官方事务文档，把 &lt;code&gt;REQUIRED&lt;/code&gt; / &lt;code&gt;REQUIRES_NEW&lt;/code&gt; / &lt;code&gt;NESTED&lt;/code&gt; 等传播语义、回滚规则与常见工程踩坑讲清楚，并给出可落地的写法与排查清单。&lt;/p&gt;
&lt;h2 id="一先分清物理事务-vs-逻辑事务"&gt;&lt;a href="#%e4%b8%80%e5%85%88%e5%88%86%e6%b8%85%e7%89%a9%e7%90%86%e4%ba%8b%e5%8a%a1-vs-%e9%80%bb%e8%be%91%e4%ba%8b%e5%8a%a1" class="header-anchor"&gt;&lt;/a&gt;一、先分清：物理事务 vs 逻辑事务
&lt;/h2&gt;&lt;p&gt;Spring 管理事务时，不能只看“有没有 &lt;code&gt;@Transactional&lt;/code&gt;”，而要区分两层：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;概念&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;工程直觉&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;物理事务（physical）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;底层资源上真正开启的事务（如一条 JDBC Connection 上的 &lt;code&gt;begin/commit/rollback&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;能不能真正提交/回滚、锁何时释放&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;逻辑事务（logical）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每个被 Spring 事务拦截的方法作用域&lt;/td&gt;
					&lt;td&gt;该方法能否单独标记 &lt;code&gt;rollback-only&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;传播行为决定的是：&lt;strong&gt;外层方法与内层方法之间，逻辑作用域如何映射到物理事务&lt;/strong&gt;。&lt;br&gt;
这正是很多“内层抛异常后外层还以为自己 commit 成功了”的根源。&lt;/p&gt;
&lt;h2 id="二七种传播行为速查"&gt;&lt;a href="#%e4%ba%8c%e4%b8%83%e7%a7%8d%e4%bc%a0%e6%92%ad%e8%a1%8c%e4%b8%ba%e9%80%9f%e6%9f%a5" class="header-anchor"&gt;&lt;/a&gt;二、七种传播行为速查
&lt;/h2&gt;&lt;p&gt;&lt;code&gt;@Transactional(propagation = …)&lt;/code&gt; 对应 &lt;code&gt;TransactionDefinition&lt;/code&gt; 中的常量。按“有没有外层事务”理解最稳：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;传播行为&lt;/th&gt;
					&lt;th&gt;无外层事务时&lt;/th&gt;
					&lt;th&gt;有外层事务时&lt;/th&gt;
					&lt;th&gt;典型用途&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;REQUIRED&lt;/code&gt;（默认）&lt;/td&gt;
					&lt;td&gt;新建物理事务&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;加入&lt;/strong&gt;外层同一物理事务&lt;/td&gt;
					&lt;td&gt;服务门面 + 多仓储同事务&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;SUPPORTS&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;非事务执行&lt;/td&gt;
					&lt;td&gt;加入外层事务&lt;/td&gt;
					&lt;td&gt;查询既可独立也可参与写事务&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;MANDATORY&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;抛异常&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;加入外层事务&lt;/td&gt;
					&lt;td&gt;强制调用方必须已开事务&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;REQUIRES_NEW&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;新建物理事务&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;挂起外层&lt;/strong&gt;，新建独立物理事务&lt;/td&gt;
					&lt;td&gt;审计日志、独立提交的状态点&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;NOT_SUPPORTED&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;非事务执行&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;挂起外层&lt;/strong&gt;，非事务执行&lt;/td&gt;
					&lt;td&gt;长耗时/远程调用不想占连接&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;NEVER&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;非事务执行&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;抛异常&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;明确禁止在事务中调用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;NESTED&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;等同 &lt;code&gt;REQUIRED&lt;/code&gt; 新建&lt;/td&gt;
					&lt;td&gt;同一物理事务内建 &lt;strong&gt;savepoint&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;内层可局部回滚，外层可继续&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;日常工程里真正高频的是：&lt;strong&gt;&lt;code&gt;REQUIRED&lt;/code&gt;、&lt;code&gt;REQUIRES_NEW&lt;/code&gt;、&lt;code&gt;NESTED&lt;/code&gt;&lt;/strong&gt;。下面展开它们的语义差异。&lt;/p&gt;
&lt;h2 id="三required默认正确但要懂-unexpectedrollbackexception"&gt;&lt;a href="#%e4%b8%89required%e9%bb%98%e8%ae%a4%e6%ad%a3%e7%a1%ae%e4%bd%86%e8%a6%81%e6%87%82-unexpectedrollbackexception" class="header-anchor"&gt;&lt;/a&gt;三、REQUIRED：默认正确，但要懂 UnexpectedRollbackException
&lt;/h2&gt;&lt;h3 id="1-语义"&gt;&lt;a href="#1-%e8%af%ad%e4%b9%89" class="header-anchor"&gt;&lt;/a&gt;1. 语义
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;PROPAGATION_REQUIRED&lt;/code&gt; 会 &lt;strong&gt;强制存在物理事务&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前没有事务 → 为本方法新建；&lt;/li&gt;
&lt;li&gt;已有外层事务 → &lt;strong&gt;参与&lt;/strong&gt;该外层物理事务（同一连接、同一提交点）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这很适合：一个 Service 编排多个 Repository，要求“要么一起成功，要么一起失败”。&lt;/p&gt;
&lt;h3 id="2-逻辑作用域仍是独立的"&gt;&lt;a href="#2-%e9%80%bb%e8%be%91%e4%bd%9c%e7%94%a8%e5%9f%9f%e4%bb%8d%e6%98%af%e7%8b%ac%e7%ab%8b%e7%9a%84" class="header-anchor"&gt;&lt;/a&gt;2. 逻辑作用域仍是独立的
&lt;/h3&gt;&lt;p&gt;即便映射到同一物理事务，Spring 仍会为每个 &lt;code&gt;REQUIRED&lt;/code&gt; 方法创建 &lt;strong&gt;逻辑事务作用域&lt;/strong&gt;。每个作用域可以独立设置 &lt;code&gt;rollback-only&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;关键后果（官方明确描述）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;内层逻辑事务标记了 rollback-only；&lt;/li&gt;
&lt;li&gt;外层自己没有决定回滚，仍尝试 commit；&lt;/li&gt;
&lt;li&gt;物理事务最终回滚，外层会收到 &lt;strong&gt;&lt;code&gt;UnexpectedRollbackException&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这不是“Spring 抽风”，而是保护调用方 &lt;strong&gt;绝不能误以为已经提交成功&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="3-内层-isolation--timeout--readonly-默认会被忽略"&gt;&lt;a href="#3-%e5%86%85%e5%b1%82-isolation--timeout--readonly-%e9%bb%98%e8%ae%a4%e4%bc%9a%e8%a2%ab%e5%bf%bd%e7%95%a5" class="header-anchor"&gt;&lt;/a&gt;3. 内层 isolation / timeout / readOnly 默认会被忽略
&lt;/h3&gt;&lt;p&gt;参与外层事务时，内层声明的隔离级别、超时、只读标志 &lt;strong&gt;默认静默忽略&lt;/strong&gt;，以外层特征为准。&lt;br&gt;
若希望“内层要求与外层不一致时直接失败”，可在事务管理器上打开 &lt;code&gt;validateExistingTransaction=true&lt;/code&gt;（非宽松模式也会拒绝只读/读写不匹配）。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 伪配置示意：开启后，内层 isolation/readOnly 与外层冲突会拒绝加入&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// PlatformTransactionManager 具体实现上设置 validateExistingTransaction = true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="四requires_new真正的独立事务"&gt;&lt;a href="#%e5%9b%9brequires_new%e7%9c%9f%e6%ad%a3%e7%9a%84%e7%8b%ac%e7%ab%8b%e4%ba%8b%e5%8a%a1" class="header-anchor"&gt;&lt;/a&gt;四、REQUIRES_NEW：真正的“独立事务”
&lt;/h2&gt;&lt;h3 id="1-语义-1"&gt;&lt;a href="#1-%e8%af%ad%e4%b9%89-1" class="header-anchor"&gt;&lt;/a&gt;1. 语义
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;PROPAGATION_REQUIRES_NEW&lt;/code&gt; &lt;strong&gt;总是&lt;/strong&gt;开启独立物理事务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有外层事务 → &lt;strong&gt;挂起&lt;/strong&gt;外层，内层用新资源事务；&lt;/li&gt;
&lt;li&gt;内层 commit/rollback &lt;strong&gt;不影响&lt;/strong&gt;外层事务的提交结果；&lt;/li&gt;
&lt;li&gt;内层结束后，其持有的锁会尽快释放；&lt;/li&gt;
&lt;li&gt;内层可使用自己的 isolation / timeout / readOnly。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-适合什么"&gt;&lt;a href="#2-%e9%80%82%e5%90%88%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;2. 适合什么
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;操作日志 / 审计记录：业务主事务回滚，日志仍要留下；&lt;/li&gt;
&lt;li&gt;状态机“已到达某检查点”的独立落库；&lt;/li&gt;
&lt;li&gt;需要更短锁持有时间、且允许与外层最终结果不一致的写路径。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-连接池风险必须重视"&gt;&lt;a href="#3-%e8%bf%9e%e6%8e%a5%e6%b1%a0%e9%a3%8e%e9%99%a9%e5%bf%85%e9%a1%bb%e9%87%8d%e8%a7%86" class="header-anchor"&gt;&lt;/a&gt;3. 连接池风险（必须重视）
&lt;/h3&gt;&lt;p&gt;官方明确警告：外层事务仍绑定着资源时，内层还要再拿 &lt;strong&gt;新的数据库连接&lt;/strong&gt;。若连接池偏小，可能出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;连接池耗尽；&lt;/li&gt;
&lt;li&gt;多线程都在外层事务中等待内层连接 → &lt;strong&gt;死锁式等待&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实践建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;连接池最大连接数要 &lt;strong&gt;大于并发线程数&lt;/strong&gt;（官方表述：至少比并发线程数多 1，实际应按峰值嵌套深度留余量）；&lt;/li&gt;
&lt;li&gt;不要在循环体里高频 &lt;code&gt;REQUIRES_NEW&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;先问自己：真的需要“独立提交”，还是只是想“内层失败不要拖死外层”——后者可能 &lt;code&gt;NESTED&lt;/code&gt; 更合适。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;AuditService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;auditService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;OrderRepository&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 默认 REQUIRED&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;placeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderCmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 审计希望独立提交：业务失败也不丢“尝试下单”痕迹&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;auditService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;recordAttempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;PLACE_ORDER&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ... 后续业务可能抛异常导致外层回滚&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;markPaid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuditService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;propagation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Propagation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REQUIRES_NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;recordAttempt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 独立物理事务：这里的提交不会被外层回滚“带走”&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="五nested同一物理事务上的-savepoint"&gt;&lt;a href="#%e4%ba%94nested%e5%90%8c%e4%b8%80%e7%89%a9%e7%90%86%e4%ba%8b%e5%8a%a1%e4%b8%8a%e7%9a%84-savepoint" class="header-anchor"&gt;&lt;/a&gt;五、NESTED：同一物理事务上的 savepoint
&lt;/h2&gt;&lt;h3 id="1-语义-2"&gt;&lt;a href="#1-%e8%af%ad%e4%b9%89-2" class="header-anchor"&gt;&lt;/a&gt;1. 语义
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;PROPAGATION_NESTED&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一个物理事务&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;内层通过 &lt;strong&gt;savepoint（保存点）&lt;/strong&gt; 支持局部回滚；&lt;/li&gt;
&lt;li&gt;内层回滚到 savepoint 后，&lt;strong&gt;外层仍可继续&lt;/strong&gt;并最终 commit/rollback。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它通常映射到 &lt;strong&gt;JDBC savepoint&lt;/strong&gt;，因此：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;依赖 JDBC 资源事务；&lt;/li&gt;
&lt;li&gt;与 &lt;code&gt;DataSourceTransactionManager&lt;/code&gt; 一类管理器配合最常见；&lt;/li&gt;
&lt;li&gt;不是所有事务管理器/资源都支持嵌套语义。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-与-requires_new-的选型"&gt;&lt;a href="#2-%e4%b8%8e-requires_new-%e7%9a%84%e9%80%89%e5%9e%8b" class="header-anchor"&gt;&lt;/a&gt;2. 与 REQUIRES_NEW 的选型
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;&lt;code&gt;REQUIRES_NEW&lt;/code&gt;&lt;/th&gt;
					&lt;th&gt;&lt;code&gt;NESTED&lt;/code&gt;&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;物理事务&lt;/td&gt;
					&lt;td&gt;独立两条&lt;/td&gt;
					&lt;td&gt;仍是一条&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;内层回滚&lt;/td&gt;
					&lt;td&gt;不影响外层提交决策&lt;/td&gt;
					&lt;td&gt;只回滚到 savepoint，外层可继续&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;内层提交可见性&lt;/td&gt;
					&lt;td&gt;内层结束即可对外可见（独立提交）&lt;/td&gt;
					&lt;td&gt;仍受外层最终 commit 约束&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;连接占用&lt;/td&gt;
					&lt;td&gt;可能额外占连接&lt;/td&gt;
					&lt;td&gt;一般不另开连接&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;资源支持&lt;/td&gt;
					&lt;td&gt;广泛&lt;/td&gt;
					&lt;td&gt;依赖 savepoint 支持&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;口诀：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;要“内层 &lt;strong&gt;真正提交&lt;/strong&gt;，外层怎么回滚都保留” → &lt;code&gt;REQUIRES_NEW&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;要“内层失败可局部撤销，但最终仍跟外层同生共死” → &lt;code&gt;NESTED&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="六回滚规则默认只认-runtimeexception--error"&gt;&lt;a href="#%e5%85%ad%e5%9b%9e%e6%bb%9a%e8%a7%84%e5%88%99%e9%bb%98%e8%ae%a4%e5%8f%aa%e8%ae%a4-runtimeexception--error" class="header-anchor"&gt;&lt;/a&gt;六、回滚规则：默认只认 RuntimeException / Error
&lt;/h2&gt;&lt;p&gt;声明式事务的默认策略（官方 &lt;code&gt;@Transactional&lt;/code&gt; 默认值）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;传播：&lt;code&gt;REQUIRED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;隔离：&lt;code&gt;ISOLATION_DEFAULT&lt;/code&gt;（交给底层数据源默认）&lt;/li&gt;
&lt;li&gt;读写：可写&lt;/li&gt;
&lt;li&gt;超时：底层默认 / 无&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回滚触发&lt;/strong&gt;：&lt;code&gt;RuntimeException&lt;/code&gt; 与 &lt;code&gt;Error&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认不回滚&lt;/strong&gt;：受检异常（checked &lt;code&gt;Exception&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这解释了一个经典坑：业务方法 &lt;code&gt;throws BusinessException extends Exception&lt;/code&gt;，调用方 catch 后发现 &lt;strong&gt;数据居然提交了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;可显式配置：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rollbackFor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;transfer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;throws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;BusinessException&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 受检业务异常也会触发回滚&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;noRollbackFor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;InventoryNotEnoughException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;reserveStock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;skuId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 某些“预期内”的运行时异常不希望回滚时使用（务必非常克制）&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;也可在 catch 后程序化标记：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NoProductInStockException&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TransactionAspectSupport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentTransactionStatus&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;setRollbackOnly&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;官方仍建议优先用声明式 &lt;code&gt;rollbackFor&lt;/code&gt; / &lt;code&gt;noRollbackFor&lt;/code&gt;，少用手写 &lt;code&gt;setRollbackOnly()&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="七代理模型自调用为什么注解失效"&gt;&lt;a href="#%e4%b8%83%e4%bb%a3%e7%90%86%e6%a8%a1%e5%9e%8b%e8%87%aa%e8%b0%83%e7%94%a8%e4%b8%ba%e4%bb%80%e4%b9%88%e6%b3%a8%e8%a7%a3%e5%a4%b1%e6%95%88" class="header-anchor"&gt;&lt;/a&gt;七、代理模型：自调用为什么“注解失效”
&lt;/h2&gt;&lt;p&gt;默认 &lt;strong&gt;proxy 模式&lt;/strong&gt;（非 AspectJ 编织）下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;只有 &lt;strong&gt;通过代理进来的外部调用&lt;/strong&gt; 才会被拦截；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同类自调用&lt;/strong&gt;（&lt;code&gt;this.otherTxMethod()&lt;/code&gt;）不会走代理 → 内层 &lt;code&gt;@Transactional&lt;/code&gt; &lt;strong&gt;不生效&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;初始化阶段（如 &lt;code&gt;@PostConstruct&lt;/code&gt;）也不应依赖事务代理；&lt;/li&gt;
&lt;li&gt;Spring Framework 6.0 起，基于类的代理默认可对 &lt;code&gt;protected&lt;/code&gt; / 包可见方法生效；但 &lt;strong&gt;基于接口的 JDK 代理&lt;/strong&gt; 仍要求方法在接口中且为 &lt;code&gt;public&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PayService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// 错误：自调用，markPaid 上的 REQUIRES_NEW 不会生效&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;markPaid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nd"&gt;@Transactional&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;propagation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Propagation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;REQUIRES_NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;markPaid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Long&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;常见修法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把需要独立传播的方法拆到 &lt;strong&gt;另一个 Spring Bean&lt;/strong&gt;，通过注入调用；&lt;/li&gt;
&lt;li&gt;注入自身代理（&lt;code&gt;@Lazy&lt;/code&gt; 自注入 / &lt;code&gt;AopContext.currentProxy()&lt;/code&gt;，后者需暴露代理）；&lt;/li&gt;
&lt;li&gt;切换到 AspectJ 模式（成本更高，团队要统一约定）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="八可复现的排查清单"&gt;&lt;a href="#%e5%85%ab%e5%8f%af%e5%a4%8d%e7%8e%b0%e7%9a%84%e6%8e%92%e6%9f%a5%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;八、可复现的排查清单
&lt;/h2&gt;&lt;p&gt;当“事务行为不符合预期”时，按下面顺序收敛：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;是不是自调用？&lt;/strong&gt; 打断点看调用栈是否经过 &lt;code&gt;TransactionInterceptor&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常类型是否触发默认回滚？&lt;/strong&gt; checked vs runtime；是否被上层吞掉。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传播是否是你以为的那个？&lt;/strong&gt; &lt;code&gt;REQUIRED&lt;/code&gt; 加入外层 ≠ &lt;code&gt;REQUIRES_NEW&lt;/code&gt; 独立提交。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;是否出现 &lt;code&gt;UnexpectedRollbackException&lt;/code&gt;？&lt;/strong&gt; 说明内层已 &lt;code&gt;rollback-only&lt;/code&gt;，外层还在 commit。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连接池是否在 &lt;code&gt;REQUIRES_NEW&lt;/code&gt; 下打满？&lt;/strong&gt; 看获取连接耗时与 active connections。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只读事务是否被误用于写？&lt;/strong&gt; 以及是否开启了 &lt;code&gt;validateExistingTransaction&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多数据源 / 非 &lt;code&gt;DataSourceTransactionManager&lt;/code&gt; 场景&lt;/strong&gt; 下 &lt;code&gt;NESTED&lt;/code&gt; 是否根本不被支持。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最小实验建议（单测或本地）：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;场景 A：outer REQUIRED → inner REQUIRED 抛 RuntimeException
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 期望：整体回滚；外层若吞异常仍可能 UnexpectedRollbackException
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;场景 B：outer REQUIRED → inner REQUIRES_NEW 成功后 outer 回滚
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 期望：inner 已提交数据仍在；outer 变更回滚
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;场景 C：同类 this.inner() 且 inner 标 REQUIRES_NEW
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 期望（proxy 默认）：inner 注解不生效，与 outer 同一逻辑路径
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="九工程建议可直接落到-code-review"&gt;&lt;a href="#%e4%b9%9d%e5%b7%a5%e7%a8%8b%e5%bb%ba%e8%ae%ae%e5%8f%af%e7%9b%b4%e6%8e%a5%e8%90%bd%e5%88%b0-code-review" class="header-anchor"&gt;&lt;/a&gt;九、工程建议（可直接落到 Code Review）
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;默认用 &lt;code&gt;REQUIRED&lt;/code&gt;&lt;/strong&gt;，不要到处 &lt;code&gt;REQUIRES_NEW&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REQUIRES_NEW&lt;/code&gt; 只用于“必须独立提交”的窄路径，并评估连接池。&lt;/li&gt;
&lt;li&gt;需要局部回滚且最终仍同事务提交 → 优先评估 &lt;code&gt;NESTED&lt;/code&gt;，并确认 JDBC savepoint 支持。&lt;/li&gt;
&lt;li&gt;业务异常若走 checked，统一 &lt;code&gt;rollbackFor = Exception.class&lt;/code&gt; 或统一业务异常基类为 runtime。&lt;/li&gt;
&lt;li&gt;事务边界放在 &lt;strong&gt;应用服务层&lt;/strong&gt;，Repository 保持细粒度；避免 Controller 直接开事务导致边界失控。&lt;/li&gt;
&lt;li&gt;长耗时远程调用不要包在大事务里；必要时 &lt;code&gt;NOT_SUPPORTED&lt;/code&gt; 或先缩短事务再调外部。&lt;/li&gt;
&lt;li&gt;Code Review 必问：&lt;strong&gt;谁提交？谁回滚？失败时用户看到的状态与 DB 是否一致？&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="十总结"&gt;&lt;a href="#%e5%8d%81%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;十、总结
&lt;/h2&gt;&lt;p&gt;Spring 事务传播不是注解装饰，而是 &lt;strong&gt;逻辑作用域如何映射到物理资源事务&lt;/strong&gt; 的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;REQUIRED&lt;/code&gt;：共享物理事务，内层 &lt;code&gt;rollback-only&lt;/code&gt; 会拖垮外层提交预期；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REQUIRES_NEW&lt;/code&gt;：独立物理事务，能独立提交，但要付连接与一致性代价；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NESTED&lt;/code&gt;：同一物理事务上的 savepoint，适合“局部失败、整体仍可继续”的路径；&lt;/li&gt;
&lt;li&gt;回滚默认只盯 &lt;code&gt;RuntimeException&lt;/code&gt;/&lt;code&gt;Error&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;代理模式下 &lt;strong&gt;自调用不走事务拦截&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这五件事吃透，线上大部分“事务没生效 / 该回没回 / 不该提交却提交”的问题都能快速定位。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Spring Framework Reference — &lt;a class="link" href="https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-propagation.html" target="_blank" rel="noopener"
 &gt;Transaction Propagation&lt;/a&gt;（&lt;code&gt;REQUIRED&lt;/code&gt; / &lt;code&gt;REQUIRES_NEW&lt;/code&gt; / &lt;code&gt;NESTED&lt;/code&gt; 语义与 &lt;code&gt;UnexpectedRollbackException&lt;/code&gt;、连接池警告）&lt;/li&gt;
&lt;li&gt;Spring Framework Reference — &lt;a class="link" href="https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/rolling-back.html" target="_blank" rel="noopener"
 &gt;Rolling Back a Declarative Transaction&lt;/a&gt;（默认 RuntimeException/Error 回滚、&lt;code&gt;rollbackFor&lt;/code&gt;/&lt;code&gt;setRollbackOnly&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;Spring Framework Reference — &lt;a class="link" href="https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html" target="_blank" rel="noopener"
 &gt;Using @Transactional&lt;/a&gt;（默认传播/隔离/回滚规则、proxy 自调用限制、可见性）&lt;/li&gt;
&lt;li&gt;Spring Framework Javadoc — &lt;a class="link" href="https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/TransactionDefinition.html" target="_blank" rel="noopener"
 &gt;TransactionDefinition&lt;/a&gt;（七种 &lt;code&gt;PROPAGATION_*&lt;/code&gt; 常量定义）&lt;/li&gt;
&lt;/ol&gt;</description></item><item><title>HTTP 条件请求与缓存协商：ETag、Last-Modified 与 Cache-Control 工程实践</title><link>https://blog.waihost.com/posts/http-conditional-requests-cache-negotiation/</link><pubDate>Wed, 15 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/http-conditional-requests-cache-negotiation/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/http-conditional-requests-cache-negotiation.svg" alt="Featured image of post HTTP 条件请求与缓存协商：ETag、Last-Modified 与 Cache-Control 工程实践" /&gt;&lt;p&gt;很多系统的“慢”并不在业务逻辑，而在重复传输不变的响应体。浏览器、CDN、反向代理都可能持有一份旧副本；真正关键的是：&lt;strong&gt;这份副本还能不能直接用？如果不能直接用，能否只问服务器“变了没有”？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;HTTP 把这件事拆成两层：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;新鲜度（freshness）&lt;/strong&gt;：缓存还在不在有效期内，决定要不要立刻回源；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证（validation）&lt;/strong&gt;：过期或必须再确认时，用条件请求做协商，成功则返回 &lt;code&gt;304 Not Modified&lt;/code&gt;，避免再次传输实体。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;本文基于 HTTP 语义规范（RFC 9110）与 HTTP 缓存规范（RFC 9111），结合 Nginx 能力，讲清楚 &lt;code&gt;Cache-Control&lt;/code&gt;、&lt;code&gt;ETag&lt;/code&gt; / &lt;code&gt;Last-Modified&lt;/code&gt;、&lt;code&gt;If-None-Match&lt;/code&gt; / &lt;code&gt;If-Modified-Since&lt;/code&gt; 的协作关系，以及可落地的配置与排错方法。&lt;/p&gt;
&lt;h2 id="一先分清存下来--可以直接用"&gt;&lt;a href="#%e4%b8%80%e5%85%88%e5%88%86%e6%b8%85%e5%ad%98%e4%b8%8b%e6%9d%a5--%e5%8f%af%e4%bb%a5%e7%9b%b4%e6%8e%a5%e7%94%a8" class="header-anchor"&gt;&lt;/a&gt;一、先分清：存下来 ≠ 可以直接用
&lt;/h2&gt;&lt;p&gt;RFC 9111 把缓存决策大致分成：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;阶段&lt;/th&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;关键概念&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;存储（store）&lt;/td&gt;
					&lt;td&gt;响应能不能进缓存&lt;/td&gt;
					&lt;td&gt;方法、状态码、&lt;code&gt;no-store&lt;/code&gt;、&lt;code&gt;private&lt;/code&gt;、&lt;code&gt;Authorization&lt;/code&gt; 等&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;新鲜度（fresh）&lt;/td&gt;
					&lt;td&gt;已存副本是否仍 fresh&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;max-age&lt;/code&gt; / &lt;code&gt;s-maxage&lt;/code&gt; / &lt;code&gt;Expires&lt;/code&gt; / Age&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;验证（validate）&lt;/td&gt;
					&lt;td&gt;stale 后能否复用&lt;/td&gt;
					&lt;td&gt;条件请求 + 校验器（validator）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;复用（reuse）&lt;/td&gt;
					&lt;td&gt;最终回什么&lt;/td&gt;
					&lt;td&gt;200 全量、304 无 body、或回源新响应&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;常见误解：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;no-cache&lt;/code&gt; 不是“禁止缓存”&lt;/strong&gt;。它表示：可以用存储副本，但&lt;strong&gt;必须先成功验证&lt;/strong&gt;后才能拿去满足其他请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;no-store&lt;/code&gt; 才是“不要存”&lt;/strong&gt;。缓存不得存储该请求/响应的任何部分，也不能拿它去满足别的请求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;private&lt;/code&gt; 不是“浏览器也不能缓存”&lt;/strong&gt;。它限制的是&lt;strong&gt;共享缓存&lt;/strong&gt;（CDN、公司代理）：响应面向单个用户；私有缓存（浏览器）仍可在规则允许下存储。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把语义说清楚，后面的工程配置才不会互相打架。&lt;/p&gt;
&lt;h2 id="二新鲜度cache-control-的核心指令"&gt;&lt;a href="#%e4%ba%8c%e6%96%b0%e9%b2%9c%e5%ba%a6cache-control-%e7%9a%84%e6%a0%b8%e5%bf%83%e6%8c%87%e4%bb%a4" class="header-anchor"&gt;&lt;/a&gt;二、新鲜度：Cache-Control 的核心指令
&lt;/h2&gt;&lt;h3 id="1-响应侧最常用指令"&gt;&lt;a href="#1-%e5%93%8d%e5%ba%94%e4%be%a7%e6%9c%80%e5%b8%b8%e7%94%a8%e6%8c%87%e4%bb%a4" class="header-anchor"&gt;&lt;/a&gt;1. 响应侧最常用指令
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;指令&lt;/th&gt;
					&lt;th&gt;含义（工程视角）&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;max-age=N&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;响应在 age &amp;gt; N 秒后视为 stale&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;s-maxage=N&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;只对共享缓存&lt;/strong&gt;覆盖 &lt;code&gt;max-age&lt;/code&gt;/&lt;code&gt;Expires&lt;/code&gt;；并带有必须再验证后才能复用 stale 的语义&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;public&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;明确允许缓存（例如带 &lt;code&gt;Authorization&lt;/code&gt; 的响应在共享缓存中的场景）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;private&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;禁止共享缓存存储（或限定字段）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;no-cache&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;使用前必须验证&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;no-store&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;不要存储&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;must-revalidate&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;一旦 stale，&lt;strong&gt;不得&lt;/strong&gt;在未验证成功前复用；断连时更应报错而不是偷偷给 stale&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一个可记的分层：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;# 静态资源：可公开、长期缓存（内容指纹化后）
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: public, max-age=31536000, immutable
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;# HTML 入口：可缓存但每次用前确认
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: no-cache
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;# 登录态 API：不要被中间层存
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: private, no-store
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;# 对一致性要求高的共享缓存内容
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: public, max-age=60, must-revalidate
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="2-age-与看起来刚生成"&gt;&lt;a href="#2-age-%e4%b8%8e%e7%9c%8b%e8%b5%b7%e6%9d%a5%e5%88%9a%e7%94%9f%e6%88%90" class="header-anchor"&gt;&lt;/a&gt;2. Age 与“看起来刚生成”
&lt;/h3&gt;&lt;p&gt;响应里的 &lt;code&gt;Age&lt;/code&gt; 表示估计已在缓存中停留的秒数。共享缓存命中时，客户端看到的“年龄”往往不是 0。排查“为什么我设了 &lt;code&gt;max-age=60&lt;/code&gt;，却总在提前回源”时，要同时看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源站给出的 &lt;code&gt;Cache-Control&lt;/code&gt; / &lt;code&gt;Expires&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;中间层是否改写了头&lt;/li&gt;
&lt;li&gt;响应是否已有较大 &lt;code&gt;Age&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="3-启发式新鲜度别依赖没写过期时间也碰巧能缓存"&gt;&lt;a href="#3-%e5%90%af%e5%8f%91%e5%bc%8f%e6%96%b0%e9%b2%9c%e5%ba%a6%e5%88%ab%e4%be%9d%e8%b5%96%e6%b2%a1%e5%86%99%e8%bf%87%e6%9c%9f%e6%97%b6%e9%97%b4%e4%b9%9f%e7%a2%b0%e5%b7%a7%e8%83%bd%e7%bc%93%e5%ad%98" class="header-anchor"&gt;&lt;/a&gt;3. 启发式新鲜度：别依赖“没写过期时间也碰巧能缓存”
&lt;/h3&gt;&lt;p&gt;没有显式过期信息时，部分缓存会用启发式算法估算 fresh 时间。这在不同实现间差异大，生产环境应&lt;strong&gt;显式给出 freshness&lt;/strong&gt;，不要赌默认行为。&lt;/p&gt;
&lt;h2 id="三校验器etag-与-last-modified"&gt;&lt;a href="#%e4%b8%89%e6%a0%a1%e9%aa%8c%e5%99%a8etag-%e4%b8%8e-last-modified" class="header-anchor"&gt;&lt;/a&gt;三、校验器：ETag 与 Last-Modified
&lt;/h2&gt;&lt;p&gt;验证阶段依赖&lt;strong&gt;表示元数据（representation metadata）&lt;/strong&gt;，最常见两类：&lt;/p&gt;
&lt;h3 id="1-last-modified"&gt;&lt;a href="#1-last-modified" class="header-anchor"&gt;&lt;/a&gt;1. &lt;code&gt;Last-Modified&lt;/code&gt;
&lt;/h3&gt;&lt;p&gt;源站认为所选表示&lt;strong&gt;最后修改&lt;/strong&gt;的时间（HTTP-date）。例如：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;优点：实现简单，文件系统/对象存储天然具备 mtime。&lt;br&gt;
局限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;秒级精度，一秒内多次变更可能撞车；&lt;/li&gt;
&lt;li&gt;时钟回拨、多副本 mtime 不一致会误判；&lt;/li&gt;
&lt;li&gt;内容没变但元数据变了时，语义可能不够准。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-etag实体标签"&gt;&lt;a href="#2-etag%e5%ae%9e%e4%bd%93%e6%a0%87%e7%ad%be" class="header-anchor"&gt;&lt;/a&gt;2. &lt;code&gt;ETag&lt;/code&gt;（实体标签）
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;ETag&lt;/code&gt; 是源站为某个表示分配的不透明标签：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;ETag: &amp;#34;xyzzy&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;ETag: W/&amp;#34;xyzzy&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;强校验器（strong）&lt;/strong&gt;：表示数据（以及会影响 200 内容的关键元数据）变化时标签应变化。适合字节级精确比较。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;弱校验器（weak，&lt;code&gt;W/&lt;/code&gt; 前缀）&lt;/strong&gt;：语义等价即可，即使字节不完全相同。生成成本更低，但比较能力更弱。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RFC 9110 明确：在不便存修改时间、一秒精度不够、或修改时间维护不一致时，&lt;strong&gt;ETag 通常比 Last-Modified 更可靠&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;工程上常见生成策略：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;策略&lt;/th&gt;
					&lt;th&gt;适用&lt;/th&gt;
					&lt;th&gt;注意&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;内容哈希（SHA-256 截断）&lt;/td&gt;
					&lt;td&gt;静态文件、构建产物&lt;/td&gt;
					&lt;td&gt;强 ETag，成本随体积上升&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;版本号 / 修订号&lt;/td&gt;
					&lt;td&gt;业务资源、CMS&lt;/td&gt;
					&lt;td&gt;发布流程必须保证“内容变则版本变”&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;inode + size + mtime&lt;/td&gt;
					&lt;td&gt;传统文件服务器&lt;/td&gt;
					&lt;td&gt;迁移/复制后可能变，跨机一致性差&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;弱 ETag（模板渲染摘要）&lt;/td&gt;
					&lt;td&gt;HTML 片段语义不变&lt;/td&gt;
					&lt;td&gt;不要拿去当字节级一致性保证&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="四条件请求如何问变了没有"&gt;&lt;a href="#%e5%9b%9b%e6%9d%a1%e4%bb%b6%e8%af%b7%e6%b1%82%e5%a6%82%e4%bd%95%e9%97%ae%e5%8f%98%e4%ba%86%e6%b2%a1%e6%9c%89" class="header-anchor"&gt;&lt;/a&gt;四、条件请求：如何问“变了没有”
&lt;/h2&gt;&lt;h3 id="1-if-none-match优先基于-etag"&gt;&lt;a href="#1-if-none-match%e4%bc%98%e5%85%88%e5%9f%ba%e4%ba%8e-etag" class="header-anchor"&gt;&lt;/a&gt;1. &lt;code&gt;If-None-Match&lt;/code&gt;（优先，基于 ETag）
&lt;/h3&gt;&lt;p&gt;客户端/缓存带上已存表示的 ETag：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/app.js&lt;/span&gt; &lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;example.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;If-None-Match&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;&amp;#34;33a64df551425fcc55e4d42a148795d9f25f89d4&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;源站比较当前 ETag：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;匹配&lt;/strong&gt;（表示未变）→ &lt;code&gt;304 Not Modified&lt;/code&gt;，通常无 body；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不匹配&lt;/strong&gt; → &lt;code&gt;200 OK&lt;/code&gt; + 新表示（含新的 ETag）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对 &lt;code&gt;If-None-Match&lt;/code&gt;，规范要求使用&lt;strong&gt;弱比较函数&lt;/strong&gt;：即使只有弱 ETag，也可用于缓存验证。&lt;/p&gt;
&lt;h3 id="2-if-modified-since基于时间"&gt;&lt;a href="#2-if-modified-since%e5%9f%ba%e4%ba%8e%e6%97%b6%e9%97%b4" class="header-anchor"&gt;&lt;/a&gt;2. &lt;code&gt;If-Modified-Since&lt;/code&gt;（基于时间）
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;GET&lt;/span&gt; &lt;span class="nn"&gt;/style.css&lt;/span&gt; &lt;span class="kr"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;If-Modified-Since&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="l"&gt;Sat, 29 Oct 1994 19:43:31 GMT&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;若所选表示的修改时间不比该日期更新，则不必再传实体。&lt;/p&gt;
&lt;h3 id="3-两者同时出现时谁说了算"&gt;&lt;a href="#3-%e4%b8%a4%e8%80%85%e5%90%8c%e6%97%b6%e5%87%ba%e7%8e%b0%e6%97%b6%e8%b0%81%e8%af%b4%e4%ba%86%e7%ae%97" class="header-anchor"&gt;&lt;/a&gt;3. 两者同时出现时谁说了算？
&lt;/h3&gt;&lt;p&gt;RFC 9110 规定：若请求里带了 &lt;code&gt;If-None-Match&lt;/code&gt;，接收方&lt;strong&gt;必须忽略&lt;/strong&gt; &lt;code&gt;If-Modified-Since&lt;/code&gt;——因为实体标签被认为是更准确的替代条件。两者同时出现多是兼容历史客户端；服务端实现应按规范优先级处理。&lt;/p&gt;
&lt;h3 id="4-304-not-modified-到底省了什么"&gt;&lt;a href="#4-304-not-modified-%e5%88%b0%e5%ba%95%e7%9c%81%e4%ba%86%e4%bb%80%e4%b9%88" class="header-anchor"&gt;&lt;/a&gt;4. &lt;code&gt;304 Not Modified&lt;/code&gt; 到底省了什么
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;304&lt;/code&gt; 表示：若没有条件约束，这次本应是 &lt;code&gt;200&lt;/code&gt;；但条件为假（未修改），客户端应继续使用本地已存表示。&lt;/p&gt;
&lt;p&gt;源站仍应带上在同等 &lt;code&gt;200&lt;/code&gt; 中会发送的关键头（如 &lt;code&gt;ETag&lt;/code&gt;、&lt;code&gt;Date&lt;/code&gt;、&lt;code&gt;Content-Location&lt;/code&gt; 等），以便缓存&lt;strong&gt;刷新元数据&lt;/strong&gt;（freshen），而不只是“空响应”。&lt;/p&gt;
&lt;p&gt;流量层面：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;首次：200 + 完整 body（例如 800KB）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;之后未变更：304 + 几乎无 body（通常几百字节头）
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对静态资源、移动端弱网、跨境回源，收益非常明显。&lt;/p&gt;
&lt;h2 id="五一张图串起完整路径"&gt;&lt;a href="#%e4%ba%94%e4%b8%80%e5%bc%a0%e5%9b%be%e4%b8%b2%e8%b5%b7%e5%ae%8c%e6%95%b4%e8%b7%af%e5%be%84" class="header-anchor"&gt;&lt;/a&gt;五、一张图串起完整路径
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Client/CDN 本地有副本？
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | 无 -----&amp;gt; 普通 GET -----&amp;gt; 200 + body + Cache-Control + ETag/LM
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | 有且 fresh -----&amp;gt; 直接复用（不回源）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; | 有但 stale / 或 no-cache
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; v
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 条件请求：If-None-Match / If-Modified-Since
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +--&amp;gt; 未变：304（刷新头，复用旧 body）
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +--&amp;gt; 已变：200（新 body + 新校验器）
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;记住两句话：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;fresh 解决“要不要问”&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;validator 解决“问了以后要不要传 body”&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;只有 &lt;code&gt;max-age&lt;/code&gt; 没有校验器，过期后往往只能全量回源；只有 ETag 没有合理 freshness，会变成“每次都协商”，仍然浪费 RTT。&lt;/p&gt;
&lt;h2 id="六工程实践按资源类型给策略"&gt;&lt;a href="#%e5%85%ad%e5%b7%a5%e7%a8%8b%e5%ae%9e%e8%b7%b5%e6%8c%89%e8%b5%84%e6%ba%90%e7%b1%bb%e5%9e%8b%e7%bb%99%e7%ad%96%e7%95%a5" class="header-anchor"&gt;&lt;/a&gt;六、工程实践：按资源类型给策略
&lt;/h2&gt;&lt;h3 id="1-带内容哈希的静态资源最推荐"&gt;&lt;a href="#1-%e5%b8%a6%e5%86%85%e5%ae%b9%e5%93%88%e5%b8%8c%e7%9a%84%e9%9d%99%e6%80%81%e8%b5%84%e6%ba%90%e6%9c%80%e6%8e%a8%e8%8d%90" class="header-anchor"&gt;&lt;/a&gt;1. 带内容哈希的静态资源（最推荐）
&lt;/h3&gt;&lt;p&gt;构建后文件名含 hash，例如 &lt;code&gt;app.3f2a1c.js&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="s"&gt;/static/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 文件名已指纹化，可大胆长缓存
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;365d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;public,&lt;/span&gt; &lt;span class="s"&gt;max-age=31536000,&lt;/span&gt; &lt;span class="s"&gt;immutable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;URL 变 = 内容变&lt;/strong&gt;，可用超长 &lt;code&gt;max-age&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;immutable&lt;/code&gt; 提示浏览器在 freshness 内不必做启发式再验证（支持情况因客户端而异，但不妨碍给出）；&lt;/li&gt;
&lt;li&gt;旧 URL 自然失效，无需纠结主动 purge 每一个文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-html--入口文档"&gt;&lt;a href="#2-html--%e5%85%a5%e5%8f%a3%e6%96%87%e6%a1%a3" class="header-anchor"&gt;&lt;/a&gt;2. HTML / 入口文档
&lt;/h3&gt;&lt;p&gt;入口常引用带 hash 的静态资源，自身却要尽快看到新版本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: no-cache
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;ETag: &amp;#34;html-rev-42&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;或短 &lt;code&gt;max-age&lt;/code&gt; + 强制再验证：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: max-age=0, must-revalidate
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;ETag: &amp;#34;html-rev-42&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样浏览器/边缘仍可存副本，但使用前会走条件请求；HTML 未变则 304，变了才下新文档。&lt;/p&gt;
&lt;h3 id="3-个性化或含凭证的-api"&gt;&lt;a href="#3-%e4%b8%aa%e6%80%a7%e5%8c%96%e6%88%96%e5%90%ab%e5%87%ad%e8%af%81%e7%9a%84-api" class="header-anchor"&gt;&lt;/a&gt;3. 个性化或含凭证的 API
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: private, no-store
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;private&lt;/code&gt; 避免 CDN 把用户 A 的响应给用户 B；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;no-store&lt;/code&gt; 进一步降低敏感信息落盘风险。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意：&lt;code&gt;no-store&lt;/code&gt; &lt;strong&gt;不是&lt;/strong&gt;完备隐私方案（恶意或失控缓存可能不遵守），敏感数据仍要靠鉴权、最小化返回字段和传输安全。&lt;/p&gt;
&lt;h3 id="4-可被-cdn-缓存的公共-api--配置"&gt;&lt;a href="#4-%e5%8f%af%e8%a2%ab-cdn-%e7%bc%93%e5%ad%98%e7%9a%84%e5%85%ac%e5%85%b1-api--%e9%85%8d%e7%bd%ae" class="header-anchor"&gt;&lt;/a&gt;4. 可被 CDN 缓存的公共 API / 配置
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;Cache-Control: public, s-maxage=30, max-age=10
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="err"&gt;ETag: &amp;#34;cfg-20260715-1&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;浏览器短缓存（&lt;code&gt;max-age&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;CDN 稍长（&lt;code&gt;s-maxage&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;过期后用 ETag 协商，降低源站带宽。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Nginx 反代缓存可配合：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-nginx" data-lang="nginx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;proxy_cache_path&lt;/span&gt; &lt;span class="s"&gt;/var/cache/nginx&lt;/span&gt; &lt;span class="s"&gt;levels=1:2&lt;/span&gt; &lt;span class="s"&gt;keys_zone=api_cache:50m&lt;/span&gt; &lt;span class="s"&gt;inactive=10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="s"&gt;/api/public/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_cache&lt;/span&gt; &lt;span class="s"&gt;api_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_cache_valid&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 过期后用条件请求回源校验
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_cache_revalidate&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 更新期间允许短暂提供 stale，降低源站抖动影响
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_cache_use_stale&lt;/span&gt; &lt;span class="s"&gt;updating&lt;/span&gt; &lt;span class="s"&gt;error&lt;/span&gt; &lt;span class="s"&gt;timeout&lt;/span&gt; &lt;span class="s"&gt;http_500&lt;/span&gt; &lt;span class="s"&gt;http_502&lt;/span&gt; &lt;span class="s"&gt;http_503&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://upstream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;proxy_cache_revalidate on&lt;/code&gt; 的作用，正是在缓存过期后启用 &lt;code&gt;If-Modified-Since&lt;/code&gt; / &lt;code&gt;If-None-Match&lt;/code&gt; 条件回源——这与 RFC 里的 validation 模型一致。&lt;/p&gt;
&lt;h2 id="七服务端如何正确实现条件-get"&gt;&lt;a href="#%e4%b8%83%e6%9c%8d%e5%8a%a1%e7%ab%af%e5%a6%82%e4%bd%95%e6%ad%a3%e7%a1%ae%e5%ae%9e%e7%8e%b0%e6%9d%a1%e4%bb%b6-get" class="header-anchor"&gt;&lt;/a&gt;七、服务端如何正确实现条件 GET
&lt;/h2&gt;&lt;p&gt;以伪代码说明最小正确逻辑：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;etag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strong_etag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# 例如 &amp;#39;&amp;#34;%s&amp;#34;&amp;#39; % sha256(body)[:16]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;last_mod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_modified_http_date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 1) 优先处理 If-None-Match&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;inm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;If-None-Match&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;inm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;etag_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="c1"&gt;# 按弱比较规则实现&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response_304&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;last_mod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response_200&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;last_mod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 2) 否则再看 If-Modified-Since&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;If-Modified-Since&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ims&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modified_after&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ims&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response_304&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;last_mod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response_200&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_modified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;last_mod&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;实现清单：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;同一表示的 ETag 必须稳定&lt;/strong&gt;：相同内容不要每次随机；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容变更必须换 ETag&lt;/strong&gt;：漏更新会造成“改了却一直 304”；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Vary&lt;/code&gt; 要配对&lt;/strong&gt;：若响应随 &lt;code&gt;Accept-Encoding&lt;/code&gt; / &lt;code&gt;Authorization&lt;/code&gt; / 自定义头变化，必须正确 &lt;code&gt;Vary&lt;/code&gt;，否则缓存会串内容；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压缩前后一致性&lt;/strong&gt;：对 gzip/br 内容，ETag 策略要与表示选择一致，避免“协商错表示”；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要手动拼错弱标记&lt;/strong&gt;：&lt;code&gt;W/&lt;/code&gt; 大小写敏感，格式为 &lt;code&gt;W/&amp;quot;tag&amp;quot;&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="八常见坑与排查清单"&gt;&lt;a href="#%e5%85%ab%e5%b8%b8%e8%a7%81%e5%9d%91%e4%b8%8e%e6%8e%92%e6%9f%a5%e6%b8%85%e5%8d%95" class="header-anchor"&gt;&lt;/a&gt;八、常见坑与排查清单
&lt;/h2&gt;&lt;h3 id="坑-1no-cache-和-no-store-用反"&gt;&lt;a href="#%e5%9d%91-1no-cache-%e5%92%8c-no-store-%e7%94%a8%e5%8f%8d" class="header-anchor"&gt;&lt;/a&gt;坑 1：&lt;code&gt;no-cache&lt;/code&gt; 和 &lt;code&gt;no-store&lt;/code&gt; 用反
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;想“每次确认后再用本地副本” → &lt;code&gt;no-cache&lt;/code&gt;（或 &lt;code&gt;max-age=0, must-revalidate&lt;/code&gt;）+ ETag；&lt;/li&gt;
&lt;li&gt;想“别存敏感响应” → &lt;code&gt;no-store&lt;/code&gt;（通常再加 &lt;code&gt;private&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="坑-2只配了长缓存没有内容指纹"&gt;&lt;a href="#%e5%9d%91-2%e5%8f%aa%e9%85%8d%e4%ba%86%e9%95%bf%e7%bc%93%e5%ad%98%e6%b2%a1%e6%9c%89%e5%86%85%e5%ae%b9%e6%8c%87%e7%ba%b9" class="header-anchor"&gt;&lt;/a&gt;坑 2：只配了长缓存，没有内容指纹
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;Cache-Control: max-age=31536000&lt;/code&gt; 配在&lt;strong&gt;固定 URL&lt;/strong&gt; 的 &lt;code&gt;app.js&lt;/code&gt; 上，发版后用户会长时间用旧脚本。正确做法是 &lt;strong&gt;hash 文件名&lt;/strong&gt; 或主动缩短 freshness 并保证能 purge。&lt;/p&gt;
&lt;h3 id="坑-3etag-在多实例间不一致"&gt;&lt;a href="#%e5%9d%91-3etag-%e5%9c%a8%e5%a4%9a%e5%ae%9e%e4%be%8b%e9%97%b4%e4%b8%8d%e4%b8%80%e8%87%b4" class="header-anchor"&gt;&lt;/a&gt;坑 3：ETag 在多实例间不一致
&lt;/h3&gt;&lt;p&gt;滚动发布时实例 A/B 对同一文件算出不同 ETag（例如混入了进程启动时间），客户端会在 200/304 间抖动，缓存命中率暴跌。校验器生成必须&lt;strong&gt;内容决定论&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id="坑-4中间层剥掉了校验器或改写了-cache-control"&gt;&lt;a href="#%e5%9d%91-4%e4%b8%ad%e9%97%b4%e5%b1%82%e5%89%a5%e6%8e%89%e4%ba%86%e6%a0%a1%e9%aa%8c%e5%99%a8%e6%88%96%e6%94%b9%e5%86%99%e4%ba%86-cache-control" class="header-anchor"&gt;&lt;/a&gt;坑 4：中间层剥掉了校验器或改写了 Cache-Control
&lt;/h3&gt;&lt;p&gt;排查命令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 看源站&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sI https://origin.example.com/static/app.js &lt;span class="p"&gt;|&lt;/span&gt; sed -n &lt;span class="s1"&gt;&amp;#39;1,20p&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 看经 CDN/Nginx 后&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sI https://www.example.com/static/app.js &lt;span class="p"&gt;|&lt;/span&gt; sed -n &lt;span class="s1"&gt;&amp;#39;1,20p&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 模拟条件请求&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;ETAG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#34;abc123&amp;#34;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;curl -sI -H &lt;span class="s2"&gt;&amp;#34;If-None-Match: &lt;/span&gt;&lt;span class="nv"&gt;$ETAG&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; https://www.example.com/static/app.js &lt;span class="p"&gt;|&lt;/span&gt; sed -n &lt;span class="s1"&gt;&amp;#39;1,15p&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对比：&lt;code&gt;ETag&lt;/code&gt;、&lt;code&gt;Cache-Control&lt;/code&gt;、&lt;code&gt;Age&lt;/code&gt;、&lt;code&gt;CF-Cache-Status&lt;/code&gt;/&lt;code&gt;X-Cache&lt;/code&gt; 等是否符合预期。第二次应看到 &lt;code&gt;304&lt;/code&gt;（在 ETag 仍匹配时）。&lt;/p&gt;
&lt;h3 id="坑-5api-被共享缓存误缓存"&gt;&lt;a href="#%e5%9d%91-5api-%e8%a2%ab%e5%85%b1%e4%ba%ab%e7%bc%93%e5%ad%98%e8%af%af%e7%bc%93%e5%ad%98" class="header-anchor"&gt;&lt;/a&gt;坑 5：API 被共享缓存误缓存
&lt;/h3&gt;&lt;p&gt;带 cookie/authorization 的响应若缺少 &lt;code&gt;private&lt;/code&gt;/&lt;code&gt;no-store&lt;/code&gt;，再叠加错误的 &lt;code&gt;public&lt;/code&gt;，可能把用户数据缓存到边缘。对用户相关响应默认保守，对真正公共的 GET 再显式放开。&lt;/p&gt;
&lt;h3 id="坑-6304-后客户端仍像没更新样式"&gt;&lt;a href="#%e5%9d%91-6304-%e5%90%8e%e5%ae%a2%e6%88%b7%e7%ab%af%e4%bb%8d%e5%83%8f%e6%b2%a1%e6%9b%b4%e6%96%b0%e6%a0%b7%e5%bc%8f" class="header-anchor"&gt;&lt;/a&gt;坑 6：304 后客户端仍像“没更新样式”
&lt;/h3&gt;&lt;p&gt;常见是 HTML 长缓存，或 Service Worker / 应用内二次缓存。协议层 304 只保证&lt;strong&gt;这一跳 HTTP 缓存协商&lt;/strong&gt;；还要检查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTML 的 Cache-Control；&lt;/li&gt;
&lt;li&gt;SW 的 &lt;code&gt;cache.addAll&lt;/code&gt; 策略；&lt;/li&gt;
&lt;li&gt;应用本地 localStorage 里的资源清单。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="九选型小结"&gt;&lt;a href="#%e4%b9%9d%e9%80%89%e5%9e%8b%e5%b0%8f%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;九、选型小结
&lt;/h2&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;目标&lt;/th&gt;
					&lt;th&gt;推荐组合&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;最大化静态资源性能&lt;/td&gt;
					&lt;td&gt;内容哈希 URL + 长 &lt;code&gt;max-age&lt;/code&gt; + &lt;code&gt;public&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;入口文档尽快更新&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;no-cache&lt;/code&gt; 或短 max-age + ETag + 304&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;公共只读 API 降源站负载&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;s-maxage&lt;/code&gt; + ETag + CDN &lt;code&gt;proxy_cache_revalidate&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;用户私有数据&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;private, no-store&lt;/code&gt;，默认不进共享缓存&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;过期后仍要强一致&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;must-revalidate&lt;/code&gt; / &lt;code&gt;proxy-revalidate&lt;/code&gt; / &lt;code&gt;s-maxage&lt;/code&gt; 语义&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;原则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;用 freshness 控制回源频率&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用 validator 控制回源体积&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用 private/no-store 控制安全边界&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用 URL 指纹解决“长缓存与发布”的矛盾&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;HTTP 缓存不是“加一个 Redis”或“在 Nginx 里 &lt;code&gt;proxy_cache on&lt;/code&gt;”这么简单，它是一套有严格语义的协议机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Cache-Control&lt;/code&gt; 回答&lt;strong&gt;能不能存、能存多久、过期后能不能直接用&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ETag&lt;/code&gt; / &lt;code&gt;Last-Modified&lt;/code&gt; 回答&lt;strong&gt;如何证明表示是否变化&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;If-None-Match&lt;/code&gt; / &lt;code&gt;If-Modified-Since&lt;/code&gt; 与 &lt;code&gt;304&lt;/code&gt; 回答&lt;strong&gt;如何在确认未变时省略 body&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把这三层设计对齐后，静态站点、前后端分离入口、CDN 边缘缓存和公共 API 都能在&lt;strong&gt;带宽、延迟、一致性&lt;/strong&gt;之间取得可解释的平衡。下次再看到“用户一直是旧页面”或“源站带宽无故升高”，先把响应头和一次条件请求抓出来——答案通常就在 &lt;code&gt;Cache-Control&lt;/code&gt; 与校验器的协作里。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;IETF RFC 9111：&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9111" target="_blank" rel="noopener"
 &gt;HTTP Caching&lt;/a&gt;（新鲜度、验证、&lt;code&gt;Cache-Control&lt;/code&gt; 指令语义）&lt;/li&gt;
&lt;li&gt;IETF RFC 9110：&lt;a class="link" href="https://www.rfc-editor.org/rfc/rfc9110" target="_blank" rel="noopener"
 &gt;HTTP Semantics&lt;/a&gt;（&lt;code&gt;ETag&lt;/code&gt;、&lt;code&gt;Last-Modified&lt;/code&gt;、&lt;code&gt;If-None-Match&lt;/code&gt;、&lt;code&gt;If-Modified-Since&lt;/code&gt;、&lt;code&gt;304 Not Modified&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;MDN：&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching" target="_blank" rel="noopener"
 &gt;HTTP caching&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MDN：&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag" target="_blank" rel="noopener"
 &gt;ETag&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MDN：&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match" target="_blank" rel="noopener"
 &gt;If-None-Match&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MDN：&lt;a class="link" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control" target="_blank" rel="noopener"
 &gt;Cache-Control&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Nginx 文档：&lt;a class="link" href="https://nginx.org/en/docs/http/ngx_http_headers_module.html" target="_blank" rel="noopener"
 &gt;ngx_http_headers_module&lt;/a&gt;（&lt;code&gt;expires&lt;/code&gt; / &lt;code&gt;add_header&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;Nginx 文档：&lt;a class="link" href="https://nginx.org/en/docs/http/ngx_http_proxy_module.html" target="_blank" rel="noopener"
 &gt;ngx_http_proxy_module&lt;/a&gt;（&lt;code&gt;proxy_cache_revalidate&lt;/code&gt;、&lt;code&gt;proxy_cache_use_stale&lt;/code&gt;、&lt;code&gt;proxy_cache_valid&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Redis 缓存穿透、击穿与雪崩：原理、场景与工程解法</title><link>https://blog.waihost.com/posts/redis-cache-penetration-breakdown-avalanche/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0800</pubDate><guid>https://blog.waihost.com/posts/redis-cache-penetration-breakdown-avalanche/</guid><description>&lt;img src="https://blog.waihost.com/images/covers/redis-cache-penetration-breakdown-avalanche.svg" alt="Featured image of post Redis 缓存穿透、击穿与雪崩：原理、场景与工程解法" /&gt;&lt;p&gt;在高并发系统里，Redis 常常被放在数据库前面做读加速。一旦缓存设计不当，问题往往不是“Redis 慢一点”，而是数据库被瞬间打穿。工程上最常见、也最容易被混为一谈的三类故障是：&lt;strong&gt;缓存穿透&lt;/strong&gt;、&lt;strong&gt;缓存击穿&lt;/strong&gt;和&lt;strong&gt;缓存雪崩&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这三类问题听起来相似，根因却不同：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;典型特征&lt;/th&gt;
					&lt;th&gt;直接伤害&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;缓存穿透&lt;/td&gt;
					&lt;td&gt;查的是不存在的数据，缓存和 DB 都没有&lt;/td&gt;
					&lt;td&gt;恶意或异常请求反复打到 DB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;缓存击穿&lt;/td&gt;
					&lt;td&gt;某个热点 key 刚好过期&lt;/td&gt;
					&lt;td&gt;同一时刻大量请求打到 DB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;缓存雪崩&lt;/td&gt;
					&lt;td&gt;大量 key 同时失效，或缓存层整体不可用&lt;/td&gt;
					&lt;td&gt;系统级读流量直接压垮 DB&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;本文从现象、根因、可落地方案三层拆开，并结合 Redis 官方能力（&lt;code&gt;SET&lt;/code&gt; 的 &lt;code&gt;NX/EX&lt;/code&gt;、&lt;code&gt;EXPIRE&lt;/code&gt;、淘汰策略、分布式锁模式）给出可复用实践。&lt;/p&gt;
&lt;h2 id="一先把请求路径画清楚"&gt;&lt;a href="#%e4%b8%80%e5%85%88%e6%8a%8a%e8%af%b7%e6%b1%82%e8%b7%af%e5%be%84%e7%94%bb%e6%b8%85%e6%a5%9a" class="header-anchor"&gt;&lt;/a&gt;一、先把请求路径画清楚
&lt;/h2&gt;&lt;p&gt;一个典型读路径是：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Client -&amp;gt; App -&amp;gt; Redis -&amp;gt; (miss) -&amp;gt; Database -&amp;gt; write back Redis -&amp;gt; response
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;缓存层的价值在于：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把高频读从数据库剥离出来；&lt;/li&gt;
&lt;li&gt;用更低延迟响应热点数据；&lt;/li&gt;
&lt;li&gt;给数据库争取容量和稳定性。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，任何“绕过缓存直接打 DB”的路径，都要被当成故障设计点来处理。&lt;/p&gt;
&lt;h2 id="二缓存穿透查不存在的数据"&gt;&lt;a href="#%e4%ba%8c%e7%bc%93%e5%ad%98%e7%a9%bf%e9%80%8f%e6%9f%a5%e4%b8%8d%e5%ad%98%e5%9c%a8%e7%9a%84%e6%95%b0%e6%8d%ae" class="header-anchor"&gt;&lt;/a&gt;二、缓存穿透：查不存在的数据
&lt;/h2&gt;&lt;h3 id="1-现象"&gt;&lt;a href="#1-%e7%8e%b0%e8%b1%a1" class="header-anchor"&gt;&lt;/a&gt;1. 现象
&lt;/h3&gt;&lt;p&gt;请求的 key 在业务上不存在，例如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户 ID &lt;code&gt;-1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;商品 ID &lt;code&gt;999999999&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;被爬虫扫出来的非法参数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此时 Redis 没有缓存，数据库也查不到记录。如果应用只在“查到结果时才写缓存”，那么每次请求都会落到数据库。&lt;/p&gt;
&lt;h3 id="2-根因"&gt;&lt;a href="#2-%e6%a0%b9%e5%9b%a0" class="header-anchor"&gt;&lt;/a&gt;2. 根因
&lt;/h3&gt;&lt;p&gt;穿透的本质不是“缓存过期”，而是：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;空结果没有被负缓存（negative caching）保护。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="3-解法"&gt;&lt;a href="#3-%e8%a7%a3%e6%b3%95" class="header-anchor"&gt;&lt;/a&gt;3. 解法
&lt;/h3&gt;&lt;h4 id="方案-a缓存空对象最常用"&gt;&lt;a href="#%e6%96%b9%e6%a1%88-a%e7%bc%93%e5%ad%98%e7%a9%ba%e5%af%b9%e8%b1%a1%e6%9c%80%e5%b8%b8%e7%94%a8" class="header-anchor"&gt;&lt;/a&gt;方案 A：缓存空对象（最常用）
&lt;/h4&gt;&lt;p&gt;查库为空时，也写入一个短 TTL 的空值标记：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 伪命令：空结果缓存 60 秒&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SET product:999999999 __NULL__ EX &lt;span class="m"&gt;60&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;应用读到 &lt;code&gt;__NULL__&lt;/code&gt; 时直接返回“不存在”，不再访问数据库。&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TTL 不要太长，避免真实数据刚创建后长时间读不到；&lt;/li&gt;
&lt;li&gt;空值要有统一约定，避免和正常业务值冲突；&lt;/li&gt;
&lt;li&gt;对写路径做缓存失效/更新，保证最终一致。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="方案-b布隆过滤器bloom-filter前置拦截"&gt;&lt;a href="#%e6%96%b9%e6%a1%88-b%e5%b8%83%e9%9a%86%e8%bf%87%e6%bb%a4%e5%99%a8bloom-filter%e5%89%8d%e7%bd%ae%e6%8b%a6%e6%88%aa" class="header-anchor"&gt;&lt;/a&gt;方案 B：布隆过滤器（Bloom Filter）前置拦截
&lt;/h4&gt;&lt;p&gt;对“可能存在”的 ID 集合做布隆过滤器：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据写入时同步/异步加入过滤器；&lt;/li&gt;
&lt;li&gt;查询前先判断“一定不存在还是可能存在”；&lt;/li&gt;
&lt;li&gt;一定不存在则直接拒绝，不打 Redis/DB。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;适合海量 ID、爬虫扫描、开放查询接口。代价是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有极低误判率（把不存在判断成可能存在）；&lt;/li&gt;
&lt;li&gt;需要维护过滤器更新与扩容。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="方案-c参数与权限校验前置"&gt;&lt;a href="#%e6%96%b9%e6%a1%88-c%e5%8f%82%e6%95%b0%e4%b8%8e%e6%9d%83%e9%99%90%e6%a0%a1%e9%aa%8c%e5%89%8d%e7%bd%ae" class="header-anchor"&gt;&lt;/a&gt;方案 C：参数与权限校验前置
&lt;/h4&gt;&lt;p&gt;很多穿透其实是脏流量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非数字 ID&lt;/li&gt;
&lt;li&gt;超范围分页&lt;/li&gt;
&lt;li&gt;未登录却访问私有资源&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在网关/应用入口直接拦截，比把压力交给缓存更划算。&lt;/p&gt;
&lt;h2 id="三缓存击穿热点-key-刚好失效"&gt;&lt;a href="#%e4%b8%89%e7%bc%93%e5%ad%98%e5%87%bb%e7%a9%bf%e7%83%ad%e7%82%b9-key-%e5%88%9a%e5%a5%bd%e5%a4%b1%e6%95%88" class="header-anchor"&gt;&lt;/a&gt;三、缓存击穿：热点 key 刚好失效
&lt;/h2&gt;&lt;h3 id="1-现象-1"&gt;&lt;a href="#1-%e7%8e%b0%e8%b1%a1-1" class="header-anchor"&gt;&lt;/a&gt;1. 现象
&lt;/h3&gt;&lt;p&gt;某个非常热的 key（首页配置、秒杀库存视图、热门商品详情）在某一瞬间过期。&lt;br&gt;
下一毫秒，成千上万请求同时看到 miss，一起去查数据库，形成“单点热点打穿”。&lt;/p&gt;
&lt;h3 id="2-根因-1"&gt;&lt;a href="#2-%e6%a0%b9%e5%9b%a0-1" class="header-anchor"&gt;&lt;/a&gt;2. 根因
&lt;/h3&gt;&lt;p&gt;击穿关注的是：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;单个热点 key 过期窗口内的并发回源。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;它和穿透不同：数据本来存在，只是缓存刚好空了。&lt;/p&gt;
&lt;h3 id="3-解法-1"&gt;&lt;a href="#3-%e8%a7%a3%e6%b3%95-1" class="header-anchor"&gt;&lt;/a&gt;3. 解法
&lt;/h3&gt;&lt;h4 id="方案-a互斥重建singleflight--分布式锁"&gt;&lt;a href="#%e6%96%b9%e6%a1%88-a%e4%ba%92%e6%96%a5%e9%87%8d%e5%bb%basingleflight--%e5%88%86%e5%b8%83%e5%bc%8f%e9%94%81" class="header-anchor"&gt;&lt;/a&gt;方案 A：互斥重建（singleflight / 分布式锁）
&lt;/h4&gt;&lt;p&gt;只有一个请求负责回源重建，其他请求等待或短暂失败重试。&lt;/p&gt;
&lt;p&gt;Redis 官方 &lt;code&gt;SET&lt;/code&gt; 支持原子条件写入，可用 &lt;code&gt;NX + EX&lt;/code&gt; 做互斥：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 抢重建锁，5 秒自动过期，防止进程崩溃后死锁&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SET rebuild:lock:product:1001 &lt;span class="m"&gt;1&lt;/span&gt; NX EX &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读缓存 miss；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SET lock NX EX&lt;/code&gt; 抢锁；&lt;/li&gt;
&lt;li&gt;抢到锁的请求查 DB，写回缓存，释放锁；&lt;/li&gt;
&lt;li&gt;没抢到锁的请求短暂 sleep/重试读缓存，或返回旧兜底。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;官方文档明确：&lt;code&gt;SET key value NX EX seconds&lt;/code&gt; 是设置值并带过期时间的常用原子写法；&lt;code&gt;SETNX&lt;/code&gt; 在新代码中更推荐用 &lt;code&gt;SET ... NX&lt;/code&gt; 替代。&lt;/p&gt;
&lt;h4 id="方案-b逻辑过期logical-expire"&gt;&lt;a href="#%e6%96%b9%e6%a1%88-b%e9%80%bb%e8%be%91%e8%bf%87%e6%9c%9flogical-expire" class="header-anchor"&gt;&lt;/a&gt;方案 B：逻辑过期（logical expire）
&lt;/h4&gt;&lt;p&gt;缓存 value 内嵌业务过期时间，物理 key 不过期或过期很久：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;data&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nt"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;&amp;#34;price&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;expireAt&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1720857600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;读取时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;未逻辑过期：直接返回；&lt;/li&gt;
&lt;li&gt;已逻辑过期：返回旧数据，同时异步触发重建。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;优点是用户几乎无感 miss；代价是要接受短暂脏读，并做好异步重建失败补偿。&lt;/p&gt;
&lt;h4 id="方案-c热点-key-永不过期--主动更新"&gt;&lt;a href="#%e6%96%b9%e6%a1%88-c%e7%83%ad%e7%82%b9-key-%e6%b0%b8%e4%b8%8d%e8%bf%87%e6%9c%9f--%e4%b8%bb%e5%8a%a8%e6%9b%b4%e6%96%b0" class="header-anchor"&gt;&lt;/a&gt;方案 C：热点 key 永不过期 + 主动更新
&lt;/h4&gt;&lt;p&gt;对真正的超级热点，不依赖 TTL 被动失效，而由写路径/定时任务主动更新。&lt;br&gt;
TTL 更适合普通数据；热点数据更适合“写时更新 + 监控”。&lt;/p&gt;
&lt;h2 id="四缓存雪崩大面积失效或缓存层故障"&gt;&lt;a href="#%e5%9b%9b%e7%bc%93%e5%ad%98%e9%9b%aa%e5%b4%a9%e5%a4%a7%e9%9d%a2%e7%a7%af%e5%a4%b1%e6%95%88%e6%88%96%e7%bc%93%e5%ad%98%e5%b1%82%e6%95%85%e9%9a%9c" class="header-anchor"&gt;&lt;/a&gt;四、缓存雪崩：大面积失效或缓存层故障
&lt;/h2&gt;&lt;h3 id="1-现象-2"&gt;&lt;a href="#1-%e7%8e%b0%e8%b1%a1-2" class="header-anchor"&gt;&lt;/a&gt;1. 现象
&lt;/h3&gt;&lt;p&gt;两类雪崩最常见：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;过期雪崩&lt;/strong&gt;：大量 key 在同一时间点过期；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可用性雪崩&lt;/strong&gt;：Redis 集群故障、网络分区、连接打满，导致缓存层整体不可用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;结果都是数据库在短时间内接到远超容量的读请求。&lt;/p&gt;
&lt;h3 id="2-根因-2"&gt;&lt;a href="#2-%e6%a0%b9%e5%9b%a0-2" class="header-anchor"&gt;&lt;/a&gt;2. 根因
&lt;/h3&gt;&lt;p&gt;雪崩的核心不是某一个 key，而是：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;缓存保护面在同一时间失效，流量失去缓冲层。&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h3 id="3-解法-2"&gt;&lt;a href="#3-%e8%a7%a3%e6%b3%95-2" class="header-anchor"&gt;&lt;/a&gt;3. 解法
&lt;/h3&gt;&lt;h4 id="方案-attl-加随机抖动"&gt;&lt;a href="#%e6%96%b9%e6%a1%88-attl-%e5%8a%a0%e9%9a%8f%e6%9c%ba%e6%8a%96%e5%8a%a8" class="header-anchor"&gt;&lt;/a&gt;方案 A：TTL 加随机抖动
&lt;/h4&gt;&lt;p&gt;不要让同类数据使用完全相同的过期时间：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ttl = base_ttl + random(0, jitter)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;# 例如 3600 + random(0, 300)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样可以把过期曲线打散，避免整点集体回源。&lt;/p&gt;
&lt;h4 id="方案-b多级缓存与本地缓存"&gt;&lt;a href="#%e6%96%b9%e6%a1%88-b%e5%a4%9a%e7%ba%a7%e7%bc%93%e5%ad%98%e4%b8%8e%e6%9c%ac%e5%9c%b0%e7%bc%93%e5%ad%98" class="header-anchor"&gt;&lt;/a&gt;方案 B：多级缓存与本地缓存
&lt;/h4&gt;&lt;ul&gt;
&lt;li&gt;本地 Caffeine/Guava Cache 挡住进程内重复读；&lt;/li&gt;
&lt;li&gt;Redis 作为分布式缓存；&lt;/li&gt;
&lt;li&gt;DB 作为最终数据源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本地缓存 TTL 更短，可显著降低 Redis 抖动对 DB 的传导。&lt;/p&gt;
&lt;h4 id="方案-c限流熔断降级"&gt;&lt;a href="#%e6%96%b9%e6%a1%88-c%e9%99%90%e6%b5%81%e7%86%94%e6%96%ad%e9%99%8d%e7%ba%a7" class="header-anchor"&gt;&lt;/a&gt;方案 C：限流、熔断、降级
&lt;/h4&gt;&lt;p&gt;缓存层异常时，应用不能无脑回源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对回源 QPS 做令牌桶/漏桶限流；&lt;/li&gt;
&lt;li&gt;数据库错误率升高时熔断；&lt;/li&gt;
&lt;li&gt;非核心接口返回兜底页/默认配置；&lt;/li&gt;
&lt;li&gt;核心读接口优先返回短暂旧数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="方案-d正确使用-redis-内存淘汰策略"&gt;&lt;a href="#%e6%96%b9%e6%a1%88-d%e6%ad%a3%e7%a1%ae%e4%bd%bf%e7%94%a8-redis-%e5%86%85%e5%ad%98%e6%b7%98%e6%b1%b0%e7%ad%96%e7%95%a5" class="header-anchor"&gt;&lt;/a&gt;方案 D：正确使用 Redis 内存淘汰策略
&lt;/h4&gt;&lt;p&gt;当 Redis 用作缓存时，官方建议配置最大内存和淘汰策略，以便内存触顶时按策略驱逐 key，而不是写失败或无规划崩溃。常见策略包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;allkeys-lru&lt;/code&gt; / &lt;code&gt;allkeys-lfu&lt;/code&gt;：在全部 key 中淘汰；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;volatile-lru&lt;/code&gt; / &lt;code&gt;volatile-lfu&lt;/code&gt; / &lt;code&gt;volatile-ttl&lt;/code&gt;：只在设置了过期时间的 key 中淘汰。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;关键点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;缓存场景应显式设置 &lt;code&gt;maxmemory&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;按业务选择 LRU/LFU；&lt;/li&gt;
&lt;li&gt;监控 &lt;code&gt;evicted_keys&lt;/code&gt;、命中率、内存使用，避免“默默淘汰关键数据”却无人知晓。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;淘汰策略解决的是&lt;strong&gt;内存压力下的可持续性&lt;/strong&gt;，不能替代业务层的穿透/击穿防护，但能降低缓存层自身失控概率。&lt;/p&gt;
&lt;h2 id="五一套可落地的组合策略"&gt;&lt;a href="#%e4%ba%94%e4%b8%80%e5%a5%97%e5%8f%af%e8%90%bd%e5%9c%b0%e7%9a%84%e7%bb%84%e5%90%88%e7%ad%96%e7%95%a5" class="header-anchor"&gt;&lt;/a&gt;五、一套可落地的组合策略
&lt;/h2&gt;&lt;p&gt;真实系统很少只选一种方案，更常见的是分层组合：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. 入口校验：挡住非法参数
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. 布隆过滤器/存在性索引：挡住明显不存在的 key
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. Redis 读：命中则返回
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. miss 时：
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 热点 key：互斥重建或逻辑过期
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - 普通 key：允许有限并发回源
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5. DB 空结果：写短 TTL 空缓存
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;6. DB 有结果：写缓存，TTL 加抖动
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;7. 全链路：限流 + 熔断 + 监控告警
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="伪代码示例"&gt;&lt;a href="#%e4%bc%aa%e4%bb%a3%e7%a0%81%e7%a4%ba%e4%be%8b" class="header-anchor"&gt;&lt;/a&gt;伪代码示例
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;valid_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bloom&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;bloom&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;might_contain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;product:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;__NULL__&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# 热点互斥重建&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;locked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;rebuild:lock:product:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;get_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# 简化：重试读缓存&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;product:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;__NULL__&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;product:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;rebuild:lock:product:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SET ... NX EX&lt;/code&gt; 用于短锁，避免击穿时并发回源；&lt;/li&gt;
&lt;li&gt;空值缓存用于防穿透；&lt;/li&gt;
&lt;li&gt;TTL 抖动用于打散过期，降低雪崩概率。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="六排查清单线上怎么快速判断是哪一种"&gt;&lt;a href="#%e5%85%ad%e6%8e%92%e6%9f%a5%e6%b8%85%e5%8d%95%e7%ba%bf%e4%b8%8a%e6%80%8e%e4%b9%88%e5%bf%ab%e9%80%9f%e5%88%a4%e6%96%ad%e6%98%af%e5%93%aa%e4%b8%80%e7%a7%8d" class="header-anchor"&gt;&lt;/a&gt;六、排查清单：线上怎么快速判断是哪一种
&lt;/h2&gt;&lt;h3 id="1-先看指标"&gt;&lt;a href="#1-%e5%85%88%e7%9c%8b%e6%8c%87%e6%a0%87" class="header-anchor"&gt;&lt;/a&gt;1. 先看指标
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Redis 命中率是否骤降&lt;/li&gt;
&lt;li&gt;Redis 连接数/超时是否升高&lt;/li&gt;
&lt;li&gt;数据库 QPS、慢查询、连接池是否打满&lt;/li&gt;
&lt;li&gt;是否集中在某几个 key，还是大面积 key&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="2-判断路径"&gt;&lt;a href="#2-%e5%88%a4%e6%96%ad%e8%b7%af%e5%be%84" class="header-anchor"&gt;&lt;/a&gt;2. 判断路径
&lt;/h3&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;观察&lt;/th&gt;
					&lt;th&gt;更可能是&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;大量非法/不存在 ID，DB 查空很多&lt;/td&gt;
					&lt;td&gt;穿透&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;单个热 key 流量尖峰，过期瞬间 DB 飙升&lt;/td&gt;
					&lt;td&gt;击穿&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;整点或批次任务后大面积 miss&lt;/td&gt;
					&lt;td&gt;过期雪崩&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Redis 超时/主从切换/集群异常同时发生&lt;/td&gt;
					&lt;td&gt;可用性雪崩&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="3-临时止血"&gt;&lt;a href="#3-%e4%b8%b4%e6%97%b6%e6%ad%a2%e8%a1%80" class="header-anchor"&gt;&lt;/a&gt;3. 临时止血
&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;对异常查询参数限流；&lt;/li&gt;
&lt;li&gt;对热点 key 预热并临时延长 TTL；&lt;/li&gt;
&lt;li&gt;打开空值缓存；&lt;/li&gt;
&lt;li&gt;给 DB 回源加总开关/配额；&lt;/li&gt;
&lt;li&gt;非核心读降级。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="七设计原则总结"&gt;&lt;a href="#%e4%b8%83%e8%ae%be%e8%ae%a1%e5%8e%9f%e5%88%99%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;七、设计原则总结
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;缓存不是数据库的影子，是保护层。&lt;/strong&gt; 任何 miss 路径都要有预算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;空结果也是结果。&lt;/strong&gt; 不做负缓存，就容易被穿透。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;热点不能靠“碰巧没过期”。&lt;/strong&gt; 要有互斥重建、逻辑过期或主动更新。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;过期时间要工程化。&lt;/strong&gt; 相同 TTL 是雪崩温床，抖动是基本操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存故障要有降级。&lt;/strong&gt; 没有限流和熔断的缓存架构，只是把风险延后到数据库。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可观测性比“感觉很快”更重要。&lt;/strong&gt; 命中率、回源 QPS、淘汰数、锁竞争都要可监控。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="八什么时候不该过度设计"&gt;&lt;a href="#%e5%85%ab%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e4%b8%8d%e8%af%a5%e8%bf%87%e5%ba%a6%e8%ae%be%e8%ae%a1" class="header-anchor"&gt;&lt;/a&gt;八、什么时候不该过度设计
&lt;/h2&gt;&lt;p&gt;不是所有系统都要上布隆过滤器 + 逻辑过期 + 多级缓存：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读 QPS 不高、数据几乎都存在：做好空值缓存和基础 TTL 即可；&lt;/li&gt;
&lt;li&gt;写极少、读极多的配置类数据：适合主动更新 + 较长 TTL；&lt;/li&gt;
&lt;li&gt;强一致要求极高的余额/库存：缓存只能做辅助，核心以数据库/专用组件为准，并明确一致性模型。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;架构选择应匹配流量模型和一致性要求，而不是堆名词。&lt;/p&gt;
&lt;h2 id="总结"&gt;&lt;a href="#%e6%80%bb%e7%bb%93" class="header-anchor"&gt;&lt;/a&gt;总结
&lt;/h2&gt;&lt;p&gt;缓存穿透、击穿、雪崩并不是三个神秘黑话，而是三条清晰的失败路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;穿透&lt;/strong&gt;：不存在的数据反复打穿 DB；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;击穿&lt;/strong&gt;：热点 key 失效瞬间被并发打穿；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;雪崩&lt;/strong&gt;：保护面大面积失效，流量失去缓冲。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;把问题定义清楚后，解法会自然收敛到：入口校验、空值缓存、互斥重建、TTL 抖动、淘汰策略、限流熔断与监控。&lt;br&gt;
Redis 提供了 &lt;code&gt;SET NX/EX&lt;/code&gt;、&lt;code&gt;EXPIRE&lt;/code&gt;、内存淘汰和分布式锁模式等基础能力，但真正决定系统稳不稳的，是你有没有把这些能力嵌进完整的读路径设计里。&lt;/p&gt;
&lt;h2 id="参考资料"&gt;&lt;a href="#%e5%8f%82%e8%80%83%e8%b5%84%e6%96%99" class="header-anchor"&gt;&lt;/a&gt;参考资料
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Redis 官方文档：&lt;a class="link" href="https://redis.io/docs/latest/commands/set/" target="_blank" rel="noopener"
 &gt;&lt;code&gt;SET&lt;/code&gt; 命令&lt;/a&gt;（含 &lt;code&gt;NX&lt;/code&gt; / &lt;code&gt;EX&lt;/code&gt; 等选项）&lt;/li&gt;
&lt;li&gt;Redis 官方文档：&lt;a class="link" href="https://redis.io/docs/latest/commands/expire/" target="_blank" rel="noopener"
 &gt;&lt;code&gt;EXPIRE&lt;/code&gt; 命令&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis 官方文档：&lt;a class="link" href="https://redis.io/docs/latest/commands/setnx/" target="_blank" rel="noopener"
 &gt;&lt;code&gt;SETNX&lt;/code&gt; 命令&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis 官方文档：&lt;a class="link" href="https://redis.io/docs/latest/develop/reference/eviction/" target="_blank" rel="noopener"
 &gt;Key eviction 内存淘汰策略&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Redis 官方文档：&lt;a class="link" href="https://redis.io/docs/latest/develop/use/patterns/distributed-locks/" target="_blank" rel="noopener"
 &gt;Distributed locks 分布式锁模式&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>