<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>系统设计 on 止语Lab</title>
        <link>https://www.wujiachen.com.cn/tags/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/</link>
        <description>Recent content in 系统设计 on 止语Lab</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Wed, 10 Jun 2026 00:20:41 +0800</lastBuildDate><atom:link href="https://www.wujiachen.com.cn/tags/%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>冷启动雪崩的三种策略：惰性加载、主动预热、渐进式预热怎么选</title>
            <link>https://www.wujiachen.com.cn/posts/cache-warmup-strategies/</link>
            <pubDate>Wed, 10 Jun 2026 00:20:40 +0800</pubDate>
            <guid>https://www.wujiachen.com.cn/posts/cache-warmup-strategies/</guid>
            <description>&lt;img src=&#34;https://img.wujiachen.com.cn/cache-warmup-strategies/cover.png&#34; alt=&#34;Featured image of post 冷启动雪崩的三种策略：惰性加载、主动预热、渐进式预热怎么选&#34; /&gt;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/cache-warmup-strategies/cover.png&#34; alt=&#34;封面&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;缓存服务一重启，数据库 CPU 瞬间 100%。你不是第一个遇到这个问题的人。&lt;/p&gt;&#xA;&lt;p&gt;这个问题有个专有名词叫&amp;quot;冷启动穿透&amp;quot;——严格来说它和标准的缓存雪崩（大量 key 同时过期或节点宕机）不是一回事，但后果类似：缓存里没有数据，所有请求穿透到数据库。很多人第一反应是&amp;quot;缓存挂了&amp;quot;，其实真正的原因是缓存&lt;strong&gt;冷着&lt;/strong&gt;的时候，你的策略没选对。&lt;/p&gt;&#xA;&lt;p&gt;选择缓存策略本质上是一道选择题：你的业务能不能接受缓存冷启动时的那几秒&amp;quot;冷&amp;quot;？如果能，最省事的策略就够了；如果不能，你需要付出什么代价来预热。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;本文是一篇决策导向的文章，不是操作教程。不教你怎么配 Redis，也不讲缓存穿透/击穿——只聚焦冷启动场景下三种策略的选择问题。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;一先看矩阵你的系统在哪个象限&#34;&gt;&lt;a href=&#34;#%e4%b8%80%e5%85%88%e7%9c%8b%e7%9f%a9%e9%98%b5%e4%bd%a0%e7%9a%84%e7%b3%bb%e7%bb%9f%e5%9c%a8%e5%93%aa%e4%b8%aa%e8%b1%a1%e9%99%90&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;一、先看矩阵：你的系统在哪个象限？&#xA;&lt;/h2&gt;&lt;p&gt;选策略之前，先看两个变量。&lt;/p&gt;&#xA;&lt;p&gt;第一个是&lt;strong&gt;缓存缺失容忍度&lt;/strong&gt;：你的业务能不能接受缓存冷启动时的&amp;quot;缓存穿透&amp;quot;？&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;高容忍：CMS、后台管理、数据分析平台&lt;/li&gt;&#xA;&lt;li&gt;低容忍：秒杀、实时推荐、高并发 API&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;第二个是&lt;strong&gt;数据预热成本&lt;/strong&gt;：预热需要多少时间和资源？&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;低成本：热点数据 &amp;lt; 10,000 条，预热耗时 &amp;lt; 10ms&lt;/li&gt;&#xA;&lt;li&gt;高成本：热点数据 &amp;gt; 100,000 条，预热耗时 &amp;gt; 100ms 或需要大量计算&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;两个变量组合起来，就是四个象限：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;低成本预热&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;高成本预热&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;&lt;strong&gt;高容忍度&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;惰性加载+保护&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;渐进式预热&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;&lt;strong&gt;低容忍度&lt;/strong&gt;&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;主动预热&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;渐进式预热（或架构优化）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;下面逐个拆解每个象限。你可以在读的过程中不断回看这个矩阵，找到自己业务的位置。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;二高容忍--低成本惰性加载--保护&#34;&gt;&lt;a href=&#34;#%e4%ba%8c%e9%ab%98%e5%ae%b9%e5%bf%8d--%e4%bd%8e%e6%88%90%e6%9c%ac%e6%83%b0%e6%80%a7%e5%8a%a0%e8%bd%bd--%e4%bf%9d%e6%8a%a4&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;二、高容忍 × 低成本：惰性加载 + 保护&#xA;&lt;/h2&gt;&lt;p&gt;大多数团队第一次遇到冷启动穿透时的反应是：加预热。但有时候你不需要预热——你需要的只是一个保护方案。&lt;/p&gt;&#xA;&lt;p&gt;惰性加载的策略很简单：不预加载任何数据，请求到了才查缓存，缓存没有就去数据库查，查到了再写回缓存。零启动成本，系统重启后立刻可用。代价是第一次请求慢，并发够大时数据库可能被打穿。&lt;/p&gt;&#xA;&lt;p&gt;但惰性加载最容易被忽视的问题不是&amp;quot;慢&amp;quot;，是&lt;strong&gt;雪崩效应&lt;/strong&gt;。假设你的服务有 50 个实例同时重启，每个实例的缓存都是空的。这时候哪怕只有一个请求打到每个实例上，数据库就要承受 50 倍的瞬时压力。如果用户的请求再密集一些——比如所有实例同时收到 100 个请求——数据库瞬间就面临 5000 个并发查询。&lt;/p&gt;&#xA;&lt;p&gt;50 倍的放大效应——这就是雪崩。&lt;/p&gt;&#xA;&lt;h3 id=&#34;保护怎么做&#34;&gt;&lt;a href=&#34;#%e4%bf%9d%e6%8a%a4%e6%80%8e%e4%b9%88%e5%81%9a&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;保护怎么做？&#xA;&lt;/h3&gt;&lt;p&gt;&amp;ldquo;保护&amp;quot;就是在惰性加载的基础上加一个断路器或限流器：当数据库连接数超过某个阈值时，拒绝后续请求，或者返回降级响应。&lt;/p&gt;&#xA;&lt;p&gt;常见的保护手段有三种：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;连接池限流&lt;/strong&gt;：设置数据库连接池上限，超过上限的请求排队或超时。简单有效，但排队可能导致请求堆积。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;断路器&lt;/strong&gt;：当数据库错误率达到阈值（比如常见默认值 50%，Hystrix 标准，请根据业务调整），断路器打开，直接返回降级响应，不再请求数据库。这能保护数据库不被完全打垮。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;缓存空值&lt;/strong&gt;：当数据库查询为空时，也把&amp;quot;空结果&amp;quot;缓存起来（设置较短 TTL），避免同一 Key 的反复穿透。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;这其实是一个权衡：你是接受少量请求的慢响应，还是接受系统启动的额外延迟？&lt;/p&gt;&#xA;&lt;h3 id=&#34;实测数据&#34;&gt;&lt;a href=&#34;#%e5%ae%9e%e6%b5%8b%e6%95%b0%e6%8d%ae&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;实测数据&#xA;&lt;/h3&gt;&lt;p&gt;我跑了一组模拟实验，配置如下：10,000 个缓存键，20% 是热点键（占 80% 访问量），50 个并发 worker，总共 10,000 次请求。运行环境：Go 1.26.4，本地单机 Redis，单表简单查询。&lt;/p&gt;&#xA;&lt;p&gt;结果：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;策略&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;缓存命中率&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;DB 查询数&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;DB 峰值连接&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;平均延迟&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;惰性加载+保护&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;61.8%&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;3,816&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;50&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;~3.6ms&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;主动预热&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;82.2%&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;1,778&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;0&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;~1.8ms&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;渐进式预热&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;82.1%&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;1,790&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;1&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;~1.9ms&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;惰性加载+保护的命中率只有 62%，DB 峰值连接数冲到 50（模拟的断路器上限），平均延迟接近 3.7ms——比预热方案慢了将近一倍。&lt;/p&gt;&#xA;&lt;p&gt;但注意：这个数据是在&lt;strong&gt;高并发、热点集中&lt;/strong&gt;的场景下测的。如果你的系统 QPS 很低，惰性加载的表现完全不同——命中率会更高，DB 连接数也不会成为瓶颈。&lt;/p&gt;&#xA;&lt;h3 id=&#34;什么时候选这个象限&#34;&gt;&lt;a href=&#34;#%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e9%80%89%e8%bf%99%e4%b8%aa%e8%b1%a1%e9%99%90&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;什么时候选这个象限？&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;QPS 低&lt;/strong&gt;：系统日常 QPS &amp;lt; 100，数据库扛得住偶尔的全量穿透&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;容忍度高&lt;/strong&gt;：首次访问的几秒延迟对业务无影响（如 CMS、后台管理）&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;数据量大&lt;/strong&gt;：全量预热成本太高，且大部分数据可能永远不会被访问&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;快速迭代&lt;/strong&gt;：服务频繁重启/发布，没有时间等预热完成&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;如果你在做一个内部管理系统，用户每天就几十个人用，缓存预热完全是浪费感情。惰性加载 + 一个简单的限流就够用了。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/cache-warmup-strategies/p2-lazy-loading-flow.png&#34; alt=&#34;惰性加载流程&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;三低容忍--低成本主动预热&#34;&gt;&lt;a href=&#34;#%e4%b8%89%e4%bd%8e%e5%ae%b9%e5%bf%8d--%e4%bd%8e%e6%88%90%e6%9c%ac%e4%b8%bb%e5%8a%a8%e9%a2%84%e7%83%ad&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;三、低容忍 × 低成本：主动预热&#xA;&lt;/h2&gt;&lt;p&gt;如果你的业务容忍度低——首次访问延迟不可接受，那就主动预热：系统启动时，先把热点数据加载到缓存里。&lt;/p&gt;&#xA;&lt;p&gt;我的做法是：既然冷启动穿透是因为缓存冷，那就让缓存不冷。提前把预测会被频繁访问的数据加载进去，让系统在对外提供服务前就已经&amp;quot;暖&amp;quot;了。&lt;/p&gt;&#xA;&lt;h3 id=&#34;关键问题预热什么&#34;&gt;&lt;a href=&#34;#%e5%85%b3%e9%94%ae%e9%97%ae%e9%a2%98%e9%a2%84%e7%83%ad%e4%bb%80%e4%b9%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;关键问题：预热什么？&#xA;&lt;/h3&gt;&lt;p&gt;主动预热最核心的问题不是&amp;quot;怎么预热&amp;rdquo;，而是&amp;quot;预热什么&amp;quot;。加载了不该加载的数据，比不加载更糟糕——浪费内存、拖慢启动。&lt;/p&gt;&#xA;&lt;p&gt;我一般把预热数据分为两类：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;可预测的热点&lt;/strong&gt;：比如电商系统的商品详情、配置中心的配置项、用户的权限数据——这些几乎 100% 会被访问&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;统计出来的热点&lt;/strong&gt;：通过历史访问日志分析出来的高频 Key，比如过去 24 小时 PV Top 1000&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;第二类有一个陷阱：热点是会变的。昨天的高频 Key 今天可能就冷下来了。所以主动预热通常需要一个&amp;quot;预热 + 定期刷新&amp;quot;的组合方案。热点预测永远有误差。我常用的做法是：对预测的热点设置较短的 TTL，让缓存自己&amp;quot;验证&amp;quot;这个热点是否真的热——如果访问频率低，TTL 过期后自然淘汰。&lt;/p&gt;&#xA;&lt;p&gt;怎么做定期刷新？两种思路：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;定时全量刷新&lt;/strong&gt;：每 N 分钟重新跑一次热点分析，重新加载热点数据。简单但浪费——热点数据可能没变。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;增量监听&lt;/strong&gt;：监听数据库的变更日志（如 MySQL 的 binlog，可以使用 Canal、Debezium 等成熟 CDC 工具），数据变了才更新缓存。成本高但精准。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;大部分团队从定时全量刷新开始就够了——又不是所有数据每秒都在变。&lt;/p&gt;&#xA;&lt;p&gt;还有一个容易被忽略的问题：&lt;strong&gt;预热的数据应该设置什么样的 TTL？&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;如果你预热的商品详情设置 24 小时 TTL，但商品价格在 2 小时后变了呢？用户看到的价格就是错的。我踩过这个坑之后的做法是：预热数据设置较短的 TTL（比如 5 分钟），同时配合定期刷新来续期。这其实是一个自动降级机制——刷新任务挂了后缓存自动过期，系统退回到惰性加载模式。虽然不是最优状态，但至少不会提供过期数据。&lt;/p&gt;&#xA;&lt;p&gt;注意：所有预热数据的 TTL 不要设成相同的值。加上随机偏移（比如基础值 ± 随机范围），避免同时过期触发第二次雪崩。&lt;/p&gt;&#xA;&lt;h3 id=&#34;启动时间的账&#34;&gt;&lt;a href=&#34;#%e5%90%af%e5%8a%a8%e6%97%b6%e9%97%b4%e7%9a%84%e8%b4%a6&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;启动时间的账&#xA;&lt;/h3&gt;&lt;p&gt;预热是要花时间的，而且这时间花在&amp;quot;系统启动阶段&amp;quot;——也就是你最希望系统快速上线的时候。&lt;/p&gt;&#xA;&lt;p&gt;从我的基准测试来看：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;预热数据量&lt;/th&gt;&#xA;          &lt;th style=&#34;text-align: center&#34;&gt;耗时&lt;/th&gt;&#xA;          &lt;th&gt;影响评估&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;1,000 条&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;~0.5ms&lt;/td&gt;&#xA;          &lt;td&gt;几乎无感&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;10,000 条&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;~3ms&lt;/td&gt;&#xA;          &lt;td&gt;可接受&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;100,000 条&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;~28ms&lt;/td&gt;&#xA;          &lt;td&gt;启动变慢，但可接受&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;1,000,000 条&lt;/td&gt;&#xA;          &lt;td style=&#34;text-align: center&#34;&gt;~320ms&lt;/td&gt;&#xA;          &lt;td&gt;已不可忽略&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;大多数业务场景的热点数据在 1,000-10,000 条这个量级，预热的成本不到 3ms。从这个角度看，主动预热几乎是&amp;quot;免费的&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;但有一个例外：如果你的热点数据是百万级的，320ms 的预热时间在微服务架构中可能触发健康检查的超时。这个场景我会放到后面的&amp;quot;高成本&amp;quot;象限讨论。&lt;/p&gt;&#xA;&lt;h3 id=&#34;怎么实现&#34;&gt;&lt;a href=&#34;#%e6%80%8e%e4%b9%88%e5%ae%9e%e7%8e%b0&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;怎么实现？&#xA;&lt;/h3&gt;&lt;p&gt;主动预热最常见的实现方式是：在应用启动后、对外提供服务前，执行一个预热函数。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Warmup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;redis&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;redis&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Client&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;hotKeys&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;loadHotKeys&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;fmt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Errorf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;加载热点列表失败: %w&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 批量写入 Redis&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pipe&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;redis&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Pipeline&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;range&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;hotKeys&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pipe&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;TTL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pipe&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Exec&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 预热允许部分失败，但至少记录日志&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;预热部分写入失败: %v&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这段代码在 Spring Boot 中通常放在 &lt;code&gt;@PostConstruct&lt;/code&gt; 或 &lt;code&gt;CommandLineRunner&lt;/code&gt; 里（注意：Spring Boot 3.x + Java 17+ 需要使用 &lt;code&gt;jakarta.annotation&lt;/code&gt; 包），在 Go 中放在 &lt;code&gt;main()&lt;/code&gt; 函数中服务启动之前。&lt;/p&gt;&#xA;&lt;p&gt;注意：如果使用 &lt;code&gt;go Warmup()&lt;/code&gt; 异步预热，服务启动时缓存可能尚未就绪。Spring Boot 的 &lt;code&gt;@PostConstruct&lt;/code&gt; 默认同步阻塞。两种语言的预热语义不同，需要根据业务决定。&lt;/p&gt;&#xA;&lt;p&gt;还有一个容易被忽略的问题：&lt;strong&gt;预热失败怎么办？&lt;/strong&gt; 如果数据库在预热时挂了，预热函数返回错误，你的服务应该拒绝启动，还是先启动再说？&lt;/p&gt;&#xA;&lt;p&gt;我一般这样区分：对于秒杀系统，缓存没有预热好就上线等于灾难——应该阻止启动。对于一般业务，更合理的做法是：预热失败打印警告，服务继续启动，让惰性加载兜底。&lt;/p&gt;&#xA;&lt;h3 id=&#34;多实例场景的陷阱&#34;&gt;&lt;a href=&#34;#%e5%a4%9a%e5%ae%9e%e4%be%8b%e5%9c%ba%e6%99%af%e7%9a%84%e9%99%b7%e9%98%b1&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;多实例场景的陷阱&#xA;&lt;/h3&gt;&lt;p&gt;50 个实例同时重启，每个都执行一次预热——等于 50 次同样的数据库查询同时打过去。经典解法是使用分布式锁或 leader 选举，只让一个实例执行预热，预热结果共享给其他实例。&lt;/p&gt;&#xA;&lt;p&gt;如果你的业务已经上了服务发现或配置中心，可以借助这些基础设施来做 leader 预热：启动时先尝试获取分布式锁，拿到锁的实例负责预热，其他实例等待预热完成或直接使用缓存中的现有数据。&lt;/p&gt;&#xA;&lt;h3 id=&#34;什么时候选这个象限-1&#34;&gt;&lt;a href=&#34;#%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e9%80%89%e8%bf%99%e4%b8%aa%e8%b1%a1%e9%99%90-1&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;什么时候选这个象限？&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;热点可预测&lt;/strong&gt;：你知道哪些数据会被频繁访问（比如商品详情、配置项）&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;容忍度低&lt;/strong&gt;：首次访问延迟不可接受（比如秒杀系统、实时推荐）&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;数据量适中&lt;/strong&gt;：热点数据量 &amp;lt; 100,000 条，预热成本可控&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;服务重启不频繁&lt;/strong&gt;：每次重启都做一次预热，启动频率越低越划算&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/cache-warmup-strategies/p3-warmup-compare.png&#34; alt=&#34;预热对比&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;四高容忍--高成本渐进式预热&#34;&gt;&lt;a href=&#34;#%e5%9b%9b%e9%ab%98%e5%ae%b9%e5%bf%8d--%e9%ab%98%e6%88%90%e6%9c%ac%e6%b8%90%e8%bf%9b%e5%bc%8f%e9%a2%84%e7%83%ad&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;四、高容忍 × 高成本：渐进式预热&#xA;&lt;/h2&gt;&lt;p&gt;主动预热的前提是你&amp;quot;知道&amp;quot;热点是什么。但如果热点不确定呢？&lt;/p&gt;&#xA;&lt;p&gt;比如你做的是一个社交 Feed 系统，每个用户看到的内容都不一样。你没法提前知道&amp;quot;哪个用户的 Feed 会被访问最多&amp;quot;，因为热点完全取决于用户行为。&lt;/p&gt;&#xA;&lt;p&gt;这时候主动预热没用——你不可能预热所有用户的 Feed。惰性加载+保护又太被动——用户量大，首次访问的延迟会拉低整体体验。&lt;/p&gt;&#xA;&lt;p&gt;渐进式预热就是第三种选择：&lt;strong&gt;先让系统起来，然后边服务边预热。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;这听起来像是&amp;quot;两全其美&amp;quot;，但它有一个隐含的代价：预热期间，部分请求仍然会穿透到数据库。渐进式预热不是在&amp;quot;要不要穿透&amp;quot;之间选，而是在&amp;quot;穿透多少&amp;quot;和&amp;quot;等待多久&amp;quot;之间找平衡点。&lt;/p&gt;&#xA;&lt;h3 id=&#34;怎么做&#34;&gt;&lt;a href=&#34;#%e6%80%8e%e4%b9%88%e5%81%9a&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;怎么做？&#xA;&lt;/h3&gt;&lt;p&gt;渐进式预热的做法是：系统启动后立即开始分批加载热点数据，同时用限流/断路器保护数据库。和主动预热的关键区别在于：服务不需要等预热完成再启动。&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;GradualWarmup&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;redis&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;redis&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Client&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 使用游标分页加载，避免一次性加载全部热点到内存&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;cursor&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int64&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;batchSize&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;hotKeys&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;nextCursor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;loadHotKeysPage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;cursor&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;batchSize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;分批加载热点失败: %v&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 每批写入 Redis&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pipe&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;redis&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Pipeline&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;range&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;hotKeys&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pipe&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;item&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;TTL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;pipe&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Exec&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;log&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Printf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;预热部分写入失败: %v&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 每批之间间隔 200ms，给数据库喘息空间&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Sleep&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;200&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Millisecond&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;nextCursor&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;==&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;            &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;cursor&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;nextCursor&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;关键参数是 &lt;code&gt;batchSize&lt;/code&gt; 和批间隔。batchSize 太大等于全量预热，太小又预热太慢。我的建议是从 100 开始，根据预热期间数据库的负载动态调整。&lt;/p&gt;&#xA;&lt;p&gt;从我的模拟实验来看，渐进式预热的表现和主动预热非常接近：命中率 82.1%，DB 峰值连接只有 1。&lt;/p&gt;&#xA;&lt;p&gt;它和主动预热的核心差异不是性能，是&lt;strong&gt;灵活性&lt;/strong&gt;：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;主动预热：启动时一次加载完，之后就不管了&lt;/li&gt;&#xA;&lt;li&gt;渐进式预热：持续加载，可以动态调整预热策略&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;比如，你可以根据预热期间的缓存命中率来动态调整：如果命中率已经超过 90%，可以放慢预热速度甚至停止；如果命中率仍然很低，加快预热速度。&lt;/p&gt;&#xA;&lt;h3 id=&#34;和主动预热怎么选&#34;&gt;&lt;a href=&#34;#%e5%92%8c%e4%b8%bb%e5%8a%a8%e9%a2%84%e7%83%ad%e6%80%8e%e4%b9%88%e9%80%89&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;和主动预热怎么选？&#xA;&lt;/h3&gt;&lt;p&gt;如果你的热点是明确的、稳定的，选主动预热就够了，不需要渐进式的复杂性。只有在你&lt;strong&gt;不确定&lt;/strong&gt;或者&lt;strong&gt;数据量大到一次加载不完&lt;/strong&gt;的时候，渐进式预热才值得。&lt;/p&gt;&#xA;&lt;p&gt;我常用的判断标准：如果预热耗时超过服务启动时间的 20%，就该考虑渐进式预热了。&lt;/p&gt;&#xA;&lt;h3 id=&#34;什么时候选这个象限-2&#34;&gt;&lt;a href=&#34;#%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e9%80%89%e8%bf%99%e4%b8%aa%e8%b1%a1%e9%99%90-2&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;什么时候选这个象限？&#xA;&lt;/h3&gt;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;热点不确定&lt;/strong&gt;：你不知道哪些数据会被频繁访问&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;数据量大&lt;/strong&gt;：全量预热会显著拖慢启动（百万级以上）&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;需要持续更新&lt;/strong&gt;：热点数据会随时间变化，需要持续加载&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;高可用要求&lt;/strong&gt;：不能因为预热而延迟服务启动&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/cache-warmup-strategies/p4-gradual-warmup-flow.png&#34; alt=&#34;渐进式预热流程&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/cache-warmup-strategies/p5-gradual-warmup-timeline.png&#34; alt=&#34;渐进式预热时间轴&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;五低容忍--高成本最棘手的象限&#34;&gt;&lt;a href=&#34;#%e4%ba%94%e4%bd%8e%e5%ae%b9%e5%bf%8d--%e9%ab%98%e6%88%90%e6%9c%ac%e6%9c%80%e6%a3%98%e6%89%8b%e7%9a%84%e8%b1%a1%e9%99%90&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;五、低容忍 × 高成本：最棘手的象限&#xA;&lt;/h2&gt;&lt;p&gt;如果你的业务既对延迟敏感（低容忍），又需要预热大量数据（高成本），矩阵的右下角——这是最难的场景。&lt;/p&gt;&#xA;&lt;p&gt;矩阵上说&amp;quot;渐进式预热（或架构优化）&amp;quot;。我来展开&amp;quot;架构优化&amp;quot;是什么意思。&lt;/p&gt;&#xA;&lt;p&gt;这个象限的核心矛盾是：系统启动必须快，但缓存又不能不预热。渐进式预热可以缓解这个问题——服务先起来，边服务边预热——但预热期间仍然会有部分请求穿透到数据库，低容忍度的业务可能接受不了。&lt;/p&gt;&#xA;&lt;p&gt;这时候有两条路：&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第一条路：本地缓存 + 预热拆分&lt;/strong&gt;&#xA;在应用层加一层本地缓存（如 Go 的 sync.Map、Java 的 Caffeine），热点数据优先从本地缓存读取。预热只预热 Redis 这一层，本地缓存靠惰性加载。启动速度几乎不受影响，本地缓存命中率在预热完成后自然下降。代价是多了一层缓存一致性要维护。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/cache-warmup-strategies/p7-local-cache-split.png&#34; alt=&#34;本地缓存&amp;#43;预热拆分&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;第二条路：重新评估缓存必要性&lt;/strong&gt;&#xA;有时候&amp;quot;低容忍 + 高成本&amp;quot;意味着你的架构选型有问题——缓存不是最好的解决方案。比如可以考虑：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;数据分片：把一个大 Redis 拆成多个，每个分片的数据量变小，预热成本自然降低&lt;/li&gt;&#xA;&lt;li&gt;读写分离：读走从库、写走主库，减少缓存的压力&lt;/li&gt;&#xA;&lt;li&gt;换个思路：是否可以用 CDN 或预计算来代替缓存？&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/cache-warmup-strategies/p1-decision-matrix.png&#34; alt=&#34;决策矩阵全景&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;六决策树三步走到答案&#34;&gt;&lt;a href=&#34;#%e5%85%ad%e5%86%b3%e7%ad%96%e6%a0%91%e4%b8%89%e6%ad%a5%e8%b5%b0%e5%88%b0%e7%ad%94%e6%a1%88&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;六、决策树：三步走到答案&#xA;&lt;/h2&gt;&lt;p&gt;如果上面四个象限看完还是不确定，走这个流程：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;你的业务能接受缓存冷启动吗？&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;能 → 惰性加载+保护就够了（回到高容忍×低成本）&lt;/li&gt;&#xA;&lt;li&gt;不能 → 进入下一步&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;你知道热点数据是什么吗？&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;知道 → 主动预热（低容忍×低成本）&lt;/li&gt;&#xA;&lt;li&gt;不知道 → 进入下一步&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;&lt;strong&gt;预热会拖慢启动吗？&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;不会 → 主动预热（即使用统计出来的热点也值得）&lt;/li&gt;&#xA;&lt;li&gt;会 → 渐进式预热（高容忍×高成本 或 低容忍×高成本）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;这个决策树覆盖了大多数场景。如果你走到第三层还是不确定，说明你的场景比较特殊——可能根本不需要缓存，或者需要重新考虑架构。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/cache-warmup-strategies/p6-decision-tree.png&#34; alt=&#34;决策树&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;比如有些场景下缓存 Key 设计不合理，大部分请求都集中在少数 Key 上——这时候与其纠结预热策略，不如先看看缓存的设计是否合理。&lt;/p&gt;&#xA;&lt;h3 id=&#34;矩阵之外什么时候不该用缓存&#34;&gt;&lt;a href=&#34;#%e7%9f%a9%e9%98%b5%e4%b9%8b%e5%a4%96%e4%bb%80%e4%b9%88%e6%97%b6%e5%80%99%e4%b8%8d%e8%af%a5%e7%94%a8%e7%bc%93%e5%ad%98&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;矩阵之外：什么时候不该用缓存？&#xA;&lt;/h3&gt;&lt;p&gt;最后说一个反直觉的结论：&lt;strong&gt;有时候不做缓存，比做缓存好。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;如果你的数据库本身响应就很快（比如单表百万级的 PostgreSQL，简单主键查询场景），缓存引入的复杂性可能得不偿失。缓存预热、缓存一致性、缓存穿透——这些问题的维护成本，可能超过缓存带来的性能收益。&lt;/p&gt;&#xA;&lt;p&gt;我的判断标准：如果数据库 P99 延迟 &amp;lt; 5ms，且 QPS &amp;lt; 1000，先别急着加缓存（注意：这只是简单查询场景的参考值，复杂 JOIN 或高并发下 P99 远不止 5ms）。先看看慢查询能不能优化。缓存是用来解决&amp;quot;优化解决不了的问题&amp;quot;的，不是用来掩盖设计缺陷的。&lt;/p&gt;&#xA;&lt;p&gt;还有一点：加了缓存不等于加了预热。很多团队上了 Redis 但没做预热，结果上线第一天缓存是空的，数据库被打穿。如果你决定用缓存，至少要确保惰性加载+保护这一层兜底是到位的。先让它不崩，再考虑怎么让它快。&lt;/p&gt;&#xA;&lt;p&gt;我见过一个案例：一个日活百万的 App，缓存集群重启后数据库直接被冲垮，DBA 紧急扩容才恢复。事后排查发现，他们根本没有缓存缺失的保护机制——Redis 一重启，数据库就裸奔。加一个简单的限流，整个事故就能避免。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/cache-warmup-strategies/p8-when-not-to-cache.png&#34; alt=&#34;什么时候不该用缓存&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&lt;h2 id=&#34;回到那个矩阵&#34;&gt;&lt;a href=&#34;#%e5%9b%9e%e5%88%b0%e9%82%a3%e4%b8%aa%e7%9f%a9%e9%98%b5&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;回到那个矩阵&#xA;&lt;/h2&gt;&lt;p&gt;没有银弹。选对策略比选对工具重要。&lt;/p&gt;&#xA;&lt;p&gt;大多数团队遇到冷启动穿透的第一反应是&amp;quot;加预热&amp;quot;——这没错，但问一句&amp;quot;我的场景真的需要预热吗&amp;quot;往往更值钱。&lt;/p&gt;&#xA;&lt;p&gt;回到开头的那个矩阵：你的业务在哪个象限？&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;高容忍 × 低成本&lt;/strong&gt; → 惰性加载+保护，省掉预热的运维成本&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;低容忍 × 低成本&lt;/strong&gt; → 主动预热，命中率最高&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;高容忍 × 高成本&lt;/strong&gt; → 渐进式预热，在灵活性和安全感之间找到平衡&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;低容忍 × 高成本&lt;/strong&gt; → 渐进式预热（或重新考虑架构）&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/cache-warmup-strategies/p9-back-to-matrix-summary.png&#34; alt=&#34;回到矩阵·总结&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;你的业务对&amp;quot;冷&amp;quot;有多敏感？&lt;/p&gt;&#xA;&lt;p&gt;下次遇到缓存重启后数据库 CPU 100% 的问题，先问自己三个问题：我的业务能忍多久？我知道热点在哪吗？预热会影响启动吗？问清楚了，方案自然就出来了。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;原文发布于 &lt;a class=&#34;link&#34; href=&#34;https://www.wujiachen.com.cn/posts/cache-warmup-strategies&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;止语Lab&lt;/a&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;</description>
        </item><item>
            <title>并发模型三流派：CSP / Actor / 线程</title>
            <link>https://www.wujiachen.com.cn/posts/concurrency-models/</link>
            <pubDate>Sat, 30 May 2026 11:03:55 +0800</pubDate>
            <guid>https://www.wujiachen.com.cn/posts/concurrency-models/</guid>
            <description>&lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/cover.png&#34; alt=&#34;Featured image of post 并发模型三流派：CSP / Actor / 线程&#34; /&gt;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/cover.png&#34; alt=&#34;封面&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;很多并发模型比较，问的是&amp;quot;谁更先进&amp;quot;。这篇换个问法：状态归谁，等待归谁，失败归谁。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;很多文章讲并发模型，喜欢从一个大表开始：线程、协程、Actor、CSP、async/await，各自一列，优缺点排开。&lt;/p&gt;&#xA;&lt;p&gt;这种写法看起来完整，但读完很容易只记住几个标签：Go 是 CSP，Erlang 是 Actor，Java 是线程。&lt;/p&gt;&#xA;&lt;p&gt;问题就在这里。&lt;/p&gt;&#xA;&lt;p&gt;真实工程里，你不会因为一个模型&amp;quot;更先进&amp;quot;就选它。你真正关心的是：这段并发代码出了问题以后，我该去哪里看？谁能改状态？谁在等？谁接住失败？&lt;/p&gt;&#xA;&lt;p&gt;这篇不做先进性排名。但&amp;quot;不排名&amp;quot;不等于&amp;quot;三种模型完全等价&amp;quot;——某些场景客观偏好某种模型，电信级长连接和毫秒级故障转移下 Actor 有结构性优势。本文反对的是脱离场景的抽象排名，不是反对场景化判断。&lt;/p&gt;&#xA;&lt;p&gt;我会用同一个任务编排器，把 Go、Erlang、Java 三种心智模型放在同一张白板上看。不是看语法谁漂亮，而是看三件事：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;状态归谁&lt;/strong&gt;：谁拥有结果，谁能修改，谁负责一致性。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;等待归谁&lt;/strong&gt;：阻塞、超时、取消、背压由谁表达。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;失败归谁&lt;/strong&gt;：一个子任务失败后，错误被关在哪里，谁决定恢复或扩散。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;这三个问题，比&amp;quot;CSP / Actor / 线程谁好&amp;quot;更接近工程现场。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/opening-wrong-question-vs-three-questions.png&#34; alt=&#34;错误问题 vs 正确问题：从&amp;#34;谁更先进&amp;#34;转向&amp;#34;三问框架&amp;#34;&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;1-先固定一个任务聚合三个下游&#34;&gt;&lt;a href=&#34;#1-%e5%85%88%e5%9b%ba%e5%ae%9a%e4%b8%80%e4%b8%aa%e4%bb%bb%e5%8a%a1%e8%81%9a%e5%90%88%e4%b8%89%e4%b8%aa%e4%b8%8b%e6%b8%b8&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;1. 先固定一个任务：聚合三个下游&#xA;&lt;/h2&gt;&lt;p&gt;先别急着下定义。&lt;/p&gt;&#xA;&lt;p&gt;想象一个用户画像接口。它要同时请求三个下游：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;profile&lt;/code&gt;：基础资料，通常很快。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;billing&lt;/code&gt;：付费状态，偶尔返回业务错误。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;risk&lt;/code&gt;：风控标签，偶尔超时。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;聚合层要做的事也很普通：并发发起请求，等结果回来，组装成一个响应。如果某个下游超时，不能让整个请求无限挂着；如果某个下游明确失败，要决定其他任务还要不要继续跑。&lt;/p&gt;&#xA;&lt;p&gt;这个小场景写了三份最小实现：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;语言 / 风格&lt;/th&gt;&#xA;          &lt;th&gt;实现路径&lt;/th&gt;&#xA;          &lt;th&gt;运行场景&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Go / CSP 风格&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;goroutine + channel + context&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;success、timeout、worker-error&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Java / virtual thread&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;virtual thread + Future/CompletionService&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;success、timeout、worker-error&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Erlang / Actor 风格&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;process + message + link/EXIT&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;success、timeout、worker-error&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch1-task-orchestrator-scene.png&#34; alt=&#34;同一个任务编排器：三个下游、三种责任表达方式&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;代码不测性能。这里没有 QPS，也没有延迟排名。&lt;/p&gt;&#xA;&lt;p&gt;这么做是有意的。并发模型讨论很容易被性能数字带偏：哪个更快、哪个开销更低、哪个能撑更多连接。这些问题当然重要，但不是这篇要解决的事。此场景偏短生命周期聚合，Actor 的长生命周期优势未覆盖——边界先放这里。&lt;/p&gt;&#xA;&lt;p&gt;一个模型性能再好，如果团队不知道失败以后谁来收场，它还是会在生产环境里变成黑盒。反过来，一个模型看起来不够&amp;quot;酷&amp;quot;，但责任边界清楚，排障时反而更省命。&lt;/p&gt;&#xA;&lt;p&gt;技术选型最后比的，往往是事故发生时谁能最快定位、最快止血，谁能避免下一次复发。&lt;/p&gt;&#xA;&lt;p&gt;只看一件事：同一个工程任务，在不同模型里，责任被放到了哪里。&lt;/p&gt;&#xA;&lt;p&gt;并发模型最容易被讲成抽象概念。但工程师真正付出的成本，通常不在&amp;quot;能不能并发&amp;quot;，而在&amp;quot;并发以后谁负责收尾&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;下面进入三问。&lt;/p&gt;&#xA;&lt;h2 id=&#34;2-第一问状态归谁&#34;&gt;&lt;a href=&#34;#2-%e7%ac%ac%e4%b8%80%e9%97%ae%e7%8a%b6%e6%80%81%e5%bd%92%e8%b0%81&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2. 第一问：状态归谁&#xA;&lt;/h2&gt;&lt;p&gt;先看状态。&lt;/p&gt;&#xA;&lt;p&gt;这个任务里，状态主要是&amp;quot;已经拿到了哪些下游结果&amp;quot;。它看起来只是一个结果集合，但并发一上来，问题就变了：多个任务能不能同时改它？如果能，谁保证一致性？如果不能，结果怎么汇合？&lt;/p&gt;&#xA;&lt;p&gt;Go 的写法很典型。&lt;/p&gt;&#xA;&lt;p&gt;Go 版本里，每个 worker goroutine 不直接修改聚合结果。它只把一个不可变的 &lt;code&gt;Result&lt;/code&gt; 发到 channel。真正持有结果列表的是聚合 goroutine——社区惯用 &lt;code&gt;errgroup&lt;/code&gt; 进一步封装这个模式。&lt;/p&gt;&#xA;&lt;p&gt;实验输出里，这个观察写成一句话：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;场景&lt;/th&gt;&#xA;          &lt;th&gt;状态所有权观察&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Go success / timeout / worker-error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;aggregator goroutine owns the result slice; workers only send immutable Result values&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;翻译成人话就是：worker 只交信封，不改账本。账本在聚合者手里。&lt;/p&gt;&#xA;&lt;p&gt;这就是 Go 偏 CSP 心智模型的好处。它鼓励你把协作关系写到通信结构里。channel 不只是传值，它也在暗示：谁能说话，谁能接收，谁能关闭这条路。&lt;/p&gt;&#xA;&lt;p&gt;但这里要补一条边界。&lt;/p&gt;&#xA;&lt;p&gt;Go 不是&amp;quot;无共享状态语言&amp;quot;。Go 项目里 mutex、atomic、WaitGroup、context 到处都是。把 Go 写成&amp;quot;只能 channel&amp;quot;是误导。更准确的说法是：Go 的默认心智模型鼓励你先问，能不能通过通信把状态所有权收束起来；如果收不住，再用锁和原子操作。&lt;/p&gt;&#xA;&lt;p&gt;Erlang 的状态边界更硬一点。&lt;/p&gt;&#xA;&lt;p&gt;在 Actor 风格里，每个 process 天然拥有自己的局部状态。外部不能直接伸手改它，只能发消息影响它。Erlang 实验里，worker process 完成后给 parent 发消息，parent 负责聚合。&lt;/p&gt;&#xA;&lt;p&gt;输出里是这样：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;场景&lt;/th&gt;&#xA;          &lt;th&gt;状态所有权观察&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Erlang success / timeout / worker-error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;each process owns its local state; parent aggregates only messages&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这个表达很 Actor：实体先存在，通信再发生。&lt;/p&gt;&#xA;&lt;p&gt;CSP 更像&amp;quot;我关心通道和协作关系&amp;quot;；Actor 更像&amp;quot;我关心实体和边界&amp;quot;。两者都是消息传递，但重心不同。CSP 的问题常常是&amp;quot;这条通信路径怎么设计&amp;quot;；Actor 的问题常常是&amp;quot;这个状态属于哪个实体&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;Java virtual thread 的位置又不一样。&lt;/p&gt;&#xA;&lt;p&gt;virtual thread 不会替你决定状态归谁。它解决的是阻塞线程的开销问题，共享状态如何保证一致性，仍然是应用层需要自己设计的。&lt;/p&gt;&#xA;&lt;p&gt;Java 实验里，聚合状态仍然是调用方普通对象和集合。virtual thread 让每个子任务可以继续写成同步阻塞代码，但它没有把共享状态变成私有状态，也不会自动给你 Actor 边界。&lt;/p&gt;&#xA;&lt;p&gt;输出里故意写得很直白：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;场景&lt;/th&gt;&#xA;          &lt;th&gt;状态所有权观察&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Java success / timeout / worker-error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;caller keeps aggregation state in ordinary objects; virtual threads do not change shared-state semantics&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这句话可以放进正文里反复提醒自己：virtual thread 改变了等待成本，但没有改变状态语义。&lt;/p&gt;&#xA;&lt;p&gt;第一问的答案大概是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Go 倾向于让状态通过通信汇聚。&lt;/li&gt;&#xA;&lt;li&gt;Erlang 倾向于让状态先属于一个 process。&lt;/li&gt;&#xA;&lt;li&gt;Java virtual thread 让你保留熟悉的对象和调用栈，但状态边界仍要自己设计。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;先别急着分高下。我们只看第一类责任：状态到底放在谁手里。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch2-state-ownership.png&#34; alt=&#34;状态所有权三分法：通道、实体、对象上下文&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;3-第二问等待归谁&#34;&gt;&lt;a href=&#34;#3-%e7%ac%ac%e4%ba%8c%e9%97%ae%e7%ad%89%e5%be%85%e5%bd%92%e8%b0%81&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;3. 第二问：等待归谁&#xA;&lt;/h2&gt;&lt;p&gt;并发代码第二个麻烦是等待。&lt;/p&gt;&#xA;&lt;p&gt;你发出三个下游请求，不可能一直等。&lt;code&gt;risk&lt;/code&gt; 慢了怎么办？整体超时谁说了算？已经成功的结果要不要保留？还没结束的任务谁去取消？&lt;/p&gt;&#xA;&lt;p&gt;先看边界感最强的方案。&lt;/p&gt;&#xA;&lt;p&gt;Erlang 的等待像 mailbox 里的时间边界。Erlang 版本用 parent process 收 worker 消息，&lt;code&gt;receive ... after&lt;/code&gt; 定义整体等待时间。&lt;code&gt;risk&lt;/code&gt; 超时后，parent 杀掉还没完成的 worker。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;erlang-timeout&lt;/code&gt; 的输出是：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;输出&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;completed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[billing,profile]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;canceled&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[risk]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Actor 风格的等待不是&amp;quot;共享一个 context&amp;quot;，而是&amp;quot;某个 process 在等消息&amp;quot;。这会改变排查入口。你会去看 mailbox、process 状态、消息有没有堆积，而不是先看某个共享取消对象。&lt;/p&gt;&#xA;&lt;p&gt;Go 的等待关系通常更显眼一些。&lt;/p&gt;&#xA;&lt;p&gt;在实验里，整体超时由 &lt;code&gt;context.WithTimeout&lt;/code&gt; 定义。worker 里用 &lt;code&gt;select&lt;/code&gt; 同时等两个东西：自己的模拟耗时，或者 &lt;code&gt;ctx.Done()&lt;/code&gt;。聚合者看到第一个错误后调用 &lt;code&gt;cancel()&lt;/code&gt;，兄弟 goroutine 通过同一个 context 收到取消。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;go-timeout&lt;/code&gt; 的输出是：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;输出&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;completed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[billing profile]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;canceled&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[risk]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;risk canceled: context deadline exceeded&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;你能从这组输出里看到 Go 的风格：等待和取消不是藏在运行时深处，而是直接写在应用编排代码里。&lt;/p&gt;&#xA;&lt;p&gt;这也是 Go 并发代码读起来有时&amp;quot;啰嗦&amp;quot;的原因。你要传 context，要处理 channel，要决定谁 close，要防止 goroutine 泄漏。它不替你消失这些问题。它只是把问题摆在桌面上。&lt;/p&gt;&#xA;&lt;p&gt;Java virtual thread 的等待最容易被误解。&lt;/p&gt;&#xA;&lt;p&gt;很多人听到 virtual thread，会以为 Java 的并发模型就变成了另一种东西。其实没有。virtual thread 的关键价值是：你可以继续写看起来同步的阻塞代码，而不会像平台线程那样为每个阻塞请求付出昂贵资源成本。&lt;/p&gt;&#xA;&lt;p&gt;Java 版本用 &lt;code&gt;Executors.newVirtualThreadPerTaskExecutor()&lt;/code&gt; 提交任务，再用 &lt;code&gt;CompletionService&lt;/code&gt; 等结果。代码仍然是 Future 编排：谁超时，谁取消，谁收错误，都由应用层决定。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;java-timeout&lt;/code&gt; 的输出是：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;输出&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;completed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[billing, profile]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;canceled&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[risk]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;overall timeout after 70ms&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这说明 virtual thread 主要解决&amp;quot;等待能不能便宜一点&amp;quot;。至于等待语义怎么设计，还得回到应用编排。&lt;/p&gt;&#xA;&lt;p&gt;把这三种模型放在一起，第二问会变得很清楚：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Erlang 把等待写成 parent process 对 mailbox 的接收边界。&lt;/li&gt;&#xA;&lt;li&gt;Go 把等待和取消写成 &lt;code&gt;context/select/channel&lt;/code&gt; 的协作协议。&lt;/li&gt;&#xA;&lt;li&gt;Java virtual thread 把等待保留在同步调用栈里，让 JVM 承接更多阻塞成本，但取消策略仍在应用层。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;不要只问&amp;quot;哪个模型更适合高并发&amp;quot;。先问：你的团队更想在代码哪里看到等待？process 边界里，通信结构里，还是同步调用栈里？&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch3-waiting-owner.png&#34; alt=&#34;等待责任三层图：mailbox、context、virtual thread&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;4-第三问失败归谁&#34;&gt;&lt;a href=&#34;#4-%e7%ac%ac%e4%b8%89%e9%97%ae%e5%a4%b1%e8%b4%a5%e5%bd%92%e8%b0%81&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;4. 第三问：失败归谁&#xA;&lt;/h2&gt;&lt;p&gt;状态和等待还只是开胃菜。并发模型真正拉开差距的地方，是失败。&lt;/p&gt;&#xA;&lt;p&gt;因为并发系统里，失败不是一个点。一个子任务失败以后，至少有三个问题跟着来：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;失败先被谁看到？&lt;/li&gt;&#xA;&lt;li&gt;兄弟任务要不要取消？&lt;/li&gt;&#xA;&lt;li&gt;这个失败会不会越过当前边界，影响更上层？&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;实验里加了两个失败场景：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;timeout&lt;/code&gt;：&lt;code&gt;risk&lt;/code&gt; 比整体超时更慢。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;worker-error&lt;/code&gt;：&lt;code&gt;billing&lt;/code&gt; 明确返回错误。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Go 的 worker-error 输出是：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;输出&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;completed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[profile]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;failed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[billing]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;canceled&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[risk]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;billing returned error&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这里的失败边界很应用化。&lt;code&gt;billing&lt;/code&gt; 出错后，聚合者调用 &lt;code&gt;cancel()&lt;/code&gt;，&lt;code&gt;risk&lt;/code&gt; 通过 context 退出。Go 没有一个&amp;quot;自动监督者&amp;quot;替你决定失败传播。你要把传播路径写出来。&lt;/p&gt;&#xA;&lt;p&gt;很多业务系统正需要这种显式控制。&lt;/p&gt;&#xA;&lt;p&gt;代价也在这里：如果你忘了传 context，忘了监听取消，忘了 drain channel，失败边界就会变成泄漏边界。Go 的并发事故里，goroutine 泄漏经常不是因为 goroutine 这个概念复杂，而是因为退出条件没有被完整表达。&lt;/p&gt;&#xA;&lt;p&gt;Erlang 的失败更像&amp;quot;边界先存在&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;最小 Erlang 实验里，worker 用 &lt;code&gt;spawn_link&lt;/code&gt; 启动——注意 parent 需要先 &lt;code&gt;process_flag(trap_exit, true)&lt;/code&gt; 才能把 EXIT 信号转为可处理消息，否则会被级联终止。&lt;code&gt;billing&lt;/code&gt; 失败时，parent 收到 &lt;code&gt;EXIT&lt;/code&gt; 信号，然后 kill 兄弟 process。&lt;/p&gt;&#xA;&lt;p&gt;输出是：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;输出&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;completed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[profile]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;failed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[billing]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;canceled&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[risk]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;{task_error,billing}&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这当然还不是完整 OTP supervision。真正的 Erlang 工程会有 supervision tree、restart strategy、monitor 等更完整的设计。这里补一个关键区分：link 是双向的（一方死另一方跟着死），monitor 是单向的（只收通知，自己不死）。supervision tree 大量依赖 monitor 来实现&amp;quot;观察而不殉葬&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;但即使在这个最小实验里，你也能看到 Actor 心智模型的味道：失败是 process 边界上的信号，不只是一个返回值。一个实体死了，另一个实体可以观察到它的死亡，并决定下一步。&lt;/p&gt;&#xA;&lt;p&gt;Java virtual thread 的失败边界则更接近传统线程模型，只是线程变轻了。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;java-worker-error&lt;/code&gt; 的输出是：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;输出&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;completed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[profile]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;failed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[billing returned error]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;canceled&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[risk]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;billing returned error&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这里的失败处理仍然靠应用编排 Future：捕获 &lt;code&gt;ExecutionException&lt;/code&gt;，记录第一个错误，取消未完成任务。&lt;/p&gt;&#xA;&lt;p&gt;Java 对&amp;quot;兄弟任务收束&amp;quot;这个问题已经给出了官方答案：&lt;code&gt;StructuredTaskScope&lt;/code&gt;（JEP 480，已正式进入 JDK）。它的设计思路是把一组子任务绑进同一个作用域，作用域退出时所有子任务必须结束——要么全成功，要么第一个失败触发其余取消。最小示例：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;try&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scope&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;StructuredTaskScope&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;ShutdownOnFailure&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Subtask&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;profile&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scope&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;fork&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fetchProfile&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;());&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Subtask&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;billing&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scope&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;fork&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fetchBilling&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;());&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Subtask&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;risk&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scope&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;fork&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fetchRisk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;());&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scope&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;join&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;           &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 等待全部完成或第一个失败&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scope&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;throwIfFailed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 有失败则抛出&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Result&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;profile&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;billing&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;risk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;());&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// scope 关闭时，未完成的子任务被自动取消&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这比手动 Future 编排清晰不少——失败边界直接写在语法结构里。不过 &lt;code&gt;StructuredTaskScope&lt;/code&gt; 的 API 仍在演进（自定义策略、嵌套作用域等场景还有边界），它尚未覆盖 supervision tree 那种&amp;quot;长期实体+重启策略&amp;quot;的场景。&lt;/p&gt;&#xA;&lt;p&gt;到这里，第三问的差异就出来了：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Go：失败边界是应用协议，靠 context、channel、错误处理约定串起来。&lt;/li&gt;&#xA;&lt;li&gt;Erlang：失败边界是 process 语义的一部分，link/monitor/supervision 让失败可观察、可隔离、可恢复。&lt;/li&gt;&#xA;&lt;li&gt;Java：失败从手动 Future 编排走向 &lt;code&gt;StructuredTaskScope&lt;/code&gt; 的作用域收束；线程变轻，失败策略正在被语法化。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;当三种模型都有明确的责任位置时，争论&amp;quot;谁更先进&amp;quot;反而显得多余。真正的差异在于：失败信号沿着什么路径传播，传播到哪里停下来。&lt;/p&gt;&#xA;&lt;p&gt;Actor 的核心设计选择，是把状态和失败一起包进实体边界里。这让它在长生命周期、需要隔离和恢复的场景下有天然优势。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch4-failure-boundary.png&#34; alt=&#34;失败边界三路径：取消信号、EXIT 信号、Future 取消&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;5-一个辅助观察责任在代码表面的分布密度&#34;&gt;&lt;a href=&#34;#5-%e4%b8%80%e4%b8%aa%e8%be%85%e5%8a%a9%e8%a7%82%e5%af%9f%e8%b4%a3%e4%bb%bb%e5%9c%a8%e4%bb%a3%e7%a0%81%e8%a1%a8%e9%9d%a2%e7%9a%84%e5%88%86%e5%b8%83%e5%af%86%e5%ba%a6&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;5. 一个辅助观察：责任在代码表面的分布密度&#xA;&lt;/h2&gt;&lt;p&gt;为了避免只凭感觉，还做了一个粗略的辅助观察。&lt;/p&gt;&#xA;&lt;p&gt;方法很简单：对三份实验源码做关键词扫描，粗略统计&amp;quot;状态/聚合&amp;quot;&amp;ldquo;等待/调度&amp;quot;&amp;ldquo;失败/取消&amp;quot;三类责任关键词在代码里出现的相对密度。这只是一个粗略代理指标，不是性能比较，也不是可维护性评分——它只反映一个现象：代码表面上，你的注意力会被拉向哪个方向。&lt;/p&gt;&#xA;&lt;p&gt;观察结果：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Go 代码里，等待和取消关键词（context、cancel、select、Done）分布最密集——你很容易看到协作痕迹。&lt;/li&gt;&#xA;&lt;li&gt;Erlang 代码里，失败边界关键词（EXIT、link、kill、trap_exit）占比最高——process 边界和信号非常显眼。&lt;/li&gt;&#xA;&lt;li&gt;Java 代码里，三类关键词分布相对均匀，状态管理稍密——同步写法保留，取消仍在 Future 编排层。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这个观察不能推出&amp;quot;Go 代码更复杂&amp;quot;或&amp;quot;Erlang 失败处理最好&amp;rdquo;。它能说明的只是：三种模型会把你注意力拉向不同地方。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;模型&lt;/th&gt;&#xA;          &lt;th&gt;写代码前先问&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Go&lt;/td&gt;&#xA;          &lt;td&gt;取消路径写完整了吗？&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Java virtual thread&lt;/td&gt;&#xA;          &lt;td&gt;同步写法背后的失败收束写清楚了吗？&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Erlang / Actor&lt;/td&gt;&#xA;          &lt;td&gt;实体边界和观察关系设计清楚了吗？&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;模型不会替你收拾现场。它只是让某些责任更容易被看见，另一些责任必须靠你自己补上。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch5-responsibility-surface.png&#34; alt=&#34;责任显性化观察：状态、等待、失败三类信号在代码表面的位置&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;6-反例别把语言等同于模型&#34;&gt;&lt;a href=&#34;#6-%e5%8f%8d%e4%be%8b%e5%88%ab%e6%8a%8a%e8%af%ad%e8%a8%80%e7%ad%89%e5%90%8c%e4%ba%8e%e6%a8%a1%e5%9e%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;6. 反例：别把语言等同于模型&#xA;&lt;/h2&gt;&lt;p&gt;专业读者可能已经想反驳了：&lt;/p&gt;&#xA;&lt;p&gt;Go 也能用锁啊。Java 也能写 Actor。Erlang 也不只是 mailbox。&lt;/p&gt;&#xA;&lt;p&gt;这些反驳都对。&lt;/p&gt;&#xA;&lt;p&gt;这篇文章从头到尾比较的是&amp;quot;默认心智模型&amp;rdquo;，不是&amp;quot;语言能力上限&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;语言像工具箱，默认心智模型像你最顺手拿起来的那把工具。Go 的工具箱里当然有锁和原子操作，但 goroutine、channel、context 会不断提醒你：协作关系能不能显式写出来？&lt;/p&gt;&#xA;&lt;p&gt;Java 的工具箱越来越大。virtual thread 让 thread-per-request 这种老写法重新变得可承受，但它并不会自动替你做状态隔离。你仍然要设计对象边界、取消策略、错误传播。&lt;/p&gt;&#xA;&lt;p&gt;Erlang 的工具箱也不只是&amp;quot;发消息&amp;quot;。如果只把 Erlang 当成 mailbox，你会错过它的核心设计：进程边界、失败信号、监督关系，以及&amp;quot;让它崩溃&amp;quot;背后的恢复语义。&lt;/p&gt;&#xA;&lt;p&gt;更准确的说法是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Go 不是 CSP 的纯实现，但它偏向用通信结构组织并发。&lt;/li&gt;&#xA;&lt;li&gt;Erlang 不只是 Actor 消息队列，但它偏向用实体边界组织状态和失败。&lt;/li&gt;&#xA;&lt;li&gt;Java 不等于传统重线程；virtual thread 让同步线程模型在高并发 I/O 下重新有吸引力。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;一旦你接受这个边界，争论就会少很多。&lt;/p&gt;&#xA;&lt;p&gt;给语言贴标签已经够多了。真正要问的是：同一个问题，用哪套责任分配方式更容易写对、读懂、排查。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch6-language-capability-vs-default-model.png&#34; alt=&#34;语言能力边界 vs 默认心智模型&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;7-最后给一张速查表&#34;&gt;&lt;a href=&#34;#7-%e6%9c%80%e5%90%8e%e7%bb%99%e4%b8%80%e5%bc%a0%e9%80%9f%e6%9f%a5%e8%a1%a8&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;7. 最后给一张速查表&#xA;&lt;/h2&gt;&lt;p&gt;如果你在设计一个并发模块，不要先问&amp;quot;我该用 CSP、Actor 还是线程&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;先问三句：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;状态归谁？&lt;/li&gt;&#xA;&lt;li&gt;等待归谁？&lt;/li&gt;&#xA;&lt;li&gt;失败归谁？&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;然后再看场景。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;场景信号&lt;/th&gt;&#xA;          &lt;th&gt;更该关注的问题&lt;/th&gt;&#xA;          &lt;th&gt;倾向的表达方式&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;多个 I/O 下游并发等待，取消关系复杂&lt;/td&gt;&#xA;          &lt;td&gt;等待归谁，取消怎么传播&lt;/td&gt;&#xA;          &lt;td&gt;Go channel/context，或 Java virtual thread + 明确 Future 编排&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;每个实体有长期状态和独立生命周期&lt;/td&gt;&#xA;          &lt;td&gt;状态归谁，失败被关在哪里&lt;/td&gt;&#xA;          &lt;td&gt;Actor/process 边界&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;团队强依赖同步调用栈可读性&lt;/td&gt;&#xA;          &lt;td&gt;能否保留顺序代码，同时降低等待成本&lt;/td&gt;&#xA;          &lt;td&gt;Java virtual thread&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;失败恢复比吞吐更重要&lt;/td&gt;&#xA;          &lt;td&gt;谁观察失败，谁负责重启/隔离&lt;/td&gt;&#xA;          &lt;td&gt;Erlang/Actor supervision 思路&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;状态集中且需要严格共享一致性&lt;/td&gt;&#xA;          &lt;td&gt;谁能改状态，锁或事务在哪里&lt;/td&gt;&#xA;          &lt;td&gt;线程 + 显式同步，或重新拆分状态所有权&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch7-concurrency-model-cheatsheet.png&#34; alt=&#34;并发模型选型速查卡：先问谁负责&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;这张表不是标准答案。它只是把&amp;quot;模型选择&amp;quot;翻译成&amp;quot;责任选择&amp;quot;，让讨论别一上来就变成语言站队。&lt;/p&gt;&#xA;&lt;p&gt;下次看到并发模型争论时，先别急着站队。&lt;/p&gt;&#xA;&lt;p&gt;有人说 channel 优雅，你就问：状态归谁？&lt;/p&gt;&#xA;&lt;p&gt;有人说 Actor 才可靠，你就问：失败归谁？&lt;/p&gt;&#xA;&lt;p&gt;能把这三件事回答清楚，模型名字反而没那么神秘。&lt;/p&gt;&#xA;&lt;p&gt;CSP、Actor、线程的区别，在于把状态、等待和失败交给不同的角色负责。&lt;/p&gt;&#xA;&lt;p&gt;工程里最怕的，不是三种模型给出了不同答案。&lt;/p&gt;&#xA;&lt;p&gt;是这三个问题，从头到尾没人问过。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;原文发布于 &lt;a class=&#34;link&#34; href=&#34;https://www.wujiachen.com.cn/posts/concurrency-models&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;止语Lab&lt;/a&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;</description>
        </item></channel>
</rss>
