<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>系统架构 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/categories/%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Thu, 22 Oct 2026 23:10:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/categories/%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>从余弦定理到 TikTok：一个用户的推荐流是怎么被算出来的</title>
      <link>https://ftmao.com/blog/posts/tiktok-recommendation-math-walkthrough/</link>
      <pubDate>Thu, 22 Oct 2026 23:10:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/tiktok-recommendation-math-walkthrough/</guid>
      <description>&lt;h1 id=&#34;从余弦定理到-tiktok一个用户的推荐流是怎么被算出来的&#34;&gt;从余弦定理到 TikTok：一个用户的推荐流是怎么被算出来的&lt;/h1&gt;
&lt;p&gt;上一篇&lt;a href=&#34;https://ftmao.com/blog/blog/posts/personalized-infinite-feed-architecture/&#34;&gt;《千人千面 + 无限瀑布流：推荐 Feed 的完整实现架构》&lt;/a&gt;把推荐系统的骨架拆完了：离线、近线、在线三层分工，召回、粗排、精排、重排四层漏斗。但通篇是框图和分工表，读完知道&amp;quot;系统长什么样&amp;quot;，却未必有体感——&lt;strong&gt;我的数据，到底是怎么变成我刷到的这一屏的？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这篇换个讲法：造一个用户，给他一晚上的真实行为，跟着他走完全程。线索有三条，正好串起三十年：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;吴军 2006 年起在 Google 黑板报连载、2012 年成书的《数学之美》——推荐算法的数学原点；&lt;/li&gt;
&lt;li&gt;Netflix 2013 年公开的离线/近线/在线三层实践——工程化的起点（上一篇的主线）；&lt;/li&gt;
&lt;li&gt;TikTok 2020 年官方披露的 For You 推荐信号，和字节跳动 2022 年发表的 Monolith 在线训练系统——这套东西被推到的工程极致。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;数学的原点数学之美的两章&#34;&gt;数学的原点：《数学之美》的两章&lt;/h2&gt;
&lt;p&gt;《数学之美》第 14 章&amp;quot;余弦定理和新闻的分类&amp;quot;讲了一个漂亮的化归：给新闻里每个实词算出 TF-IDF 值，按词汇表位置排成一个向量，一篇新闻就变成了一个&lt;strong&gt;特征向量&lt;/strong&gt;；&amp;ldquo;新闻分类&amp;quot;这个看似需要理解语义的问题，变成了&lt;strong&gt;在向量空间里算余弦相似度&lt;/strong&gt;的几何问题。第 15 章&amp;quot;矩阵运算和文本处理中的两个分类问题&amp;quot;接着讲：成千上万篇新闻构成的大矩阵，可以用 SVD（奇异值分解）一次拆开，分类在分解后的小矩阵上完成。&lt;/p&gt;
&lt;p&gt;推荐系统是&lt;strong&gt;同一套数学，换了一张矩阵&lt;/strong&gt;：把&amp;quot;新闻 × 词汇&amp;quot;换成&amp;quot;用户 × 内容&amp;rdquo;。余弦相似度回答的问题，从&amp;quot;两篇新闻像不像&amp;quot;变成&amp;quot;这个用户的口味和这条内容像不像&amp;quot;；SVD 分解的那张大矩阵，就是后来协同过滤要分解的用户-物品交互矩阵。&lt;/p&gt;
&lt;p&gt;别急着说这是类比——直接手算一遍。假设全站内容只有四个类目：宠物、科技、健身、汽车。用户的口味表示成一个 4 维向量，每个分量是他在该类目上累计的行为强度。&lt;/p&gt;
&lt;h2 id=&#34;造一个用户u10086-的一晚&#34;&gt;造一个用户：U10086 的一晚&lt;/h2&gt;
&lt;p&gt;主角 U10086，昨晚刷了一小时短视频，留下这些行为：&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;22:10&lt;/td&gt;
					&lt;td&gt;完播&lt;/td&gt;
					&lt;td&gt;A123「橘猫拆家」&lt;/td&gt;
					&lt;td&gt;宠物&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;22:13&lt;/td&gt;
					&lt;td&gt;点赞&lt;/td&gt;
					&lt;td&gt;B456「居家哑铃训练」&lt;/td&gt;
					&lt;td&gt;健身&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;22:15&lt;/td&gt;
					&lt;td&gt;1.2 秒划走&lt;/td&gt;
					&lt;td&gt;C789「新能源车测评」&lt;/td&gt;
					&lt;td&gt;汽车&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;22:18&lt;/td&gt;
					&lt;td&gt;停留 8 秒&lt;/td&gt;
					&lt;td&gt;D012「AI 编程工具实测」&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;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;内容的标签不是作者标的是算出来的&#34;&gt;内容的标签不是作者标的，是算出来的&lt;/h3&gt;
&lt;p&gt;一个常见误解：视频的类目来自作者上传时勾选的分区。在 TikTok 这个量级的平台上不是这样。作者自选标签噪声大，还挡不住&amp;quot;标签作弊&amp;quot;——挂个热门标签蹭流量。真实做法是&lt;strong&gt;内容理解管道&lt;/strong&gt;：视频上传后先过一遍机器理解——抽帧做图像分类和目标检测、ASR 把语音转成文字、OCR 读画面上的字、NLP 分析标题和文案——最后由模型产出类目、标签和 embedding 向量。作者的 caption 和 hashtag 只是输入信号之一，没有最终决定权。TikTok 官方披露的三类信号里，&amp;ldquo;内容信息&amp;rdquo;（captions、sounds、hashtags）就是这条管道的产物：内容进流量池之前，&amp;ldquo;这条视频是关于什么的&amp;quot;已由机器定好。&lt;/p&gt;
&lt;p&gt;甚至播放之后还在算。前面提到的 item2vec，训练数据就是全站用户的观看行为——被同一批人看完的内容，向量天然相近。也就是说，&lt;strong&gt;内容的向量随每一次播放被持续校准&lt;/strong&gt;，而不是发布那一刻就定格。&lt;/p&gt;
&lt;h3 id=&#34;用户向量是内容向量的累计&#34;&gt;用户向量是内容向量的累计&lt;/h3&gt;
&lt;p&gt;所以系统手里 U10086 的口味画像——宠物 0.55、科技 0.25、健身 0.15、汽车 0.05，即向量 $u = (0.55, 0.25, 0.15, 0.05)$——不是他填的问卷，也不是对人工类目的简单计数，而是&lt;strong&gt;他过去几个月每次行为对应的机器内容向量，按行为强度加权累计出来的&lt;/strong&gt;。两边都是算出来的：内容侧机器识别，用户侧行为聚合。&lt;/p&gt;
&lt;p&gt;这个设计值两个好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;准&lt;/strong&gt;：作者标错、乱标的类目污染不了画像，用户向量锚定在内容的真实特征上——他看完的是一只真猫，不是一个挂着&amp;quot;萌宠&amp;quot;标签的广告；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;细&lt;/strong&gt;：4 个类目只是教学简化，底下是几百维的 embedding，&amp;ldquo;橘猫&amp;quot;和&amp;quot;布偶猫&amp;quot;这种类目装不下的区分，向量装得下。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;他今晚每一次播放、点赞，都会乘上那条内容的机器向量、计入自己的向量——这正是后面&amp;quot;短期兴趣向量&amp;quot;一节的微观机制。&lt;/p&gt;
&lt;p&gt;今晚候选池里有三条内容（向量都出自内容理解管道）：猫视频 $v_1 = (0.9, 0.1, 0, 0)$，哑铃教学 $v_2 = (0.05, 0.1, 0.8, 0.05)$，汽车测评 $v_3 = (0, 0.05, 0, 0.95)$。&lt;/p&gt;
&lt;p&gt;余弦相似度的公式就一行：&lt;/p&gt;
&lt;p&gt;$$\cos(u, v) = \frac{u \cdot v}{|u||v|}$$&lt;/p&gt;
&lt;p&gt;给猫视频算一次：分子是点积 $0.55 \times 0.9 + 0.25 \times 0.1 = 0.52$；分母是两个向量的模长之积 $\sqrt{0.39} \times \sqrt{0.82} \approx 0.565$。结果是 $0.52 / 0.565 \approx 0.92$。三条候选全算完：&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;猫视频 $v_1$&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;0.92&lt;/strong&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;哑铃教学 $v_2$&lt;/td&gt;
					&lt;td&gt;0.35&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;汽车测评 $v_3$&lt;/td&gt;
					&lt;td&gt;0.10&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;**这就是&amp;quot;推荐&amp;quot;在最原始形态下的全部：一次点积，一次归一化。**今天 TikTok 的排序模型参数量以百亿计，打分函数的内核仍然是这个动作——只是向量从 4 维人工统计变成几百维神经网络学出来的 embedding（item2vec，2016；双塔模型），从&amp;quot;类目&amp;quot;粒度细化到&amp;quot;每条内容&amp;quot;粒度。三十年里数学没有换，换的是向量的生产方式和计算规模。&lt;/p&gt;
&lt;h2 id=&#34;行为怎么变成画像netflix-的三层分工&#34;&gt;行为怎么变成画像：Netflix 的三层分工&lt;/h2&gt;
&lt;p&gt;4 维向量是教学模型，真实系统里的用户向量没法靠人工统计类目得到——内容有几亿条，行为有几十种。工程上的解法就是上一篇展开讲过的 Netflix 三层分工，以及它背后的一整套大数据栈：用户的每一次曝光、点击、停留，客户端上报后先进 &lt;strong&gt;Kafka&lt;/strong&gt;（事件总线）；一份实时流进 &lt;strong&gt;Flink&lt;/strong&gt; 作业，秒级滚动更新短期画像；同时全量落盘到 &lt;strong&gt;HDFS&lt;/strong&gt;（Hadoop 生态的分布式存储），由 &lt;strong&gt;Hive&lt;/strong&gt;（或 Iceberg）建成离线数仓。&lt;strong&gt;离线层&lt;/strong&gt;的 Spark 批任务（天级）从数仓读数据，算物品向量、itemCF 共现矩阵、用户长期兴趣向量；&lt;strong&gt;在线层&lt;/strong&gt;只做查表和轻量推理，画像和榜单都躺在 Redis 里。重活在用户到来之前全部干完。&lt;/p&gt;
&lt;p&gt;问题是：一次&amp;quot;完播&amp;quot;和一次&amp;quot;秒划走&amp;rdquo;，该给短期画像各贡献多少？这个强弱排序不是工程师拍脑袋——TikTok 官方给过答案。&lt;/p&gt;
&lt;p&gt;2020 年 6 月 18 日，TikTok 在官方 newsroom 发表 &amp;ldquo;How TikTok recommends videos #ForYou&amp;rdquo;，是平台方罕见的一次坦白。它把推荐信号分成三类：&lt;/p&gt;
&lt;ul&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;：语言、国家、设备类型——官方明说这类权重更低，因为&amp;quot;用户没有主动把这些表达为偏好&amp;rdquo;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;更重要的是官方对&lt;strong&gt;信号强度&lt;/strong&gt;的排序：原话是&amp;quot;用户是否把一个较长的视频从头看到尾&amp;quot;这类强兴趣指标，权重远大于&amp;quot;观众和创作者是否在同一国家&amp;quot;这类弱指标。翻译成 U10086 的表格：他完播猫视频这件事的权重，压过他的手机和品牌方在同一城市。&lt;/p&gt;
&lt;p&gt;这套强弱排序落到工程上，就是近线层滚动更新的&lt;strong&gt;短期兴趣向量&lt;/strong&gt;——最近 N 次行为对应的物品向量做加权平均：&lt;/p&gt;
&lt;p&gt;$$u_{short} = \frac{\sum_{i} w_i \cdot e^{-\Delta t_i / \tau} \cdot v_i}{\sum_{i} w_i \cdot e^{-\Delta t_i / \tau}}$$&lt;/p&gt;
&lt;p&gt;权重 $w_i$ 沿行为强度排：完播 3.0、点赞 2.0、停留 5 秒以上 1.5、点击 1.0、秒划走 -0.5（负反馈反向拉）；指数项是时间衰减，越久的行为影响越小。代入 U10086 的一晚：猫视频向量乘 3.0 主导方向，汽车视频乘 -0.5 往反方向拽。&lt;strong&gt;他昨晚的每一个动作，在他下次下拉之前，就已经被压缩成这个向量躺在 Redis 里了。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;把公式拆开每个符号是什么&#34;&gt;把公式拆开：每个符号是什么&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$i$：遍历用户最近 N 次行为的下标。N 常见取 50-200，只保留最近一小段——太久之前的行为对&amp;quot;今晚想看什么&amp;quot;没有预测力；&lt;/li&gt;
&lt;li&gt;$v_i$：第 i 次行为涉及的&lt;strong&gt;内容向量&lt;/strong&gt;——就是上一节内容理解管道算出来的那条视频的向量。注意它是物品的向量，不是用户的；用户向量正是由一堆物品向量合成的；&lt;/li&gt;
&lt;li&gt;$w_i$：这次行为的强度权重（完播 3.0、点赞 2.0、停留 1.5、点击 1.0、秒划 -0.5），回答&amp;quot;这个动作多能代表他的兴趣&amp;quot;；&lt;/li&gt;
&lt;li&gt;$\Delta t_i$：这次行为距今的时间差；&lt;/li&gt;
&lt;li&gt;$\tau$：时间衰减常数，控制&amp;quot;多久之前的行为算老&amp;quot;。$\tau$ 取 12 小时，则 12 小时前的行为权重衰减到 $e^{-1} \approx 0.37$，一天前只剩 0.14；&lt;/li&gt;
&lt;li&gt;$e^{-\Delta t_i / \tau}$：指数时间衰减因子，越久的行为对当前兴趣越没有发言权；&lt;/li&gt;
&lt;li&gt;分母：归一化项。没有它，刷得多的人向量模长就大，&amp;ldquo;勤奋&amp;quot;会冒充&amp;quot;兴趣浓&amp;rdquo;；除一下只留方向，不带数量。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话：&lt;strong&gt;短期兴趣 = 最近看过的内容的向量，按&amp;quot;行为多重 × 时间多近&amp;quot;加权平均。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;演算u10086-的短期向量&#34;&gt;演算：U10086 的短期向量&lt;/h3&gt;
&lt;p&gt;把内容向量代入他昨晚的四次行为。$\tau$ 取 0.5 小时，站在 22:30 这一刻计算（D012 的内容向量是 (0.05, 0.85, 0.05, 0.05)，科技向）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;行为&lt;/th&gt;
					&lt;th&gt;内容向量 $v_i$&lt;/th&gt;
					&lt;th&gt;强度 $w_i$&lt;/th&gt;
					&lt;th&gt;距今 $\Delta t_i$&lt;/th&gt;
					&lt;th&gt;衰减 $e^{-\Delta t_i / \tau}$&lt;/th&gt;
					&lt;th&gt;有效权重 $w_i \cdot e^{-\Delta t_i / \tau}$&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;完播猫视频&lt;/td&gt;
					&lt;td&gt;(0.9, 0.1, 0, 0)&lt;/td&gt;
					&lt;td&gt;3.0&lt;/td&gt;
					&lt;td&gt;20 分钟&lt;/td&gt;
					&lt;td&gt;0.51&lt;/td&gt;
					&lt;td&gt;1.54&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;点赞哑铃&lt;/td&gt;
					&lt;td&gt;(0.05, 0.1, 0.8, 0.05)&lt;/td&gt;
					&lt;td&gt;2.0&lt;/td&gt;
					&lt;td&gt;17 分钟&lt;/td&gt;
					&lt;td&gt;0.57&lt;/td&gt;
					&lt;td&gt;1.13&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;秒划汽车&lt;/td&gt;
					&lt;td&gt;(0, 0.05, 0, 0.95)&lt;/td&gt;
					&lt;td&gt;-0.5&lt;/td&gt;
					&lt;td&gt;15 分钟&lt;/td&gt;
					&lt;td&gt;0.61&lt;/td&gt;
					&lt;td&gt;-0.30&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;停留 AI 工具&lt;/td&gt;
					&lt;td&gt;(0.05, 0.85, 0.05, 0.05)&lt;/td&gt;
					&lt;td&gt;1.5&lt;/td&gt;
					&lt;td&gt;12 分钟&lt;/td&gt;
					&lt;td&gt;0.67&lt;/td&gt;
					&lt;td&gt;1.01&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;分子是四个&amp;quot;有效权重 × 内容向量&amp;quot;之和，这里的算术只有两步：标量乘向量、向量相加。全部展开（保留三位小数）：&lt;/p&gt;
&lt;p&gt;第一步，&lt;strong&gt;标量乘向量&lt;/strong&gt;——每条内容向量逐分量乘上自己的有效权重：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;          有效权重 × 内容向量                =  加权后的向量
猫视频     1.54 × (0.9,  0.1,  0,    0)     = ( 1.386,  0.154,  0,      0)
哑铃       1.13 × (0.05, 0.1,  0.8,  0.05)  = ( 0.057,  0.114,  0.904,  0.057)
汽车      -0.30 × (0,    0.05, 0,    0.95)  = ( 0,     -0.015,  0,     -0.285)
AI工具     1.01 × (0.05, 0.85, 0.05, 0.05)  = ( 0.050,  0.854,  0.050,  0.050)&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;第二步，&lt;strong&gt;逐分量相加&lt;/strong&gt;——四个向量的同一列相加，每一列就是一个类目方向：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;宠物分量： 1.386 + 0.057 + 0      + 0.050 =  1.49
科技分量： 0.154 + 0.114 - 0.015  + 0.854 =  1.11
健身分量： 0     + 0.904 + 0      + 0.050 =  0.96
汽车分量： 0     + 0.057 - 0.285  + 0.050 = -0.18&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;分子向量就是 (1.49, 1.11, 0.96, -0.18)；分母是有效权重之和 1.54 + 1.13 - 0.30 + 1.01 = 3.38。最后逐分量相除：1.49 / 3.38 ≈ 0.44，1.11 / 3.38 ≈ 0.33，0.96 / 3.38 ≈ 0.28，-0.18 / 3.38 ≈ -0.05。得到：&lt;/p&gt;
&lt;p&gt;$$u_{short} \approx (0.44, 0.33, 0.28, -0.05)$$&lt;/p&gt;
&lt;p&gt;从第二步的列里能直接读出故事：宠物分量的九成来自那条完播的猫视频（1.49 里它独占 1.386）；汽车这一列，哑铃和 AI 工具带来的两个正贡献（0.057 + 0.050）加起来也抵不过一次秒划的 -0.285——&lt;strong&gt;负权重的意义就在这里：他不喜欢的内容在向量里留下的不是零，而是负坐标&lt;/strong&gt;，检索时会把相似内容往反方向推。&lt;/p&gt;
&lt;p&gt;对比他的长期画像 $(0.55, 0.25, 0.15, 0.05)$：一小时的行为已经把&lt;strong&gt;科技顶到第二位、健身翻了倍、汽车打成负值&lt;/strong&gt;。在线服务一般把长短期两个向量融合（拼接或加权和）作为最终用户向量——长期画像管&amp;quot;他是个什么人&amp;quot;，短期向量管&amp;quot;他今晚想看什么&amp;quot;。下一刷召回时，猫和科技内容能同时被捞回来，靠的就是这个融合向量。&lt;/p&gt;
&lt;h2 id=&#34;tiktok-把闭环推到极致&#34;&gt;TikTok 把闭环推到极致&lt;/h2&gt;
&lt;h3 id=&#34;官方确认的和官方从没确认的&#34;&gt;官方确认的，和官方从没确认的&lt;/h3&gt;
&lt;p&gt;除了信号分类，TikTok 那篇文章还有一句关键坦白：&lt;strong&gt;粉丝数和账号历史爆款都不是直接推荐因素&lt;/strong&gt;（原话：neither follower count nor previous high-performing videos are direct factors）。这解释了新账号为什么也能出爆款——内容进场不看出身。&lt;/p&gt;
&lt;p&gt;中文互联网流传的&amp;quot;流量池&amp;quot;机制要在这里说清边界：新视频先给 200-500 次曝光的小池子，完播率、互动率跑赢同期内容就推进千级、万级的更大池子，逐级叠加放量——&lt;strong&gt;这套具体说法出自运营圈的逆向总结，TikTok 和抖音官方从未确认过细节&lt;/strong&gt;。但它的方向和官方披露自洽：既然粉丝数不算直接因素，新内容就必须有一个&amp;quot;不看出身、先看小范围真实反馈&amp;quot;的冷启动机制，流量池只是这个必然性的具体形态。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;新视频发布&amp;lt;br/&amp;gt;初始小流量池 数百次曝光&amp;#34;] --&amp;gt; B{&amp;#34;完播率 互动率&amp;lt;br/&amp;gt;跑赢同期内容?&amp;#34;}
    B -- &amp;#34;达标&amp;#34; --&amp;gt; C[&amp;#34;二级流量池&amp;lt;br/&amp;gt;千级曝光&amp;#34;]
    B -- &amp;#34;不达标&amp;#34; --&amp;gt; D[&amp;#34;停止放量&amp;lt;br/&amp;gt;转入自然长尾&amp;#34;]
    C --&amp;gt; E{&amp;#34;数据继续跑赢?&amp;#34;}
    E -- &amp;#34;达标&amp;#34; --&amp;gt; F[&amp;#34;更大流量池&amp;lt;br/&amp;gt;万级到百万级&amp;#34;]
    E -- &amp;#34;不达标&amp;#34; --&amp;gt; D&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;（图为第三方运营经验的通行描述，非官方口径。）&lt;/p&gt;
&lt;h3 id=&#34;monolith连模型本身都在线学习&#34;&gt;Monolith：连模型本身都在线学习&lt;/h3&gt;
&lt;p&gt;Netflix 的三层架构里，模型参数是离线训练的，近线只更新特征和画像。字节跳动 2022 年发表的 Monolith（RecSys &amp;lsquo;22，arXiv:2209.07663）把这条边界也打掉了：&lt;strong&gt;推荐模型在线训练&lt;/strong&gt;——行为事件实时变成训练样本回流，模型参数近实时更新。&lt;/p&gt;
&lt;p&gt;工程难点在 embedding 表。在线场景下新内容源源不断涌进来，传统哈希表会发生碰撞：两条不同的内容被映射到同一份 embedding，互相污染。Monolith 用 cuckoo hashing 实现无碰撞的 embedding 表，再配合特征过期和低频过滤控制内存，硬是把&amp;quot;在线深度学习&amp;quot;做成了生产级系统。&lt;/p&gt;
&lt;p&gt;对比一下闭环速度：Netflix 2013 年的近线层，&amp;ldquo;行为 → 画像生效&amp;quot;做到秒级已经是当时的极致；Monolith 把&amp;quot;行为 → 模型参数更新&amp;quot;也压到了近实时。&lt;strong&gt;闭环速度，就是推荐系统的军备竞赛指标。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;跟着-u10086-走一遍四层漏斗&#34;&gt;跟着 U10086 走一遍四层漏斗&lt;/h2&gt;
&lt;p&gt;铺垫完了，现在让 U10086 在 23:05 下拉刷新，跟他的数据走完全程。四层漏斗的原理上一篇讲过，这篇只看一件事：&lt;strong&gt;每一步的数据长什么样。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;召回1-亿--8000&#34;&gt;召回：1 亿 → 8000&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;输入&lt;/strong&gt;：短期兴趣向量 $u_{short}$、最近行为物品列表 [A123, B456, D012]、长期画像。&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;itemCF 协同过滤&lt;/td&gt;
					&lt;td&gt;Redis 批量查 A123、B456、D012 各自的相似物品 Top50（相似度矩阵是 Spark 离线算好的共现余弦）&lt;/td&gt;
					&lt;td&gt;约 3000 条，大量猫、健身、科技内容&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;向量召回&lt;/td&gt;
					&lt;td&gt;$u_{short}$ 在 Faiss（HNSW 索引）里做近似最近邻检索，几毫秒返回&lt;/td&gt;
					&lt;td&gt;3000 条&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;热门榜&lt;/td&gt;
					&lt;td&gt;直接读 Redis 里 Flink 滚动窗口算好的全站热榜&lt;/td&gt;
					&lt;td&gt;1000 条&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;标签/关键词召回&lt;/td&gt;
					&lt;td&gt;内容元数据（标题、标签、类目）灌在 Elasticsearch 的倒排索引里，拿画像里的高权重标签当查询条件去检索——「搜推一体」里 ES 干的就是这路&lt;/td&gt;
					&lt;td&gt;约 500 条&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;&lt;strong&gt;输出&lt;/strong&gt;：多路合并、去重、按路配额截断——&lt;strong&gt;8000 条候选 ID 的列表&lt;/strong&gt;，每条只带召回来源和粗分。注意一个细节：没有一路会拿 C789（秒划走的汽车视频）当种子去查相似内容，汽车类在召回阶段就已经天然稀疏了。&lt;/p&gt;
&lt;h3 id=&#34;粗排8000--1000&#34;&gt;粗排：8000 → 1000&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;输入&lt;/strong&gt;：8000 条候选 + 一批&amp;quot;便宜特征&amp;rdquo;——从特征存储（Feast 或自研 Feature Store，底层通常是 Redis）批量读出物品热度分、质量分、该用户的类目偏好分、历史 CTR。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;计算&lt;/strong&gt;：轻量模型打分（简化双塔就是一次向量点积，GBDT 就是几千次小树遍历——XGBoost / LightGBM），单条成本压到微秒级。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输出&lt;/strong&gt;：每条一个粗排分，截断 Top 1000：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;cat_video_8891   (A123 的相似内容)   0.81   ← 和短期向量点积极高
ai_tool_3302     (D012 的相似内容)   0.74
hot_funny_1102   (热门榜兜底)        0.62
car_review_5567                      0.05   ← 秒划负反馈直接压死&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;精排1000--200&#34;&gt;精排：1000 → 200&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;输入&lt;/strong&gt;：1000 条候选 + 全量特征拼接——用户画像（Redis KV 读出）、物品特征、近线实时特征（最近一小时的点击序列，Flink 产出）、实时上下文（23:05、手机、WiFi）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;计算&lt;/strong&gt;：深度排序模型（DIN 系，阿里 2018 年 KDD 发表）在推理服务（TF Serving 或 NVIDIA Triton，GPU 批量矩阵乘）上跑推理。DIN 的注意力机制值得演算一步：给候选 cat_video_8891 打分时，模型给 U10086 的历史行为分配权重——同为猫视频的 A123 权重拉到最高，B456 次之，C789 趋近于零，输出高 pCTR；而给 car_review_5567 打分时，被注意机制激活的恰恰是 C789——方向是负的。&lt;strong&gt;同一个用户的历史，在不同候选面前讲出不同的故事&lt;/strong&gt;，这是 DIN 相比&amp;quot;死板的平均历史向量&amp;quot;的本质进步。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;输出&lt;/strong&gt;：每条候选一对预测值 (pCTR, 预估观看时长），加权成综合分后排序，截断 Top 200。&lt;/p&gt;
&lt;h3 id=&#34;重排200--20&#34;&gt;重排：200 → 20&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;输入&lt;/strong&gt;：200 条带精排分的列表。这一层几乎不算模型，全是规则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;已曝光过滤&lt;/strong&gt;：查 Redis 里 &lt;code&gt;user:10086:seen&lt;/code&gt; 集合（或布隆过滤器），剔除本次 session 已刷过的 15 条；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负反馈硬屏蔽&lt;/strong&gt;：汽车类目整体降权——秒划走加历史&amp;quot;不感兴趣&amp;quot;记录；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多样性打散&lt;/strong&gt;：同作者最多 2 条、相邻 3 条不同类目——Top 200 里连着的 8 条猫视频被打散重插；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混排&lt;/strong&gt;：第 4 位插入广告（广告单独按 eCPM = 出价 × 预估点击率 排序选出），第 9 位插一条关注博主的更新；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;业务护栏&lt;/strong&gt;：过滤审核中内容，新内容扶持配额 5%——这就是流量池机制落在自然流量里的那一小块。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;输出&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;items&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;...20 条：16 条自然内容 + 2 条广告 + 1 条关注更新 + 1 条新内容扶持...&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;&amp;#34;next_cursor&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;eyJscCI6MC44MSwiYmF0Y2giOiJUYjI5...&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;游标里封着最后一条的排序分、本次批次号、过期时间，客户端不许解析。同时这 20 条写入已曝光集合，曝光和后续行为事件经 Kafka 回流——&lt;strong&gt;闭环合上，U10086 的下一刷已经在被这一刷改写。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;沿途的三方件总表&#34;&gt;沿途的三方件总表&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;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;Kafka&lt;/td&gt;
					&lt;td&gt;行为事件解耦、削峰、可回放&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据底座&lt;/td&gt;
					&lt;td&gt;Hadoop（HDFS）+ Hive / Iceberg&lt;/td&gt;
					&lt;td&gt;全量行为日志落盘，离线数仓&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;离线计算&lt;/td&gt;
					&lt;td&gt;Spark&lt;/td&gt;
					&lt;td&gt;模型训练、itemCF 共现矩阵、物品向量&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;近线计算&lt;/td&gt;
					&lt;td&gt;Flink&lt;/td&gt;
					&lt;td&gt;秒级更新短期画像、滚动热榜&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;在线训练&lt;/td&gt;
					&lt;td&gt;Monolith（字节自研，论文开源）&lt;/td&gt;
					&lt;td&gt;模型参数近实时更新&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;在线 KV&lt;/td&gt;
					&lt;td&gt;Redis（或 HBase）&lt;/td&gt;
					&lt;td&gt;画像、榜单、已曝光集合、itemCF 结果&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;向量检索&lt;/td&gt;
					&lt;td&gt;Faiss / HNSWlib / Milvus&lt;/td&gt;
					&lt;td&gt;双塔向量的近似最近邻召回&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;倒排索引&lt;/td&gt;
					&lt;td&gt;Elasticsearch&lt;/td&gt;
					&lt;td&gt;标签/关键词召回路，「搜推一体」的内容检索&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;特征平台&lt;/td&gt;
					&lt;td&gt;Feast 或自研 Feature Store&lt;/td&gt;
					&lt;td&gt;离线、在线特征一致性&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;模型推理&lt;/td&gt;
					&lt;td&gt;TF Serving / NVIDIA Triton&lt;/td&gt;
					&lt;td&gt;精排批量打分&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;轻量模型&lt;/td&gt;
					&lt;td&gt;XGBoost / LightGBM&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;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;这套链路怎么扛住高并发&#34;&gt;这套链路怎么扛住高并发&lt;/h2&gt;
&lt;p&gt;漏斗走完了，但还有一个工程问题没回答：几十毫秒的延迟预算，乘以几十万的并发请求，靠什么扛？先把账算出来。假设 1 亿日活、人均 50 次下拉：平均约 6 万次请求/秒，晚高峰按 3-5 倍算是 20-30 万 QPS。每次下拉背后是几十上百次 KV 读取、一次向量检索、一次千级候选的模型推理。能扛住靠的不是某台快机器，是一组选型决策：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redis：O(1) 查表 + 批量读。&lt;/strong&gt; 单实例简单 GET 是十万级 QPS、亚毫秒延迟；一次召回要查几十上百个 key（itemCF 种子物品、画像、热榜），用 &lt;code&gt;mget&lt;/code&gt; 合并成一次网络往返，QPS 压力直接降一个量级；集群模式水平分片，容量不够就加分片。已曝光集合的布隆过滤器也放在这里。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;向量检索：这是召回里唯一真正&amp;quot;算&amp;quot;的地方，也是选型的重点。&lt;/strong&gt; 几个主流方案的性能画像（百万到十亿级向量、TopK 检索）：&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;Faiss（Meta）&lt;/td&gt;
					&lt;td&gt;嵌入服务进程内的库&lt;/td&gt;
					&lt;td&gt;没有网络和序列化开销；IVF+PQ 量化后单机可扛亿级向量，检索毫秒级&lt;/td&gt;
					&lt;td&gt;召回服务内嵌，延迟敏感场景的主力&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;HNSWlib&lt;/td&gt;
					&lt;td&gt;嵌入库&lt;/td&gt;
					&lt;td&gt;图索引召回率高（95% 以上），代价是全量向量驻内存，内存开销大&lt;/td&gt;
					&lt;td&gt;中等规模、高精度&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;ScaNN（Google）&lt;/td&gt;
					&lt;td&gt;嵌入库&lt;/td&gt;
					&lt;td&gt;各向异性量化，同等召回率下 QPS 领先&lt;/td&gt;
					&lt;td&gt;内嵌、高 QPS&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Milvus&lt;/td&gt;
					&lt;td&gt;独立分布式服务&lt;/td&gt;
					&lt;td&gt;存算分离、支持 GPU 索引，十亿级横向扩展&lt;/td&gt;
					&lt;td&gt;多业务共享的向量中台&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Elasticsearch kNN&lt;/td&gt;
					&lt;td&gt;搜索引擎插件&lt;/td&gt;
					&lt;td&gt;复用现有 ES 集群，开箱即用，但 QPS 和召回率都不及专用索引&lt;/td&gt;
					&lt;td&gt;小规模、搜推一体&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;工程上的关键判断：&lt;strong&gt;召回的向量检索优先用内嵌库（Faiss / HNSWlib），省掉一次 RPC&lt;/strong&gt;——30 万 QPS 面前，每次网络往返都是命。Milvus 这类独立服务的价值在多团队共享、在线扩缩容和统一的向量管理，不在单点延迟。内容池上到十亿级，靠 PQ 量化把每条向量从几百字节浮点压成几十字节编码，再加分片。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kafka：百万级消息/秒是常态负载。&lt;/strong&gt; 曝光点击的洪峰对它来说不算事；真正要设计的是分区数（决定下游消费者的并行上限）和消费滞后监控——滞后就是&amp;quot;画像生效延迟&amp;quot;，直接等于用户体感。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flink：状态在本地的流计算。&lt;/strong&gt; 短期画像的滚动更新是 keyed state，状态读写走本地 RocksDB，不回远程存储——这是近线层敢吞全量事件流的原因。反过来说，一旦作业挂掉从 checkpoint 恢复，RocksDB 状态的加载时间就是画像的&amp;quot;失明时间&amp;quot;，所以 checkpoint 间隔要按业务能容忍的失明时长来定。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GPU 推理：批量摊薄。&lt;/strong&gt; 精排把 1000 条候选拼成一个大 batch 一次过 GPU，摊到每条只有几微秒；Triton 的动态批处理（dynamic batching）会在毫秒级窗口里把多个请求的 batch 攒起来再推理，用一点点延迟换几倍吞吐——这是&amp;quot;几十毫秒预算&amp;quot;能成立的关键手法之一。&lt;/p&gt;
&lt;p&gt;共同的模式只有一条：&lt;strong&gt;所有重活提前算完存起来，在线路径上只剩内存级查询和批量推理&lt;/strong&gt;——30 万 QPS 里，没有一个请求需要&amp;quot;现场思考&amp;quot;。&lt;/p&gt;
&lt;h2 id=&#34;收尾数学没变闭环变快&#34;&gt;收尾：数学没变，闭环变快&lt;/h2&gt;
&lt;p&gt;走完全程回头看，三十年里真正变化的不是数学：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;2006 年&lt;/strong&gt;，吴军讲余弦定理给新闻分类——打分内核从那时起就是向量点积，今天依然如此；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2013 年&lt;/strong&gt;，Netflix 定下三层分工——把计算摊到用户到来之前，至今是主流形态；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2020 年&lt;/strong&gt;，TikTok 官方坦白完播是强信号、粉丝数不是直接因素——行为强度加权第一次有了平台方背书；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;2022 年&lt;/strong&gt;，Monolith 把模型训练本身搬进在线链路——闭环的最后一段延迟也被压缩。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;推荐系统真正的军备竞赛，从来不是&amp;quot;谁的公式更巧妙&amp;quot;，而是&lt;strong&gt;谁的闭环转得更快&lt;/strong&gt;：从用户划走一条汽车视频，到系统少推汽车内容，中间隔了几秒还是几天。U10086 今晚的每一次停留和划走都在投票，明早打开 App 时看到的那个&amp;quot;更懂他&amp;quot;的首页，没有一次是当场算出来的——全是昨晚就已开始的计算，刚好在他到来之前完成。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;相关阅读：&lt;a href=&#34;https://ftmao.com/blog/blog/posts/personalized-infinite-feed-architecture/&#34;&gt;《千人千面 + 无限瀑布流：推荐 Feed 的完整实现架构》&lt;/a&gt;——本文的架构视角姊妹篇，讲四层漏斗的原理和无限下拉的分页协议。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考：吴军《数学之美》（第三版，人民邮电出版社）第 14、15 章；TikTok Newsroom, &amp;ldquo;How TikTok recommends videos #ForYou&amp;rdquo;, 2020-06-18（newsroom.tiktok.com/en-us/how-tiktok-recommends-videos-for-you）；Liu et al., &amp;ldquo;Monolith: Real Time Recommendation System With Collisionless Embedding Table&amp;rdquo;, RecSys 2022, arXiv:2209.07663；&amp;ldquo;流量池&amp;quot;分级机制为运营圈第三方经验总结，非官方披露口径。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>C 端 IPTV 产品的 Java 高并发架构设计：高可用、高性能、零信任与缓存一致性</title>
      <link>https://ftmao.com/blog/posts/iptv-java-high-concurrency-architecture/</link>
      <pubDate>Mon, 19 Oct 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/iptv-java-high-concurrency-architecture/</guid>
      <description>&lt;h1 id=&#34;c-端-iptv-产品的-java-高并发架构设计高可用高性能零信任与缓存一致性&#34;&gt;C 端 IPTV 产品的 Java 高并发架构设计：高可用、高性能、零信任与缓存一致性&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;过去几年我一直在做一个面向海外的 C 端 IPTV 产品（STB 机顶盒 + 移动端，多品牌多租户，跑在 AWS 上）。这篇文章是我对这套系统架构的复盘：高并发、高可用、高性能、高安全、缓存一致性这几件事我们分别是怎么做的、踩过什么坑。过程中我们落地了不少 Netflix 的开源组件和公开实践（Hystrix、EVCache、Zuul 的网关思想、混沌工程等），但每一个决策都是经过我们自己生产环境验证或修正的，不是照搬。文中方案细节来自真实的系统设计文档（Video Cloud 目标模型、vod-auth、CloudRouter、DRM、网盘整改等），脱敏后分享。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一先搞清楚对手c-端-iptv-的流量模型&#34;&gt;一、先搞清楚对手：C 端 IPTV 的流量模型&lt;/h2&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;晚 8 点 - 11 点是高峰，大促、球赛、爆款剧上线时流量成倍脉冲&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;EPG 节目单、Meta 元数据、Layout 下发占了 9 成以上流量&lt;/td&gt;
					&lt;td&gt;缓存优先，读路径尽量不打 DB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;播放鉴权是超级热点&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每次起播、切台都要鉴权取播放链接，QPS 随在线用户数线性涨&lt;/td&gt;
					&lt;td&gt;热点接口单独优化（缓存 + 短 TTL + 滑动续期）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;设备在线心跳&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;app 轮询 session，百万级设备定时上报&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;分库隔离爆炸半径，account_id 前缀区分品牌&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这个流量形状——读多写少、晚间高峰、播放路径是生命线——决定了后面所有的架构取舍。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二总体架构一张图看清全貌&#34;&gt;二、总体架构：一张图看清全貌&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph 终端
        STB[&amp;#34;STB 机顶盒&amp;#34;]
        APP[&amp;#34;移动 App&amp;#34;]
        WEB[&amp;#34;Web / Portal&amp;#34;]
    end

    subgraph 接入层
        ELB[&amp;#34;AWS ELB&amp;#34;]
        APISIX[&amp;#34;APISIX 网关&amp;lt;br/&amp;gt;限流 Sentinel / WAF / 鉴权前置&amp;#34;]
    end

    subgraph 微服务层
        AUTH[&amp;#34;vod-auth&amp;lt;br/&amp;gt;登录注册 / 账号 / 子账号&amp;#34;]
        SESS[&amp;#34;common-session&amp;lt;br/&amp;gt;会话管理&amp;#34;]
        META[&amp;#34;vod-meta-api&amp;lt;br/&amp;gt;元数据 / Layout / EPG&amp;#34;]
        PROF[&amp;#34;profile&amp;lt;br/&amp;gt;观看历史 / 收藏 / 偏好&amp;#34;]
        CLOUD[&amp;#34;vod-cloud&amp;lt;br/&amp;gt;个人网盘&amp;#34;]
        ROUTER[&amp;#34;CloudRouter&amp;lt;br/&amp;gt;播放链接路由&amp;#34;]
        DRM[&amp;#34;drmlicense&amp;lt;br/&amp;gt;DRM 鉴权与 license 下发&amp;#34;]
    end

    subgraph 数据层
        MYSQL[&amp;#34;MySQL RDS&amp;lt;br/&amp;gt;按品牌分库&amp;#34;]
        REDIS[&amp;#34;Redis Cluster&amp;lt;br/&amp;gt;Session / 验证码 / Meta 缓存&amp;#34;]
        METAREDIS[&amp;#34;Meta 专用 Redis Cluster&amp;#34;]
        ES[&amp;#34;Elasticsearch&amp;lt;br/&amp;gt;检索字段&amp;#34;]
        KAFKA[&amp;#34;Kafka MSK&amp;lt;br/&amp;gt;Meta 同步 / 网盘同步&amp;#34;]
    end

    subgraph 内容侧
        STREAM[&amp;#34;Streaming 流媒体&amp;#34;]
        CDN[&amp;#34;CDN / Squid 边缘缓存&amp;#34;]
        CP[&amp;#34;cloudProxy&amp;lt;br/&amp;gt;第三方网盘适配&amp;#34;]
    end

    STB --&amp;gt; ELB
    APP --&amp;gt; ELB
    WEB --&amp;gt; ELB
    ELB --&amp;gt; APISIX
    APISIX --&amp;gt; AUTH
    APISIX --&amp;gt; SESS
    APISIX --&amp;gt; META
    APISIX --&amp;gt; PROF
    APISIX --&amp;gt; CLOUD
    APISIX --&amp;gt; ROUTER
    ROUTER --&amp;gt; DRM
    ROUTER --&amp;gt; STREAM
    STREAM --&amp;gt; CDN
    STREAM --&amp;gt; CP
    META --&amp;gt; METAREDIS
    META --&amp;gt; ES
    AUTH --&amp;gt; MYSQL
    AUTH --&amp;gt; REDIS
    SESS --&amp;gt; REDIS
    CLOUD --&amp;gt; KAFKA
    META --&amp;gt; KAFKA&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;：链路是 &lt;code&gt;app -&amp;gt; ELB -&amp;gt; APISIX -&amp;gt; vod 微服务&lt;/code&gt;，限流用 Sentinel，网关做鉴权前置和 WAF。网关选型时我认真研究过 Netflix Zuul 2 从同步阻塞改异步非阻塞的历程——结论是网关本身绝不能成为瓶颈。我们体量没那么大，APISIX（底层同样是 Nginx + Lua 的高性能异步模型）已经够用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务拆分&lt;/strong&gt;：按业务域拆（Auth / Profile / Meta / Cloud / Session），不是按技术层拆。领域拆分的完整过程在&lt;a href=&#34;https://ftmao.com/blog/blog/posts/iptv/&#34;&gt;视频系统 DDD 实战&lt;/a&gt;里写过。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据层&lt;/strong&gt;：MySQL 按品牌分库（&lt;code&gt;{brand}_account_db&lt;/code&gt; 等）；Redis 承载 Session、验证码、热点数据；Meta 用&lt;strong&gt;独立的 Redis Cluster&lt;/strong&gt; 全量缓存，与业务缓存物理隔离；检索字段入 ES。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步化&lt;/strong&gt;：Kafka（MSK）只干两件事——Meta 同步管道和网盘同步，不滥用。这是我给自己立的规矩：每加一个 MQ topic 都要回答「同步调用为什么不行」。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三高并发把流量挡在离用户最近的地方&#34;&gt;三、高并发：把流量挡在离用户最近的地方&lt;/h2&gt;
&lt;p&gt;高并发的核心心法就一句：&lt;strong&gt;每一层都尽量把请求消化掉，别让它穿透到下一层&lt;/strong&gt;。越靠近 DB 越贵。&lt;/p&gt;
&lt;h3 id=&#34;31-网关层第一道削峰闸&#34;&gt;3.1 网关层：第一道削峰闸&lt;/h3&gt;
&lt;p&gt;我们在网关层做了三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;限流&lt;/strong&gt;：APISIX 自带 limit-req / limit-count 插件做粗粒度限流，Sentinel 做服务级细粒度熔断限流（现网组合就是「ELK + Skywalking + Sentinel（APISIX）」）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;鉴权前置&lt;/strong&gt;：明显非法的请求（无 token、签名错误、UA 黑名单）在网关直接 401/403，不放行到业务服务。被攻击时这一层挡掉的流量经常比业务层处理的还多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态配置下发走 CDN&lt;/strong&gt;：&lt;code&gt;/ui/cfg/v1&lt;/code&gt;、&lt;code&gt;/layout/v1&lt;/code&gt; 这类终端配置接口，内容变化频率低，直接走 CDN 缓存，源站只承担回源请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;32-热点接口播放鉴权链接的缓存设计&#34;&gt;3.2 热点接口：播放鉴权链接的缓存设计&lt;/h3&gt;
&lt;p&gt;CloudRouter（播放源路由服务）是我手里最热的路径：每次起播都要按 &lt;code&gt;logicStreamId&lt;/code&gt; 换真实的下载/播放链接。我做的设计非常直接：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[&amp;#34;App 起播请求&amp;lt;br/&amp;gt;logicStreamId&amp;#34;] --&amp;gt; B{&amp;#34;查 Redis&amp;#34;}
    B -- &amp;#34;命中&amp;#34; --&amp;gt; C[&amp;#34;刷新 TTL 滑动续期&amp;lt;br/&amp;gt;直接返回链接&amp;#34;]
    B -- &amp;#34;未命中&amp;#34; --&amp;gt; D[&amp;#34;查 MySQL 两表&amp;lt;br/&amp;gt;logic_stream + file_source&amp;#34;]
    D --&amp;gt; E[&amp;#34;构建下载链接 DownloadBO&amp;lt;br/&amp;gt;link / fileSize / expired&amp;#34;]
    E --&amp;gt; F[&amp;#34;写回 Redis&amp;#34;]
    F --&amp;gt; G[&amp;#34;返回&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三个细节值得说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;命中即续期（滑动 TTL）&lt;/strong&gt;：热播内容的链接永不过期，冷内容自然淘汰——不需要人工预热。这个设计替我省掉了整个「热点内容运营」的麻烦。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存对象自带过期时间&lt;/strong&gt;：链接本身有 &lt;code&gt;expired&lt;/code&gt; 字段，客户端在链接到期前会重新请求，和缓存 TTL 是两层独立的过期机制，互不相信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败快速抛出&lt;/strong&gt;：查不到直接抛业务异常，不做 fallback 到第三方存储的慢路径——播放链路每一毫秒都值钱。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;33-多级缓存l1--l2&#34;&gt;3.3 多级缓存：L1 + L2&lt;/h3&gt;
&lt;p&gt;纯 Redis 方案在超热点 key 上依然会打满单个分片（新片上线当晚的 metaId 我真实遇到过）。为了解决这个问题我研究了 Netflix 的 EVCache——它的多副本加客户端本地缓存思路给了我启发，但我们体量用不上 EVCache 全套，于是我基于 Caffeine（L1 进程内缓存）+ Redis（L2）自己实现了一个两级缓存框架并开源，见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/kecache-intro/&#34;&gt;我开发的 keCache：面向 Spring Boot 3 的 L1 + L2 两级缓存框架&lt;/a&gt;，要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;L1 短 TTL（秒级），扛住瞬时洪峰；&lt;/li&gt;
&lt;li&gt;L1 失效消息走 Redis Pub/Sub 广播，保证多实例间 L1 不一致的窗口可控；&lt;/li&gt;
&lt;li&gt;序列化用 Kryo，比 JDK 序列化小 3-5 倍。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;34-写路径异步化mq-削峰&#34;&gt;3.4 写路径异步化：MQ 削峰&lt;/h3&gt;
&lt;p&gt;写少但集中的场景（新片上线批量导入 Meta、网盘大规模刷新）我全部走 Kafka：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[&amp;#34;vod-sync-dispatcher&amp;lt;br/&amp;gt;每 30 秒轮询上游片源&amp;#34;] --&amp;gt; B[&amp;#34;Kafka MSK&amp;#34;]
    B --&amp;gt; C[&amp;#34;vod-imdb-meta-sync&amp;lt;br/&amp;gt;元数据入 Redis + ES&amp;#34;]
    D[&amp;#34;vod-cloud-sync-dispatcher&amp;#34;] --&amp;gt; B
    B --&amp;gt; E[&amp;#34;vod-cloud-sync-worker&amp;lt;br/&amp;gt;刷新网盘&amp;#34;]
    F[&amp;#34;http2Msk&amp;lt;br/&amp;gt;大数据 HTTP 回调转 MQ&amp;#34;] --&amp;gt; B
    B --&amp;gt; G[&amp;#34;偏好计算&amp;lt;br/&amp;gt;you may like&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;生产者-消费者模型 + MQ 缓冲，让上游脉冲（片源方一次推几万条 meta）被摊平成消费者能吃得动的匀速流。当时的 MQ 选型过程见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/mq-comparison-rabbitmq-kafka-rocketmq/&#34;&gt;消息队列总结：RabbitMQ、Kafka、RocketMQ 与 Redis MQ&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;另一个异步化的例子是我设计的转码调度：&lt;strong&gt;Quartz + DB 状态机&lt;/strong&gt;驱动，每个片源转出 1-3 个分辨率、每个分辨率一个任务，用「动态归一化权重调度算法」分配执行器（priority = 客户权重 / 活跃任务数，兼顾多客户公平与并发槽位控制），30 分钟轮询任务进度。慢不要紧，稳和公平才要紧——这是典型的「离线任务用状态机换可靠性」。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四高可用假设一切依赖都会挂&#34;&gt;四、高可用：假设一切依赖都会挂&lt;/h2&gt;
&lt;p&gt;我做高可用的两条心法，是从 Netflix 的工程实践里学来并在我们系统里反复验证过的：&lt;strong&gt;Design for failure&lt;/strong&gt;（为失败而设计）和 &lt;strong&gt;Blast radius&lt;/strong&gt;（控制爆炸半径）。落地到我们系统：&lt;/p&gt;
&lt;h3 id=&#34;41-熔断降级与兜底重试&#34;&gt;4.1 熔断、降级与兜底重试&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;熔断&lt;/strong&gt;：我们早期直接用 Netflix 开源的 Hystrix 做熔断隔离，Hystrix 停更后迁移到 Resilience4j（网关层用 Sentinel）。工具换了，规则不变：下游超时率/错误率超阈值就熔断，快速失败，绝不线程排队等死。Hystrix 的线程池隔离思想对我们影响很大——核心链路（登录、起播）和非核心链路（埋点上报）的资源池必须分开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;降级而非失败&lt;/strong&gt;：Meta 自动匹配失败时我定的策略是&lt;strong&gt;降级创建默认元数据&lt;/strong&gt;，不影响文件源状态——内容能播永远比信息完美重要。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兜底重试&lt;/strong&gt;：CloudAsset 的异常处理是我给全系统定的统一模式——&lt;strong&gt;3 次以内本轮调度重试，超过 3 次任务进失败状态，由独立的失败任务调度器兜底&lt;/strong&gt;。关键设计点：主流程不被重试拖死，失败任务有独立通道排队消化，互不阻塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    A[&amp;#34;任务执行&amp;#34;] --&amp;gt; B{&amp;#34;成功?&amp;#34;}
    B -- &amp;#34;是&amp;#34; --&amp;gt; C[&amp;#34;完成&amp;#34;]
    B -- &amp;#34;否, failCount+1&amp;#34;] --&amp;gt; D{&amp;#34;failCount ＞ 3?&amp;#34;}
    D -- &amp;#34;否&amp;#34; --&amp;gt; A
    D -- &amp;#34;是&amp;#34; --&amp;gt; E[&amp;#34;status = fail&amp;#34;]
    E --&amp;gt; F[&amp;#34;独立失败任务调度器&amp;lt;br/&amp;gt;定时扫描兜底重试&amp;#34;]
    F --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;后来我把这类「一定要跑到」的可靠执行逻辑抽成了通用组件开源，见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/ke-reliable-executor/&#34;&gt;我开发的 KE Reliable Executor&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id=&#34;42-多租户隔离分库就是隔舱&#34;&gt;4.2 多租户隔离：分库就是隔舱&lt;/h3&gt;
&lt;p&gt;多品牌共享一套代码，但我在数据层做了物理隔离：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL 按品牌分库（&lt;code&gt;{brand}_account_db&lt;/code&gt;、&lt;code&gt;{brand}_profile_db&lt;/code&gt;），公共数据在 &lt;code&gt;vod_meta_db&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;account_id&lt;/code&gt; 前缀区分品牌（MFC:11、TVELA:12、WAKA:13），应用层强制路由；&lt;/li&gt;
&lt;li&gt;实体普遍带 &lt;code&gt;env&lt;/code&gt; / &lt;code&gt;vnoId&lt;/code&gt; 字段，&lt;code&gt;logicStreamId&lt;/code&gt; 内嵌环境标识。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;某品牌数据被打爆、误删、需要回滚时，影响面天然被限制在单库内。这就是水密隔舱——后来某品牌做数据订正时我真切体会到这个设计的价值。&lt;/p&gt;
&lt;h3 id=&#34;43-依赖解耦订阅模式&#34;&gt;4.3 依赖解耦：订阅模式&lt;/h3&gt;
&lt;p&gt;DRM 链路里我有个坚持：drmlicense 不直接依赖 Headend 和 drmencryptor 的在线接口，而是以&lt;strong&gt;订阅模式&lt;/strong&gt;收 &lt;code&gt;DrmContentInfoBO&lt;/code&gt; 和 &lt;code&gt;ContentKeyBO&lt;/code&gt;，自己组装 logicalStream 与 PSSH 的关系。上游挂了，license 下发用已订阅到的数据照常工作。原因是算术：&lt;strong&gt;同步调用是可用性的乘法，异步订阅是加法&lt;/strong&gt;——一个可用性 99.9% 的服务同步串依赖十个同样 99.9% 的服务，链路可用性就只剩 99% 了。&lt;/p&gt;
&lt;h3 id=&#34;44-混沌工程轻量版就够&#34;&gt;4.4 混沌工程：轻量版就够&lt;/h3&gt;
&lt;p&gt;Netflix 的 Chaos Monkey 是随机杀生产实例，逼所有服务习惯「邻居随时消失」。我评估后没敢直接上生产，但保留了这个思想的两个轻量实践：预发环境定期随机关停单个实例观察告警与自愈；每个季度做一次依赖演练（手动切断 Redis 从库、Kafka 单 broker），验证降级路径真的能用。我的原则：&lt;strong&gt;降级方案不演练就等于没有&lt;/strong&gt;——每次演练几乎都能发现某条降级路径已经悄悄失效。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五高性能把钱花在读路径上&#34;&gt;五、高性能：把钱花在读路径上&lt;/h2&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;&lt;strong&gt;Meta 全量入内存&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;元数据全量进专用 Redis Cluster，终端 &lt;code&gt;/root/v1&lt;/code&gt;、&lt;code&gt;/tree/v1&lt;/code&gt; 等接口零 DB 访问&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;检索走 ES&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;只有关键检索字段入 Elasticsearch，不全量双写&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;播放内容边缘缓存&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Streaming 前置 Squid + CDN，热点片源命中边缘节点，源站只承担回源（对应 Netflix Open Connect 的角色，我们用商用 CDN 实现）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;大表治理&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;观看历史表现网 2-3 亿行，是我一直盯着的最大风险点：每天定时任务删 90 天前历史 + 按账号双表拆分（设备账号/普通账号）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;JVM 与连接池&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;G1/ZGC、HikariCP 连接池按「连接数 = (核数 × 2) + 磁盘数」起步压测调优，慢 SQL 进 Skywalking 告警&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;原则：&lt;strong&gt;性能优化的 80% 是让请求根本到不了慢的地方&lt;/strong&gt;，剩下 20% 才是调参数。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六缓存一致性与缓存高可用&#34;&gt;六、缓存一致性与缓存高可用&lt;/h2&gt;
&lt;p&gt;这是全文我最想细说的一章。缓存是性能的来源，也是我事故最多的地方。&lt;/p&gt;
&lt;h3 id=&#34;61-一致性策略先写-db后删缓存&#34;&gt;6.1 一致性策略：先写 DB，后删缓存&lt;/h3&gt;
&lt;p&gt;vod-auth 的账号数据我用的是标准的 &lt;strong&gt;Cache Aside + 写后删除&lt;/strong&gt;：先写 DB，成功后再删缓存，下次读时回源重建。为什么选「删除」而不是「更新」？我想清楚过：「先写 DB 再删缓存」只有在「读线程恰好踩着写间隙回源了旧值」的极端窗口才会短暂不一致，而「更新缓存」在并发双写下会乱序覆盖出脏数据。删除的失败模式是 miss（回源一次，便宜），更新的失败模式是脏数据（播错内容，贵）。&lt;/p&gt;
&lt;p&gt;对一致性要求更高的场景（套餐权益、付费状态），我在删除之上再加一层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;延迟双删&lt;/strong&gt;：写 DB → 删缓存 → 延迟 N 秒 → 再删一次，兜住间隙回源；&lt;/li&gt;
&lt;li&gt;或用 MQ 发缓存失效消息，由消费者负责删除 + 失败重试，把「删缓存失败」这个尾巴也闭环掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;62-缓存三大经典问题的组合拳&#34;&gt;6.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;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;恶意刷不存在的 metaId / accountId&lt;/td&gt;
					&lt;td&gt;布隆过滤器拦截 + 空值短 TTL 缓存（30 秒）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;击穿&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;爆款剧上线，热点 key 过期瞬间&lt;/td&gt;
					&lt;td&gt;逻辑过期 + 互斥重建：value 里带逻辑过期时间，过期后只放一个线程回源，其余返回旧值&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;雪崩&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;大量 key 同 TTL 同时到期&lt;/td&gt;
					&lt;td&gt;TTL 加随机抖动（基础值 ± 20%）+ Redis Cluster 多主分摊&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;逻辑过期方案有个工程细节：回源重建要用分布式锁（Meta 绑定我用的就是分布式锁，获取失败快速失败、重新调度），且锁的 TTL 必须大于回源耗时，finally 里释放——我在 CloudAsset 方案里专门写了「任何异常都会释放分布式锁、清除 ThreadLocal」，这是血泪经验，锁泄漏一次就够排查一晚上。&lt;/p&gt;
&lt;h3 id=&#34;63-缓存自身的高可用&#34;&gt;6.3 缓存自身的高可用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redis Cluster 多主 + 从&lt;/strong&gt;：Meta 缓存我用独立集群，和 Session/验证码的集群物理隔离——Meta 集群被打挂不影响用户登录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多级缓存兜底&lt;/strong&gt;：Redis 整体不可用时，L1（Caffeine）还能扛住读过期窗口，服务降级而不是全灭。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从 EVCache 学到的运营观&lt;/strong&gt;：我研究 Netflix EVCache 时最大的收获不是 memcached 本身，而是&lt;strong&gt;他们把缓存当成 tier-0 服务运营&lt;/strong&gt;——独立容量规划、独立监控、独立 SLO。我照这个思路给缓存集群配了和业务服务同级别的告警与容量评审。缓存在高并发系统里不是优化项，是一级基础设施——这点很多团队要到出事才懂，我庆幸自己学得早。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七零信任安全与必要防御&#34;&gt;七、零信任安全与必要防御&lt;/h2&gt;
&lt;h3 id=&#34;71-零信任永不信任始终验证&#34;&gt;7.1 零信任：永不信任，始终验证&lt;/h3&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;：accessToken（JWT，&lt;code&gt;sub = accountId&lt;/code&gt;）+ 播放票据 tk12（内含 &lt;code&gt;expireTime / accountId / streamId&lt;/code&gt;，由平台密钥签发）。App 起播时两个都要验，token 验「你是谁」，tk12 验「你能不能播这条流、能播到什么时候」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备侧&lt;/strong&gt;：账号卡与设备绑定（&lt;code&gt;ac_device_binding&lt;/code&gt;），&lt;code&gt;device_limit&lt;/code&gt; 限制并发设备数，挤掉盗号共享的盈利空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务间&lt;/strong&gt;：内部接口（&lt;code&gt;/internal-api/...&lt;/code&gt;）不走公网网关，服务间调用带服务身份凭证。我评估过 mTLS + SPIFFE 这套工作负载身份方案（Netflix 零信任改造用的就是这条路），对我们体量偏重，最终落地为「内部接口不暴露 + 调用方白名单」，等规模上来再升级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最小权限&lt;/strong&gt;：cloudProxy 是我专门做的收敛层——把对第三方网盘（Mega 等）的所有访问收敛成一个适配层，只暴露 &lt;code&gt;checkPassword&lt;/code&gt; 和 &lt;code&gt;getDownloadLink&lt;/code&gt; 两个接口，业务服务根本不接触第三方凭证。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;72-一次真实的安全整改网盘密码泄露&#34;&gt;7.2 一次真实的安全整改：网盘密码泄露&lt;/h3&gt;
&lt;p&gt;这是我亲手推动的整改，值得完整复盘。背景：网盘打包工具生成的加密网盘包里&lt;strong&gt;含账号 + 加密后的登录密码&lt;/strong&gt;，加解密密钥一旦泄露，所有网盘账号密码全部泄露。&lt;/p&gt;
&lt;p&gt;整改分两步走：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步，密码不落地。&lt;/strong&gt; 新网盘包不再写密码，改为写入与网盘账号一一对应的 &lt;code&gt;shareKey&lt;/code&gt;（UUID）。导入时只校验 shareKey 一致性，通过后把网盘与用户关联。按网盘包 &lt;code&gt;version&lt;/code&gt; 字段区分新老包走新旧两条校验逻辑，平滑迁移——&lt;strong&gt;存量兼容是安全改造里最容易被低估的工作量&lt;/strong&gt;，这一步我花的精力比新逻辑本身还多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步，密钥体系升级。&lt;/strong&gt; 对称加密改 RSA 非对称加密：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    A[&amp;#34;CP / Reseller&amp;lt;br/&amp;gt;只拿到 RSA 公钥&amp;#34;] --&amp;gt; B[&amp;#34;公钥加密网盘包&amp;lt;br/&amp;gt;公钥泄露也无法解包&amp;#34;]
    C[&amp;#34;VNO 上传私钥&amp;lt;br/&amp;gt;加密保存于平台 pkg_rsa_key 表&amp;#34;] --&amp;gt; D[&amp;#34;导入时按 VNO 查私钥&amp;#34;]
    B --&amp;gt; E[&amp;#34;网盘包带签名&amp;lt;br/&amp;gt;含 pkgId + keyId&amp;#34;]
    E --&amp;gt; D
    D --&amp;gt; F[&amp;#34;先验签再解密&amp;lt;br/&amp;gt;解密成功才关联用户&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;配套我设计的&lt;strong&gt;密钥轮换机制&lt;/strong&gt;：平台同时最多两个 Key 生效；更新时旧密钥先进入最长 30 天缓冲期，再生成新密钥对；存在生效中的旧密钥则不允许再次更新——防止轮换频率失控导致历史包无法解密。&lt;/p&gt;
&lt;p&gt;另外扫码登录链路我也推动做过同类加固：扫码站点从 S3 静态托管（明文登录）迁移到托管服务并接入加固方案，鉴权统一收口到 vod-auth。&lt;/p&gt;
&lt;h3 id=&#34;73-接口消息体加密https-之上的第二道锁&#34;&gt;7.3 接口消息体加密：HTTPS 之上的第二道锁&lt;/h3&gt;
&lt;p&gt;有人会问：全站都上 HTTPS 了，为什么还要在应用层再加密一次消息体？我的理由有三个，都来自实际教训：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;HTTPS 防不住终端侧的抓包&lt;/strong&gt;。Charles/Fiddler 挂个代理证书，App 和网关之间的明文报文一览无余——协议字段、业务参数、token 用法全暴露。C 端 App 发出去的那一刻起，客户端环境就不可信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTPS 到网关就终止了&lt;/strong&gt;。TLS 在 ELB/APISIX 卸载后，内网转发的是明文；零信任的原则是内网也不可信，敏感报文在网关之后依然应该处于加密状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPTV 场景的特殊性&lt;/strong&gt;：播放鉴权、充值、账号接口一旦被逆向出报文格式，黑产就可以脱离 App 直接伪造请求刷接口。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我设计的方案是 &lt;strong&gt;TLS 为底线 + 应用层 AES 加密消息体&lt;/strong&gt;，核心链路如下：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[&amp;#34;App 启动&amp;lt;br/&amp;gt;ECDH 密钥协商&amp;lt;br/&amp;gt;网关公钥经 SSL Pinning 校验&amp;#34;] --&amp;gt; B[&amp;#34;得到会话密钥 sessionKey&amp;lt;br/&amp;gt;按 session 绑定&amp;#34;]
    B --&amp;gt; C[&amp;#34;业务请求&amp;lt;br/&amp;gt;AES-256-GCM 加密 body&amp;#34;]
    C --&amp;gt; D[&amp;#34;明文头带 token + 时间戳&amp;lt;br/&amp;gt;+ nonce + HMAC 签名&amp;#34;]
    D --&amp;gt; E[&amp;#34;网关统一解密过滤器&amp;lt;br/&amp;gt;验签名 / 验时间窗 / 验 nonce&amp;#34;]
    E --&amp;gt; F[&amp;#34;业务服务收到明文&amp;lt;br/&amp;gt;响应同样加密回传&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;工程上的几个取舍，都是试出来的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;密钥协商用 ECDH，不用硬编码密钥&lt;/strong&gt;。早期图省事在 App 里内置过 AES 密钥，App 被反编译后全线失守，改成登录/启动时 ECDH 动态协商、密钥绑定 session、服务端定期强制轮换。SSL Pinning（证书固定）挡住大部分中间人代理，让 ECDH 协商本身不被劫持。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AES-256-GCM 而不是 CBC&lt;/strong&gt;：GCM 自带完整性校验（AEAD），加密和防篡改一次完成；密钥 nonce 绝不复用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;头部明文、body 加密&lt;/strong&gt;：token、时间戳、nonce、签名放明文头里，网关不解密就能先做限流和鉴权前置；body 整体加密，解密收敛在网关的统一过滤器里，业务服务无感知——加解密逻辑散落进每个 Controller 是维护灾难。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不是所有接口都加密&lt;/strong&gt;。EPG、Meta 这类公开内容接口只走 HTTPS + 签名；登录、充值、播放鉴权、账号操作这些敏感接口才上消息体加密。全量加密的 CPU 成本和排查问题的痛苦不成比例——加密后的日志排障要靠专门的脱密工具，这也是成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绝不自研算法&lt;/strong&gt;。所有原语都用成熟实现（AES-GCM、ECDH、HMAC-SHA256），自己拼协议可以，自己造算法不行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;74-必要防御防攻击清单&#34;&gt;7.4 必要防御：防攻击清单&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;接口刷量 / CC 攻击&lt;/td&gt;
					&lt;td&gt;网关限流 + Sentinel 熔断；登录、验证码接口按 IP + 账号双维度计数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;短信/邮件轰炸机&lt;/td&gt;
					&lt;td&gt;验证码存 Redis 带 TTL，&lt;strong&gt;验证成功后立即删除&lt;/strong&gt;防重放；单账号/单 IP 频次限制&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重放攻击&lt;/td&gt;
					&lt;td&gt;请求带时间戳 + nonce，token 短过期&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;抓包 / 中间人&lt;/td&gt;
					&lt;td&gt;HTTPS + SSL Pinning + 敏感接口消息体加密（见 7.3）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;越权访问&lt;/td&gt;
					&lt;td&gt;tk12 绑定 &lt;code&gt;accountId + streamId&lt;/code&gt;，换账号播放直接鉴权失败&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;篡改网盘包&lt;/td&gt;
					&lt;td&gt;RSA 签名验签，验签不过不解密&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;撞库 / 盗号&lt;/td&gt;
					&lt;td&gt;登录失败计数锁定（&lt;code&gt;account_black_list&lt;/code&gt;）、设备绑定、子账号体系隔离&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;敏感信息外泄&lt;/td&gt;
					&lt;td&gt;交接文档脱敏（我们的交接文档里曾出现明文环境密码，是我抓出来的典型反面教材）；日志脱敏；DB 密码字段只存哈希&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;75-drm内容安全的最后一道门&#34;&gt;7.5 DRM：内容安全的最后一道门&lt;/h3&gt;
&lt;p&gt;DRM 链路的安全设计我同样按零信任思路做：ContentKey 由独立密钥服务（drmencryptor）管理，&lt;strong&gt;先查缓存没有再向外部 DRM 厂商申请并落库缓存&lt;/strong&gt;；播放优先走离线 license（有缓存用缓存），PSSH 异常才降级在线 license。license 本身带过期时间，App 本地不持久化密钥。系统支持 Goose DRM 与 Multi-DRM（Widevine）切换，切换动作就是删旧流加新流——密钥体系与业务流解耦，这个设计让后来的 DRM 厂商迁移几乎没动业务代码。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八可观测性没有度量就没有高可用&#34;&gt;八、可观测性：没有度量就没有高可用&lt;/h2&gt;
&lt;p&gt;我们的现网组合：&lt;strong&gt;ELK 日志 + Skywalking 链路追踪 + Prometheus/Grafana 指标 + Nacos 配置中心&lt;/strong&gt;，每个子系统独立 ES index。&lt;/p&gt;
&lt;p&gt;我压测过的容量数据（v7.16 集群）：只写日志约 11 万条/秒，只写 Skywalking 约 2 万条/秒，混合写时日志 6 万 + 链路 1 万/秒。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;血泪教训&lt;/strong&gt;：周一高峰 Skywalking 曾出现 4 小时写入延迟，日志量增长也让 Logstash 排队。事后我立了两条规矩：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;日志与链路采样分离&lt;/strong&gt;：高峰期链路数据降采样，保住日志；两者绝不能互相拖死。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地日志保留 30 天&lt;/strong&gt;，crontab 定时清理——磁盘写满导致的 ELK 雪崩比业务故障还常见。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我还坚持一点：告警要打在&lt;strong&gt;用户可感知的指标&lt;/strong&gt;上（起播成功率、鉴权失败率、链接过期率），而不是只打 CPU 内存——机器好好的但用户看不了，是最尴尬的监控盲区。服务间的依赖关系复杂到一定程度后，「告警来了不知道谁影响了谁」会成为新问题，Netflix 为此专门做过一套服务拓扑系统，我研究过它的实现并整理了笔记：&lt;a href=&#34;https://ftmao.com/blog/blog/posts/netflix-service-topology-at-scale/&#34;&gt;Netflix 服务拓扑系统笔记&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结一张-checklist&#34;&gt;九、总结：一张 checklist&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;用到的 Netflix 开源组件 / 实践&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;高并发&lt;/td&gt;
					&lt;td&gt;网关限流、缓存优先、MQ 削峰、多级缓存&lt;/td&gt;
					&lt;td&gt;Zuul 2 异步化思想、EVCache 启发 keCache&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;高可用&lt;/td&gt;
					&lt;td&gt;熔断降级、失败兜底调度器、多租户分库隔离、订阅模式解耦&lt;/td&gt;
					&lt;td&gt;Hystrix（后迁 Resilience4j/Sentinel）、Chaos Monkey 轻量版&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;高性能&lt;/td&gt;
					&lt;td&gt;Meta 全量入内存、ES 检索、CDN/Squid 边缘缓存、大表治理&lt;/td&gt;
					&lt;td&gt;Open Connect 的边缘缓存角色&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;缓存一致性&lt;/td&gt;
					&lt;td&gt;先写 DB 后删缓存 + 延迟双删、逻辑过期防击穿、随机 TTL 防雪崩&lt;/td&gt;
					&lt;td&gt;EVCache 的缓存运营观&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;零信任&lt;/td&gt;
					&lt;td&gt;双票据鉴权、设备绑定、内部接口白名单、最小权限适配层&lt;/td&gt;
					&lt;td&gt;零信任改造思路（mTLS/SPIFFE 评估后暂缓）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;攻击防御&lt;/td&gt;
					&lt;td&gt;网关 WAF/限流、验证码防重放、RSA 验签、黑名单、敏感接口消息体加密&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;可观测&lt;/td&gt;
					&lt;td&gt;ELK + Skywalking + Prometheus，日志链路采样分离&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;/strong&gt;。网盘密码泄露、Skywalking 四小时延迟、2-3 亿行的历史大表——每个设计决策背后都有一次事故或一次险些事故。Netflix 的开源组件和博客帮我少踩了很多坑，但终究要自己踩过才知道坑有多深。架构师的职责不是画出完美的图，而是让每个坑只踩一次。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>Spark 与 Flink 入门：批与流的两种血统，视频公司里各干什么活</title>
      <link>https://ftmao.com/blog/posts/spark-flink-basics-video-company/</link>
      <pubDate>Fri, 02 Oct 2026 19:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/spark-flink-basics-video-company/</guid>
      <description>&lt;h1 id=&#34;spark-与-flink-入门批与流的两种血统视频公司里各干什么活&#34;&gt;Spark 与 Flink 入门：批与流的两种血统，视频公司里各干什么活&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：四类人——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;后端/数据工程师&lt;/strong&gt;：要上手 Spark 或 Flink，需要心智模型加能跑的代码（看&amp;quot;基础&amp;quot;两节，附 Java API 示例）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架构师/技术负责人&lt;/strong&gt;：在给数据平台选型，想知道两者的边界和硬件投入（看&amp;quot;血统对比&amp;quot;&amp;ldquo;算力规格&amp;quot;和&amp;quot;选型决策&amp;rdquo;）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运维/SRE&lt;/strong&gt;：要看监控什么、出事怎么处理（看&amp;quot;监控&amp;quot;和&amp;quot;故障与恢复&amp;quot;）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;正在踩坑的人&lt;/strong&gt;：直接翻到文末两家的避坑清单。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;阅读前提：了解 Kafka 是什么即可，不需要大数据背景。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;前两篇讲推荐 Feed 架构时，Spark 和 Flink 反复出现：Spark 做离线训练，Flink 做近线特征。有读者问：Spark 不就是内存分布式计算吗，和 Flink 功能差不多？&lt;/p&gt;
&lt;p&gt;看着像，血统完全不同。这篇把两者各自的基础概念讲清，然后放进一家类 Netflix 的视频公司里看它们分别干什么活，最后附上两家的避坑指南。&lt;/p&gt;
&lt;h2 id=&#34;spark-基础批处理血统的内存计算引擎&#34;&gt;Spark 基础：批处理血统的内存计算引擎&lt;/h2&gt;
&lt;p&gt;Spark 在 2010 年前后接棒 Hadoop MapReduce，核心卖点一句话：&lt;strong&gt;把中间结果放内存，迭代计算不用反复落盘&lt;/strong&gt;。MapReduce 每轮迭代都要读写 HDFS，Spark 把数据缓存进内存，迭代型任务（机器学习训练要扫数据几十遍）快了十到一百倍。&lt;/p&gt;
&lt;p&gt;三个核心概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RDD / DataFrame&lt;/strong&gt;：数据被抽象成分布式集合，切成分区（partition）摊在集群各节点上。RDD 是原始 API，现在日常用的是 DataFrame——带结构（schema）的表，配合 Spark SQL 直接写 SQL。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DAG 执行&lt;/strong&gt;：你对数据做的一串操作（filter、join、groupBy）不会立即执行，而是攒成一张有向无环图；遇到&amp;quot;要结果&amp;quot;的动作（count、写盘）才触发，调度器把图切成 stage、分发执行。惰性求值让引擎有机会做全局优化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shuffle&lt;/strong&gt;：数据按 key 重新分发的操作（groupBy、join 都会触发），是 Spark 作业里最贵的一步——数据要跨节点搬运、序列化、落盘。后面避坑指南里一半的坑都和它有关。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;血统决定视角：在 Spark 眼里，数据是一批一批的&lt;/strong&gt;——有界的、静态的数据集。它的流处理能力（Structured Streaming）是后来在批的骨架上加的：把数据流切成一小批一小批，攒一批算一次（micro-batch）。延迟天然是秒级到分钟级。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;上手：一个 ETL 作业的 Java 写法&lt;/strong&gt;。场景：把昨天的播放原始日志清洗成&amp;quot;每视频日播放量&amp;quot;聚合表：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SparkSession spark &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; SparkSession.&lt;span style=&#34;color:#a6e22e&#34;&gt;builder&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;appName&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;play-events-etl&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;getOrCreate&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 读：数据湖里昨天的原始播放事件（分区表）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Dataset&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Row&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; events &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; spark.&lt;span style=&#34;color:#a6e22e&#34;&gt;read&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;parquet&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;s3://warehouse/raw/play_events/dt=2026-10-01/&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 处理：过滤脏数据、按 event_id 去重、换算播放分钟数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Dataset&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Row&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; cleaned &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; events
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;filter&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;duration &amp;gt; 0 AND user_id IS NOT NULL&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;dropDuplicates&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;event_id&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;withColumn&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;play_minutes&amp;#34;&lt;/span&gt;, col(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;duration&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;divide&lt;/span&gt;(60));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// join 内容维度表补标题和类目（小表广播，避免 shuffle）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Dataset&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Row&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; enriched &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; cleaned
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;join&lt;/span&gt;(broadcast(spark.&lt;span style=&#34;color:#a6e22e&#34;&gt;read&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;parquet&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;s3://warehouse/dim/videos/&amp;#34;&lt;/span&gt;)), &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video_id&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 聚合出结果并写回数仓（coalesce 控制输出文件数，防止小文件）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;enriched.&lt;span style=&#34;color:#a6e22e&#34;&gt;groupBy&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video_id&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;category&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;agg&lt;/span&gt;(count(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;*&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;alias&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;plays&amp;#34;&lt;/span&gt;), sum(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;play_minutes&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;alias&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;total_minutes&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;coalesce&lt;/span&gt;(4)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;write&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;mode&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;overwrite&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;parquet&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;s3://warehouse/agg/daily_video_plays/dt=2026-10-01/&amp;#34;&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;注意这段代码提交后&lt;strong&gt;什么都不会立刻发生&lt;/strong&gt;——filter、join、groupBy 只是在攒 DAG，最后的 &lt;code&gt;write()&lt;/code&gt; 这个动作才触发真正的执行。这就是前面说的惰性求值。&lt;/p&gt;
&lt;p&gt;这段代码里还有一件&amp;quot;看不见&amp;quot;的事值得说破：&lt;strong&gt;分区是自动切的，你全程没有指定过它&lt;/strong&gt;。分区的三个来源：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;读入分区由文件决定&lt;/strong&gt;：读 parquet 时 Spark 按文件切块，&lt;code&gt;spark.sql.files.maxPartitionBytes&lt;/code&gt;（默认 128MB）决定每块多大，每块一个分区。路径里的 &lt;code&gt;dt=2026-10-01/&lt;/code&gt; 本身就是物理分区，Spark 只扫这一天的目录，其余按分区裁剪跳过；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;shuffle 后分区由配置决定&lt;/strong&gt;：&lt;code&gt;groupBy&lt;/code&gt; 触发数据重分发，之后的分区数由 &lt;code&gt;spark.sql.shuffle.partitions&lt;/code&gt; 控制，&lt;strong&gt;默认 200&lt;/strong&gt;——小作业跑完写出 200 个小文件，罪魁祸首就是它；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;手动干预只有两个 API&lt;/strong&gt;：&lt;code&gt;repartition(n)&lt;/code&gt; 重新洗牌（会产生一次 shuffle，贵），&lt;code&gt;coalesce(n)&lt;/code&gt; 只合并不洗牌（便宜）。上面代码写出前的 &lt;code&gt;coalesce(4)&lt;/code&gt; 就是故意把默认的 200 个分区合并成 4 个输出文件。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;那&amp;quot;拉起几十台机器并行计算&amp;quot;体现在哪？答案是：不体现在代码里，体现在物理执行里。&lt;/strong&gt; 同一份代码在一台笔记本上（local 模式）和一百台机器上跑，一行都不用改——分布式是部署特征，不是代码特征。看这段 ETL 提交到集群时实际发生什么：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;spark-submit &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --class com.example.PlayEventsETL &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --master yarn &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --num-executors &lt;span style=&#34;color:#ae81ff&#34;&gt;50&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\ &lt;/span&gt;       &lt;span style=&#34;color:#75715e&#34;&gt;# 拉起 50 个 executor 进程，分布在集群各节点&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --executor-cores &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\ &lt;/span&gt;       &lt;span style=&#34;color:#75715e&#34;&gt;# 每个 executor 5 核&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --executor-memory 20g &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  --driver-memory 8g &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  app.jar&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这里的 &lt;code&gt;app.jar&lt;/code&gt; 是&lt;strong&gt;你自己的应用代码打包产物&lt;/strong&gt;：前面那段 ETL 的 Java 代码用 Maven/Gradle 编译，打成包含全部依赖的 fat jar（Maven 用 shade 插件、Gradle 用 shadow 插件），&lt;code&gt;--class&lt;/code&gt; 指定里面的入口类。提交时这个 jar 被上传到集群，driver 拿到后把代码分发到各 executor——&lt;strong&gt;executor 上跑的 task，执行的就是这个 jar 里的字节码&lt;/strong&gt;。所以 Spark 作业的交付物就是一个 jar：改代码 = 重新打包重新提交。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;生产环境里谁来&amp;quot;敲&amp;quot;这个 spark-submit？&lt;/strong&gt; 凌晨两点跑批不是值班同学手动敲命令。业界的提交方式按层次分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SparkLauncher&lt;/strong&gt;：Spark 官方自带的 Java API，在 Java 进程里编程式提交作业，还返回句柄可轮询状态——自研平台的标准做法：&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SparkAppHandle handle &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; SparkLauncher()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setAppResource&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;app.jar&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setMainClass&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;com.example.PlayEventsETL&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setMaster&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;yarn&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setConf&lt;/span&gt;(SparkLauncher.&lt;span style=&#34;color:#a6e22e&#34;&gt;EXECUTOR_MEMORY&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;20g&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;startApplication&lt;/span&gt;();   &lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; 异步返回&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;，&lt;/span&gt;拿 handle 跟踪作业状态&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Apache Livy&lt;/strong&gt;：把提交做成 REST 服务——HTTP POST 一个 jar、轮询状态、拉日志。Web 系统里嵌入 Spark 提交能力的标准件；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调度系统&lt;/strong&gt;：Apache DolphinScheduler、Airflow 这类工作流平台，管的是&amp;quot;凌晨 2 点跑 ETL、成功了再跑模型训练&amp;quot;这种依赖编排，底层调的仍然是 spark-submit 或 Livy；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;云厂商 API&lt;/strong&gt;：EMR Steps、Databricks Jobs 这类托管服务，提交动作变成一个云 API 调用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;顺带纠个常见的偏：Apache Commons 系列里没有 Spark 专用提交工具——Commons Exec 只是通用的外部进程包装器，有些自研平台拿它包 spark-submit 命令行（拿退出码、控超时），能用但不是主流。&lt;strong&gt;想在 Java 里提交作业，正路是 SparkLauncher 或 Livy。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;50 个 executor × 5 核 = &lt;strong&gt;250 个任务槽位同时干活，1TB 堆内存的总池子&lt;/strong&gt;。作业跑起来后，逻辑计划被拆成物理 stage，每个分区对应一个 task，task 被调度到空闲槽位上：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;Stage 1 读取+过滤&amp;lt;br/&amp;gt;2TB parquet 按 128MB 切块&amp;lt;br/&amp;gt;约 16000 个分区 = 16000 个 task&amp;#34;] --&amp;gt; B[&amp;#34;Stage 2 join&amp;lt;br/&amp;gt;维度表广播到每个 executor&amp;lt;br/&amp;gt;无 shuffle&amp;#34;]
    B --&amp;gt; C[&amp;#34;shuffle 重分发&amp;lt;br/&amp;gt;按 video_id 搬运数据&amp;#34;]
    C --&amp;gt; D[&amp;#34;Stage 3 聚合+写盘&amp;lt;br/&amp;gt;200 个 task 各自聚合自己那份&amp;#34;]
    A -.-&amp;gt;|&amp;#34;250 个槽位并发&amp;lt;br/&amp;gt;16000 个 task 分批消化&amp;#34;| A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个数字感：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一天 100 亿条播放事件约 2TB parquet，切成约 16000 个分区，每个分区一个 task——&lt;strong&gt;250 个槽位并发，分批消化完这 16000 个 task&lt;/strong&gt;，这就是&amp;quot;拉起多个节点&amp;quot;的物理形态；&lt;/li&gt;
&lt;li&gt;谁挂了谁重试：某个 executor 宕机，它跑过的 task 在别的节点上按血统重算，作业整体不停；&lt;/li&gt;
&lt;li&gt;想更快就加机器：&lt;code&gt;--num-executors 100&lt;/code&gt;，槽位翻倍，16000 个 task 消化速度就近似翻倍——&lt;strong&gt;这就是水平扩展：代码不变，吞吐随机器数线性涨&lt;/strong&gt;（直到被 shuffle 或存储 IO 卡住）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ALS 训练是同一个道理，只是把&amp;quot;一批 task&amp;quot;换成&amp;quot;几十轮、每轮一批 task&amp;quot;：每一轮迭代都是一次全集群的分布式计算，中间结果留在内存里不落盘——这正是 Spark 相比 MapReduce 快的根源。&lt;/p&gt;
&lt;p&gt;所以读 Spark 代码的正确姿势是：&lt;strong&gt;代码是蓝图，集群按分区把蓝图实体化成成千上万的 task&lt;/strong&gt;。你在代码里看不到&amp;quot;多节点&amp;quot;，正如你在建筑图纸上看不到施工队——但图纸上的每一堵墙，都会变成某个工人在某个脚手架上的具体动作。&lt;/p&gt;
&lt;p&gt;机器学习也一样是 DataFrame 风格的 API。Spark MLlib 里自带 ALS（交替最小二乘，就是矩阵分解），拿交互数据训练推荐模型只要几行：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ALS als &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; ALS()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setMaxIter&lt;/span&gt;(10)          &lt;span style=&#34;color:#75715e&#34;&gt;// 迭代轮数：交替优化用户矩阵和物品矩阵&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setRegParam&lt;/span&gt;(0.&lt;span style=&#34;color:#a6e22e&#34;&gt;1&lt;/span&gt;)        &lt;span style=&#34;color:#75715e&#34;&gt;// 正则化，防过拟合&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setUserCol&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;user_id&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setItemCol&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video_id&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setRatingCol&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;score&amp;#34;&lt;/span&gt;)   &lt;span style=&#34;color:#75715e&#34;&gt;// 行为强度：完播=3 点赞=2 点击=1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setColdStartStrategy&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;drop&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ALSModel model &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; als.&lt;span style=&#34;color:#a6e22e&#34;&gt;fit&lt;/span&gt;(playEvents);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 输出用户向量和物品向量，供线上召回使用&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;model.&lt;span style=&#34;color:#a6e22e&#34;&gt;userFactors&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;write&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;parquet&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;s3://warehouse/rec/user_factors/&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;model.&lt;span style=&#34;color:#a6e22e&#34;&gt;itemFactors&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;write&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;parquet&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;s3://warehouse/rec/item_factors/&amp;#34;&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;fit()&lt;/code&gt; 跑完，&lt;code&gt;userFactors&lt;/code&gt; 就是前面推荐架构文章里说的&amp;quot;每个用户一个几百维的向量&amp;quot;——概念和代码在这里接上了。&lt;/p&gt;
&lt;h2 id=&#34;flink-基础流处理血统的计算引擎&#34;&gt;Flink 基础：流处理血统的计算引擎&lt;/h2&gt;
&lt;p&gt;Flink 的出发点反过来：&lt;strong&gt;世界本质是流，批只是流的特例&lt;/strong&gt;（有界流）。数据逐条进来逐条处理，毫秒到秒级延迟。&lt;/p&gt;
&lt;p&gt;四个核心概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;事件时间与水位线（event time &amp;amp; watermark）&lt;/strong&gt;：事件&amp;quot;发生的时间&amp;quot;和&amp;quot;到达系统的时间&amp;quot;经常不同（用户手机离线两小时，数据才补传上来）。Flink 允许按发生时间计算，水位线是&amp;quot;我认为早于 t 的事件基本都到齐了&amp;quot;的声明，晚到超过容忍度的数据进侧输出流。&lt;strong&gt;这是流处理区别于批处理最本质的概念&lt;/strong&gt;——批处理里根本没有&amp;quot;数据晚到&amp;quot;这个问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态（state）&lt;/strong&gt;：算子可以持有状态，比如&amp;quot;最近一小时的点击数&amp;quot;。状态按 key 分布在各并行实例上，大作业可以到 TB 级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;checkpoint / exactly-once&lt;/strong&gt;：定期给所有算子的状态做全局一致性快照，故障后从快照恢复，保证每条数据恰好被计算一次。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;savepoint&lt;/strong&gt;：手动触发的快照，用于升级和扩缩容——代价是&amp;quot;停作业、改并行度、从快照重启&amp;quot;，这也是之前那篇 &lt;a href=&#34;https://ftmao.com/blog/blog/posts/flink-autoscaling-netflix-two-autoscalers/&#34;&gt;Netflix 给 Flink 做自动扩缩容&lt;/a&gt;那么难的原因。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;上手：实时热榜的 Java 写法&lt;/strong&gt;。场景：统计每个视频最近 10 分钟的播放量，每分钟刷新一次（滑动窗口）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;StreamExecutionEnvironment env &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; StreamExecutionEnvironment.&lt;span style=&#34;color:#a6e22e&#34;&gt;getExecutionEnvironment&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 每 60 秒做一次 checkpoint，故障恢复就回到最近一次快照&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;env.&lt;span style=&#34;color:#a6e22e&#34;&gt;enableCheckpointing&lt;/span&gt;(60_000);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 数据源：Kafka&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;KafkaSource&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;PlayEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; source &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; KafkaSource.&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;PlayEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;builder()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setBootstrapServers&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;kafka:9092&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setTopics&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;play-events&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setGroupId&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hot-ranking&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setValueOnlyDeserializer&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; PlayEventSchema())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 关键一步：声明用事件时间，容忍 5 分钟乱序&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// 手机离线两小时补传的数据，仍然算回&amp;#34;发生那一刻&amp;#34;的窗口&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;WatermarkStrategy&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;PlayEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; wm &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; WatermarkStrategy
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;PlayEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt;forBoundedOutOfOrderness(Duration.&lt;span style=&#34;color:#a6e22e&#34;&gt;ofMinutes&lt;/span&gt;(5))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;withTimestampAssigner&lt;/span&gt;((event, ts) &lt;span style=&#34;color:#f92672&#34;&gt;-&amp;gt;&lt;/span&gt; event.&lt;span style=&#34;color:#a6e22e&#34;&gt;getEventTime&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DataStream&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;PlayEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; events &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; env.&lt;span style=&#34;color:#a6e22e&#34;&gt;fromSource&lt;/span&gt;(source, wm, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;kafka-play-events&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;events
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;keyBy&lt;/span&gt;(PlayEvent::getVideoId)                                  &lt;span style=&#34;color:#75715e&#34;&gt;// 按视频分流&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;window&lt;/span&gt;(SlidingEventTimeWindows.&lt;span style=&#34;color:#a6e22e&#34;&gt;of&lt;/span&gt;(Time.&lt;span style=&#34;color:#a6e22e&#34;&gt;minutes&lt;/span&gt;(10), Time.&lt;span style=&#34;color:#a6e22e&#34;&gt;minutes&lt;/span&gt;(1)))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;aggregate&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; PlayCounter())                                  &lt;span style=&#34;color:#75715e&#34;&gt;// 窗口内计数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;addSink&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; RedisSink&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;());                                   &lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; 结果写 Redis&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;，&lt;/span&gt;榜单服务读&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这段代码里藏着 Flink 思维方式的全部要点：&lt;code&gt;keyBy&lt;/code&gt; 决定状态按什么分发（每个 video_id 一份计数状态），&lt;code&gt;window&lt;/code&gt; 按事件时间划分窗口而不是按到达时间，水位线声明&amp;quot;晚到 5 分钟以内的数据我等你&amp;quot;。&lt;/p&gt;
&lt;p&gt;状态的 TTL 管理（避坑指南第一条）在代码里长这样：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;StateTtlConfig ttl &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; StateTtlConfig.&lt;span style=&#34;color:#a6e22e&#34;&gt;newBuilder&lt;/span&gt;(Time.&lt;span style=&#34;color:#a6e22e&#34;&gt;days&lt;/span&gt;(7))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setUpdateType&lt;/span&gt;(StateTtlConfig.&lt;span style=&#34;color:#a6e22e&#34;&gt;UpdateType&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;OnCreateAndWrite&lt;/span&gt;)   &lt;span style=&#34;color:#75715e&#34;&gt;// 写入时刷新过期时间&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;setStateVisibility&lt;/span&gt;(StateTtlConfig.&lt;span style=&#34;color:#a6e22e&#34;&gt;StateVisibility&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NeverReturnExpired&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ValueStateDescriptor&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;Long&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; desc &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; ValueStateDescriptor&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;play-count&amp;#34;&lt;/span&gt;, Long.&lt;span style=&#34;color:#a6e22e&#34;&gt;class&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;desc.&lt;span style=&#34;color:#a6e22e&#34;&gt;enableTimeToLive&lt;/span&gt;(ttl);   &lt;span style=&#34;color:#f92672&#34;&gt;//&lt;/span&gt; 状态 7 天不活跃自动清理&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;，&lt;/span&gt;防止无限膨胀&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;部署：对应的启动命令&lt;/strong&gt;。Flink 的交付物同样是一个 fat jar，但启动命令和 Spark 有一个本质区别——&lt;strong&gt;spark-submit 起的作业算完就退出，flink run 起的作业是常驻的&lt;/strong&gt;（流处理理论上永不结束）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 提交常驻流作业到 YARN（跑在 K8s 上则换成 -t kubernetes-application）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;flink run-application &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -t yarn-application &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -c com.example.HotRankingJob &lt;span style=&#34;color:#ae81ff&#34;&gt;\ &lt;/span&gt;     &lt;span style=&#34;color:#75715e&#34;&gt;# main 入口类&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -p &lt;span style=&#34;color:#ae81ff&#34;&gt;64&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\ &lt;/span&gt;                            &lt;span style=&#34;color:#75715e&#34;&gt;# 全局并行度：占用 64 个 slot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  hot-ranking.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 升级代码或改并行度的标准动作：先打 savepoint，再从快照恢复&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;flink stop &amp;lt;jobId&amp;gt; --savepointPath s3://checkpoints/hot-ranking/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;flink run-application &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -t yarn-application &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -c com.example.HotRankingJob &lt;span style=&#34;color:#ae81ff&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -s s3://checkpoints/hot-ranking/savepoint-abc123 &lt;span style=&#34;color:#ae81ff&#34;&gt;\ &lt;/span&gt;  &lt;span style=&#34;color:#75715e&#34;&gt;# 从这份状态恢复&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  -p &lt;span style=&#34;color:#ae81ff&#34;&gt;128&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;\ &lt;/span&gt;                                            &lt;span style=&#34;color:#75715e&#34;&gt;# 并行度可以顺便改&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  hot-ranking.jar&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;对照着看两套命令的分工：Spark 用 &lt;code&gt;--num-executors / --executor-cores&lt;/code&gt; 申请资源（因为要装下一批又一批的 task）；Flink 用 &lt;code&gt;-p&lt;/code&gt; 声明并行度（因为作业结构固定，并行度就是常驻 worker 数）。而 &lt;code&gt;-s&lt;/code&gt; 恢复 savepoint 这个参数是 Flink 独有的——Spark 批作业没有&amp;quot;带着状态继续跑&amp;quot;的概念，它每次都是从头算一批新的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;生产环境谁来提交 Flink 作业？&lt;/strong&gt; 和 Spark 不完全对称——Flink 没有一个像 SparkLauncher 那样常用的官方 Java 提交 API，业界的答案按部署形态分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flink Kubernetes Operator（当前主流）&lt;/strong&gt;：作业定义成一份 &lt;code&gt;FlinkDeployment&lt;/code&gt; YAML（K8s 自定义资源），&lt;code&gt;kubectl apply&lt;/code&gt; 即提交。Operator 托管整个生命周期：拉起、监控、&lt;strong&gt;升级时自动做 savepoint 并从快照恢复&lt;/strong&gt;——上面手动敲的 &lt;code&gt;flink stop --savepointPath&lt;/code&gt; 那套动作，它全自动；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apache StreamPark（原 StreamX）&lt;/strong&gt;：作业管理平台，Web 界面管理 jar 上传、配置、启停和 savepoint，国内团队用得多；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ververica Platform&lt;/strong&gt;：Flink 创始团队公司的平台产品（有社区版）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JobManager REST API&lt;/strong&gt;：直接 HTTP 上传 jar 再触发 run——Flink Web UI 的提交走的就是它，自研平台想自己包一层时用；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;YARN 模式&lt;/strong&gt;：就是上面那条 &lt;code&gt;flink run-application&lt;/code&gt; 命令，CLI 直连，老集群常见。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;趋势很明确：&lt;strong&gt;K8s Operator 是官方主推方向&lt;/strong&gt;，新平台直接从这里起步，别再自建提交流程。&lt;/p&gt;
&lt;h2 id=&#34;血统对比一张表&#34;&gt;血统对比：一张表&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;Spark&lt;/th&gt;
					&lt;th&gt;Flink&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;/td&gt;
					&lt;td&gt;弱（流场景下才需要，能力有限）&lt;/td&gt;
					&lt;td&gt;一等公民（keyed state、TB 级）&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;数仓 SQL、MLlib、Python 数据科学&lt;/td&gt;
					&lt;td&gt;实时管道、实时数仓、CEP&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;思维负担&lt;/td&gt;
					&lt;td&gt;低：写 SQL / DataFrame 像写单机代码&lt;/td&gt;
					&lt;td&gt;高：要时刻想着时间、状态、背压&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一句话记忆：&lt;strong&gt;Spark 把流当批做，Flink 把批当流做；前者简单但延迟高，后者精确但心智负担重。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;视频公司里各干什么活&#34;&gt;视频公司里各干什么活&lt;/h2&gt;
&lt;p&gt;场景设定：一家类 Netflix 的视频公司，几亿用户，每天产生百亿级行为事件。同一条数据流，两种引擎各管一段：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;用户行为事件&amp;lt;br/&amp;gt;播放 暂停 点赞 卡顿&amp;#34;] --&amp;gt; B[&amp;#34;Kafka 事件总线&amp;#34;]
    B --&amp;gt; C[&amp;#34;Flink 实时链路&amp;lt;br/&amp;gt;秒级生效&amp;#34;]
    B --&amp;gt; D[&amp;#34;落地数仓&amp;lt;br/&amp;gt;Iceberg 数据湖&amp;#34;]
    D --&amp;gt; E[&amp;#34;Spark 离线链路&amp;lt;br/&amp;gt;小时级 天级&amp;#34;]
    C --&amp;gt; F[&amp;#34;在线服务&amp;lt;br/&amp;gt;画像 热榜 监控告警&amp;#34;]
    E --&amp;gt; G[&amp;#34;模型 报表 实验分析&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;Flink 的活——&amp;ldquo;这件事现在就要知道&amp;rdquo;：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;实时画像更新&lt;/strong&gt;：用户每看一条视频，行为事件进 Kafka，Flink 作业滚动更新&amp;quot;最近 N 次行为的加权兴趣向量&amp;quot;，秒级生效。下一刷推荐就变。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时热榜&lt;/strong&gt;：按事件时间滑动窗口统计各视频的播放增速，每分钟刷新 Trending 榜单。注意必须用事件时间——用户手机离线补传的数据不能算到&amp;quot;现在&amp;quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;播放质量监控（QoS）&lt;/strong&gt;：卡顿率、首帧时间、播放失败率按地区/运营商/客户端版本实时聚合，超过阈值就告警。某运营商骨干网抽风，值班同学三分钟内就该收到电话。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时风控&lt;/strong&gt;：同一设备十分钟内播放 200 个视频？异常刷量实时拦截，不等第二天对账。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Spark 的活——&amp;ldquo;这件事今天算完就行&amp;rdquo;：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数仓 ETL&lt;/strong&gt;：Kafka 落地到 Iceberg 的原始日志，Spark 按天清洗、去重、补维度（join 用户表、内容表），产出标准明细表。全公司数据分析的地基。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推荐模型训练&lt;/strong&gt;：拿过去 30 天的用户-内容交互矩阵，训练排序模型（交互矩阵怎么变成用户偏好，详见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/personalized-infinite-feed-architecture/&#34;&gt;推荐 Feed 架构&lt;/a&gt;里&amp;quot;用户偏好怎么算出来&amp;quot;一节）。训练要扫数据几十遍，正是 Spark 内存迭代的甜点场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;经营报表&lt;/strong&gt;：每日播放时长、新增会员、留存率——管理层早上九点要看的看板，Spark 凌晨跑批产出。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A/B 实验分析&lt;/strong&gt;：实验组对照组的指标对比、显著性检验，数据科学家写 Spark SQL + Python 完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;同一条数据的两次旅程&lt;/strong&gt;：用户晚上 8 点点开一条视频——这个事件两秒内被 Flink 消费，画像更新、热榜计数、质量监控全部生效；凌晨 2 点，同一个事件（已从 Kafka 落盘）被 Spark 的 ETL 作业清洗进数仓，早上 6 点进入模型训练数据。&lt;strong&gt;实时链路管&amp;quot;现在&amp;quot;，离线链路管&amp;quot;沉淀&amp;quot;，谁也别抢谁的活。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;算力规格硬件怎么配&#34;&gt;算力规格：硬件怎么配&lt;/h2&gt;
&lt;p&gt;先给结论再讲推理：&lt;strong&gt;大数据集群的钱主要花在内存和 SSD 上，CPU 反而不是瓶颈的主角&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spark 节点的典型规格&lt;/strong&gt;（按 2026 年的行情）：&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;Worker 节点&lt;/td&gt;
					&lt;td&gt;32-64 核 CPU、128-256GB 内存、4-8 块 NVMe SSD 做本地 shuffle 盘、10-25Gbps 网卡&lt;/td&gt;
					&lt;td&gt;shuffle 数据写本地盘，SSD 比内存更影响大 join 的稳定性&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Executor 切分&lt;/td&gt;
					&lt;td&gt;每个 executor 4-5 核、16-24GB 堆内存&lt;/td&gt;
					&lt;td&gt;一台 32 核 128GB 的机器切 5-6 个 executor；executor 太大 GC 停顿长，太小则 broadcast 等开销被重复多份&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Driver&lt;/td&gt;
					&lt;td&gt;8-16GB 内存起步&lt;/td&gt;
					&lt;td&gt;调度 + 收集元数据；大 job 的执行计划、broadcast 变量都压在这里&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Flink 节点的典型规格&lt;/strong&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;TaskManager&lt;/td&gt;
					&lt;td&gt;8-16 核、32-64GB 内存、&lt;strong&gt;本地 NVMe SSD 必须有&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;大状态用 RocksDB state backend，状态落在本地盘，SSD 的随机读写直接决定状态访问性能&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;slot 划分&lt;/td&gt;
					&lt;td&gt;slot 数 = CPU 核数（1:1 起步）&lt;/td&gt;
					&lt;td&gt;并行度不能超过总 slot 数；一个 slot 一个线程，避免 CPU 争抢&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;JobManager&lt;/td&gt;
					&lt;td&gt;4 核 8GB 起步，HA 模式至少两个&lt;/td&gt;
					&lt;td&gt;协调 checkpoint，不搬数据，轻&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;checkpoint 存储&lt;/td&gt;
					&lt;td&gt;S3 / HDFS / 对象存储&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;/strong&gt;，把数字走一遍：假设行为事件峰值 10 万条/秒、单条 1KB，实时链路（画像 + 热榜 + 监控）合计维护 500GB 状态。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;网络：10 万 × 1KB = 100MB/s，25Gbps 网卡毫无压力——&lt;strong&gt;数据管道的瓶颈很少是网络&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;状态：按单个 TaskManager 管理 64-128GB RocksDB 状态比较稳妥的经验值，500GB 状态需要 4-8 台 TaskManager；&lt;/li&gt;
&lt;li&gt;CPU：窗口聚合类作业每条事件的计算很轻，10 万条/秒摊到 64 个核上每核每秒 1500 条，富余；&lt;/li&gt;
&lt;li&gt;所以这套链路 8 台&amp;quot;16 核 64GB + NVMe&amp;quot;的机器跑得很从容，再留 30% 峰值冗余即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;反过来，凌晨的 Spark 训练作业要扫 30 天 × 百 TB 级日志，它的瓶颈是 &lt;strong&gt;shuffle 吞吐和内存&lt;/strong&gt;，机器数量按&amp;quot;凌晨 6 点前必须跑完&amp;quot;倒推——离线集群的规格是被截止时间定义的。&lt;/p&gt;
&lt;h2 id=&#34;监控两套引擎各看什么&#34;&gt;监控：两套引擎各看什么&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Spark 监控的主入口是 Spark UI&lt;/strong&gt;（每个作业一个 Web 界面），配合 Prometheus 抓指标。值班时按这个顺序看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stages 页&lt;/strong&gt;：哪个 stage 慢、shuffle read/write 多大——shuffle spill（内存放不下溢写磁盘）是性能第一警报；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tasks 页的任务时长分布&lt;/strong&gt;：p50 是 2 分钟、max 是 40 分钟 → 数据倾斜，不用怀疑；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Executors 页&lt;/strong&gt;：GC 时间占比超过 10% 说明内存配置或数据结构有问题；executor lost 频繁出现说明节点或内存超卖；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Event Log&lt;/strong&gt; 持久化到 HDFS/S3，作业挂了也能回放分析——务必开启，默认不一定开。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Flink 监控的主入口是 Web UI + 指标上报&lt;/strong&gt;（Prometheus reporter），核心看四组：&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;backPressuredTimeMsPerSecond&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;哪个算子被顶住&lt;/td&gt;
					&lt;td&gt;持续 &amp;gt;50% 就要查&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;checkpoint &lt;code&gt;lastCheckpointDuration&lt;/code&gt; / &lt;code&gt;lastCheckpointSize&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;Kafka 消费 lag（&lt;code&gt;records-lag&lt;/code&gt;）&lt;/td&gt;
					&lt;td&gt;消费追不上生产 = 延迟在堆积&lt;/td&gt;
					&lt;td&gt;持续增长 10 分钟告警&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;&lt;strong&gt;告警的验收标准只有一条：每个告警都必须对应一个动作。&lt;/strong&gt;&amp;ldquo;CPU 高&amp;quot;不是告警，&amp;ldquo;热榜作业 Kafka lag 连续增长 10 分钟&amp;quot;才是——前者看了也不知道干嘛，后者直接指向扩容或查背压。&lt;/p&gt;
&lt;h2 id=&#34;故障与恢复出事时的标准动作&#34;&gt;故障与恢复：出事时的标准动作&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Spark 的故障模型是&amp;quot;重算&amp;rdquo;&lt;/strong&gt;，血统里带着 MapReduce 的基因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;task 失败 → 自动重试（默认 4 次，&lt;code&gt;spark.task.maxFailures&lt;/code&gt; 可调），重试时只重算这个分区；&lt;/li&gt;
&lt;li&gt;executor 挂了 → 它算过的分区&lt;strong&gt;按 lineage（血统）重新算&lt;/strong&gt;——RDD 天生记得自己从哪算来的，这就是当年 RDD 论文的核心思想；&lt;/li&gt;
&lt;li&gt;个别 task 特别慢（straggler）→ 开推测执行（&lt;code&gt;spark.speculation=true&lt;/code&gt;），同样的任务在别的节点再跑一份，谁先完用谁；&lt;/li&gt;
&lt;li&gt;driver 挂了 → 最麻烦，批作业整体重跑；所以重要的 ETL 要幂等（写分区用 overwrite 而不是 append），重跑不出脏数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Flink 的故障模型是&amp;quot;回滚&amp;rdquo;&lt;/strong&gt;，回到最近一次 checkpoint：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TaskManager 挂了 → 作业自动从最近的成功 checkpoint 恢复，Kafka source 回到快照时记录的 offset 重新消费，配合两阶段提交的 sink 保证恰好一次；&lt;/li&gt;
&lt;li&gt;重启策略别用默认：生产上推荐 &lt;code&gt;failure-rate&lt;/code&gt;（10 分钟内允许失败 N 次，超过才放弃）配合重启间隔，防止依赖的 Kafka/Redis 抖动时作业无脑高频重启把对方打垮；&lt;/li&gt;
&lt;li&gt;JobManager 挂 → 必须配 HA（ZooKeeper 或 K8s 选主），否则协调者单点；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;checkpoint 是自动的、savepoint 是手动的&lt;/strong&gt;：代码升级、改并行度必须先做 savepoint 再停作业，从 savepoint 启动新版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;一个标准故障处置剧本&lt;/strong&gt;（实时画像更新作业 lag 暴涨）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;看 Flink UI 背压页——找到链条上第一个变红的算子；&lt;/li&gt;
&lt;li&gt;是某个下游慢（比如 Redis 抖动）→ 修下游，作业自己追上来；&lt;/li&gt;
&lt;li&gt;是数据洪峰 → 临时调大并行度（savepoint → 重启 → 恢复），或者干脆等洪峰过去，Kafka 里的数据丢不了；&lt;/li&gt;
&lt;li&gt;恢复后盯 Kafka lag 回落到正常水位，确认 checkpoint 恢复绿色；&lt;/li&gt;
&lt;li&gt;事后复盘：这次故障该变成哪条告警规则？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;注意底气来自哪里：&lt;strong&gt;Kafka 保留了原始事件，Flink 保留了状态快照&lt;/strong&gt;——前者保证&amp;quot;数据丢不了&amp;quot;，后者保证&amp;quot;进度丢不了&amp;quot;。两个都在，故障就只是插曲不是事故。&lt;/p&gt;
&lt;h3 id=&#34;kafka--flink-组合失败后到底会不会重复会不会丢&#34;&gt;Kafka + Flink 组合：失败后到底会不会重复、会不会丢&lt;/h3&gt;
&lt;p&gt;这是面试和值班都绕不开的问题，拆开两个角色看就清楚了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kafka 侧：消息几乎不可能&amp;quot;被消费掉&amp;quot;。&lt;/strong&gt; Kafka 是持久化日志，消费&lt;strong&gt;不删除&lt;/strong&gt;数据，保留期（比如 7 天）内随便重读。Flink 的 Kafka source 消费时不逐条 ack，只在 checkpoint 里记录各分区的 offset。所以消息丢失只有两种可能：保留期过了，或者 Kafka 集群自己故障。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flink 侧：重启 = 状态回滚 + offset 回拨，两件事同步发生。&lt;/strong&gt; 故障后从最近一次成功的 checkpoint 恢复，同一时刻两个动作：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;故障发生&amp;lt;br/&amp;gt;部分事件已消费已计算&amp;#34;] --&amp;gt; B[&amp;#34;从最近 checkpoint 恢复&amp;#34;]
    B --&amp;gt; C[&amp;#34;Kafka offset 回拨&amp;lt;br/&amp;gt;到快照时刻&amp;#34;]
    B --&amp;gt; D[&amp;#34;算子状态回滚&amp;lt;br/&amp;gt;到快照时刻&amp;#34;]
    C --&amp;gt; E[&amp;#34;快照之后的事件重新消费&amp;#34;]
    D --&amp;gt; E
    E --&amp;gt; F[&amp;#34;在回滚后的状态上重算&amp;lt;br/&amp;gt;内部不多算也不少算&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;假设 checkpoint 间隔 60 秒：故障意味着&lt;strong&gt;最多重放最近 60 秒的事件&lt;/strong&gt;——重放不是丢失，是进度倒退后追赶。由于状态和 offset 同步回滚，窗口计数这类状态计算在内部是精确的（exactly-once）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;但&amp;quot;对外不重复&amp;quot;取决于 sink，这是最容易踩的边界：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;sink 类型&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;两阶段提交 sink&lt;/td&gt;
					&lt;td&gt;exactly-once&lt;/td&gt;
					&lt;td&gt;Kafka sink（事务）、Iceberg&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;幂等写入&lt;/td&gt;
					&lt;td&gt;等效 exactly-once&lt;/td&gt;
					&lt;td&gt;Redis 按 key 覆盖写、MySQL upsert&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;普通追加&lt;/td&gt;
					&lt;td&gt;at-least-once，&lt;strong&gt;会重复&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;直接 insert 进 MySQL、对外发 HTTP 回调&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;重放的那 60 秒里，如果 sink 是普通 insert，下游表里就会出现重复行——作业内部算得再精确，写出去的那一刻语义就降级了。这就是&amp;quot;exactly-once 只在链路两端都配合时才成立&amp;quot;的含义。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;真正会丢、会重的边界情况清单：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;没开 checkpoint&lt;/strong&gt;：offset 只能周期性提交到 Kafka，重启从提交点读——退化成 at-least-once，且恢复语义模糊；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kafka 保留期 &amp;lt; 故障停机时间&lt;/strong&gt;：作业停 8 天、Kafka 只留 7 天，恢复时最早的数据真没了；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步 IO 和侧输出不参与 checkpoint&lt;/strong&gt;：算子里偷偷调外部接口、写侧输出流，这些动作不在快照里，重放时会重复执行；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;savepoint 与升级不兼容&lt;/strong&gt;：改了状态结构（比如给状态类加了字段）还想从旧 savepoint 恢复，直接恢复失败——升级前想清楚状态兼容性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;多节点会不会重复消费同一个分区消费组的互斥语义&#34;&gt;多节点会不会重复消费同一个分区：消费组的互斥语义&lt;/h3&gt;
&lt;p&gt;顺带的另一个常见疑问：Flink 作业有几十个 source subtask 在多个节点上跑，会不会两个节点同时读同一个 Kafka 分区、把数据算两遍？&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不会，这是 Kafka 消费组协议内建的互斥保证，不需要你做任何事：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一作业的所有 source subtask 用同一个 &lt;code&gt;group.id&lt;/code&gt; 注册，Kafka 的组协调器把分区&lt;strong&gt;互斥分配&lt;/strong&gt;——每个分区同一时刻只属于组内一个消费者。200 个分区、64 个 subtask，每个 subtask 分 3-4 个，互不重叠；&lt;/li&gt;
&lt;li&gt;节点挂了触发 rebalance，分区重新分配，但交接是&lt;strong&gt;排他的&lt;/strong&gt;：旧持有者被踢出组之后，新持有者才接手，不存在两个节点同时持有同一分区的窗口；&lt;/li&gt;
&lt;li&gt;所以同一作业内&amp;quot;重复消费&amp;quot;只有一种情况：故障恢复时的&lt;strong&gt;时间维度重放&lt;/strong&gt;（上一节讲的回滚），不存在&amp;quot;空间维度&amp;quot;的并行重复。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个相关的配置纪律：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;source 并行度不要超过分区数&lt;/strong&gt;。并行度 64、分区只有 32 个时，32 个 subtask 永远分不到分区、空转白占资源。反过来分区数多于并行度没问题，一个 subtask 可以吃多个分区。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不同作业必须用不同的 &lt;code&gt;group.id&lt;/code&gt;&lt;/strong&gt;。消费组之间是广播语义——画像作业和热榜作业各自完整消费同一个 topic，互不干扰，这是特性。但两个本该独立的作业如果误配成同一个 group.id，就会&lt;strong&gt;瓜分分区、各自只读到一半数据&lt;/strong&gt;，这种 bug 症状隐蔽（数据看着都在流，就是量不对），值得配进上线检查清单。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;数据源不止-kafka还能从哪读api-数据怎么办&#34;&gt;数据源不止 Kafka：还能从哪读，API 数据怎么办&lt;/h3&gt;
&lt;p&gt;Flink 的 source 生态按类别列全：&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;Kafka、Pulsar、Kinesis、RocketMQ（社区）&lt;/td&gt;
					&lt;td&gt;实时事件流的主力入口&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据库 CDC&lt;/td&gt;
					&lt;td&gt;MySQL CDC、PostgreSQL CDC、MongoDB CDC（Flink CDC 项目）&lt;/td&gt;
					&lt;td&gt;直接订阅 binlog，数据库变更实时变成流——替代&amp;quot;定时全量导库&amp;quot;，是这几年增量最大的方向&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;文件/对象存储&lt;/td&gt;
					&lt;td&gt;FileSource（S3 / HDFS / 本地），支持批读和持续监听目录&lt;/td&gt;
					&lt;td&gt;批处理、日志文件实时采集&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;JDBC&lt;/td&gt;
					&lt;td&gt;Table API 的 JDBC connector&lt;/td&gt;
					&lt;td&gt;轮询式读关系库，伪实时&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;测试&lt;/td&gt;
					&lt;td&gt;DataGen 数据生成器、socket 流&lt;/td&gt;
					&lt;td&gt;本地开发与压测&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;HTTP API 提供的数据怎么办？&lt;/strong&gt; 核心发行版里没有内置的 HTTP source，分两种情况：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况一：API 是数据源（想持续消费一个 REST 接口）&lt;/strong&gt;。正确姿势不是让 Flink 直接轮询 API，而是&lt;strong&gt;中间加一层 Kafka&lt;/strong&gt;：一个轻量采集服务定时调 API、把结果写进 Kafka，Flink 消费 Kafka。原因正是前两节的内容——Kafka 提供可回放、削峰、多消费者，API 直连则什么都没有：Flink 作业一挂，这段时间的 API 数据就永远丢了。非要直连也行：实现自定义 Source 接口自己轮询，但 offset 管理、故障恢复语义全得自己做，一般不值。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;情况二：API 是维表（事件流经过时补充字段）&lt;/strong&gt;。这是更常见的需求：每条事件只有 &lt;code&gt;video_id&lt;/code&gt;，需要调内容服务拿标题、类目、时长。Flink 有标准件——&lt;strong&gt;异步 IO（Async I/O）&lt;/strong&gt;：在算子里并发调用 HTTP 接口、回调组装结果，不阻塞主流；Table API 里对应的写法是 lookup join。配一层本地缓存（Guava/Caffeine）挡掉热点 key 的重复调用，避免把对方服务打挂。注意它的语义是&amp;quot;查询&amp;quot;不是&amp;quot;消费&amp;quot;——只在事件流经过时被动调用，永远不会主动拉数据。&lt;/p&gt;
&lt;h2 id=&#34;spark-避坑指南&#34;&gt;Spark 避坑指南&lt;/h2&gt;
&lt;p&gt;按踩坑频率排序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;数据倾斜是第一杀手&lt;/strong&gt;。某个 key 的数据量特别大（比如头部视频占了 10% 播放量），groupBy/join 时对应分区要处理别人几十倍的数据，99% 的任务十分钟跑完，最后 1% 卡两小时。解法：给热点 key 加盐（key 后面拼随机后缀打散）、先聚合再关联、小表广播（broadcast join）避免 shuffle。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小文件问题&lt;/strong&gt;。ETL 作业输出几万个几 KB 的小文件，下游读取慢、元数据服务被打爆。解法：写出前 &lt;code&gt;coalesce&lt;/code&gt;/&lt;code&gt;repartition&lt;/code&gt; 控制分区数，或开自适应合并。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;shuffle 爆炸&lt;/strong&gt;。join 两张大表，中间数据量是笛卡尔级增长。先看能不能把一张表过滤小再 join，能 broadcast 就 broadcast。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;滥用 cache&lt;/strong&gt;。&lt;code&gt;.cache()&lt;/code&gt; 不是免费的，内存不够会挤占执行内存反而更慢。只缓存&amp;quot;会被复用多次&amp;quot;的中间结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分区数拍脑袋&lt;/strong&gt;。分区太少 → 单任务处理量太大 OOM；太多 → 任务调度开销压垮 driver。经验值：单个分区处理 128MB 上下。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;driver OOM&lt;/strong&gt;。&lt;code&gt;collect()&lt;/code&gt; 把全量数据拉回 driver——生产事故的经典配方。要看几行数据用 &lt;code&gt;show()&lt;/code&gt;/&lt;code&gt;take()&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;flink-避坑指南&#34;&gt;Flink 避坑指南&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;状态不设 TTL 无限膨胀&lt;/strong&gt;。keyed state 默认永不过期，跑一个月状态涨到 TB 级，checkpoint 从秒级拖到分钟级最后超时失败。用户维度状态几乎总该设 TTL（比如 7 天）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;checkpoint 间隔拍太短&lt;/strong&gt;。间隔小于一次 checkpoint 本身的耗时，系统永远在做快照没有空算数据。先看一次 checkpoint 要多久，间隔至少留它几倍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;背压不会看&lt;/strong&gt;。作业 lag 持续上涨，先去 UI 看背压面板：红色背压的算子就是瓶颈，瓶颈的上游全会被&amp;quot;传染&amp;quot;成红色——&lt;strong&gt;找到链条上第一个变红的，那才是真正的病灶&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;事件时间和处理时间混用&lt;/strong&gt;。用处理时间算&amp;quot;每分钟播放量&amp;quot;，用户手机补传的数据会把昨天的量记到今天。涉及&amp;quot;按发生时间统计&amp;quot;的需求，必须事件时间 + 水位线，并接受乱序容忍度的权衡。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;扩缩容当成无状态服务做&lt;/strong&gt;。改并行度 = savepoint + 停作业 + 重启，大状态作业分钟级中断。扩缩容策略要保守，这个坑的完整故事见 &lt;a href=&#34;https://ftmao.com/blog/blog/posts/flink-autoscaling-netflix-two-autoscalers/&#34;&gt;Netflix 两代 autoscaler&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;exactly-once 当默认值&lt;/strong&gt;。端到端 exactly-once 要求 sink 支持两阶段提交，代价是吞吐下降。很多监控类场景 at-least-once + 下游幂等就够了，别为不需要的语义付费。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;通用坑：用 Spark 微批硬扛秒级需求&lt;/strong&gt;。需求方说&amp;quot;准实时&amp;quot;，你拿 Structured Streaming 把触发间隔压到秒级——能跑，但状态管理和乱序处理的坑会一个个冒出来。先问清楚：延迟到底是秒级还是分钟级需求？分钟级 Spark 很舒服，秒级请直接上 Flink，别硬扛。&lt;/p&gt;
&lt;h2 id=&#34;选型决策&#34;&gt;选型决策&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;数据是有界的、延迟容忍分钟级以上 → Spark
数据是持续流、延迟要求秒级、或需要复杂状态/事件时间 → Flink&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;但必须诚实：&lt;strong&gt;边界在模糊化&lt;/strong&gt;。Spark 能做流、Flink 能做批，两个社区都在往对方的领地走；Flink 的 Table API 写 SQL 也越来越像 Spark SQL。真实的选型经常是这样决定的：团队现有栈是什么、招聘市场上谁会、云厂商托管服务谁更便宜。技术血统决定的是&amp;quot;哪个更顺&amp;quot;，不是&amp;quot;哪个能做&amp;quot;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;国内的实际格局&lt;/strong&gt;（2020 年代中期的现状，供校准）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;新上实时项目基本是 Flink&lt;/strong&gt;。阿里 2019 年收购 Flink 母公司后全力主推（内部版 Blink 撑双 11 实时大屏），字节、美团、腾讯、快手的实时平台全部 Flink 化，新立项用 Spark Streaming 的已很难见到；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存量离线仍是 Spark 的天下&lt;/strong&gt;：数仓 ETL、T+1 报表、Hive 承接，生态太厚，替换动力不足；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两边都在被挤压&lt;/strong&gt;：Spark 的机器学习场景在流失到 Python 原生生态（PyTorch/Ray）；Flink 在进攻离线——&amp;ldquo;流批一体&amp;rdquo;（一套 SQL 跑流也跑批）配合 Paimon 这类流式湖格式在落地，但离线存量太重，替换以年计；&lt;/li&gt;
&lt;li&gt;一句话：&lt;strong&gt;新实时 = Flink，存量离线 = Spark，交界处（实时数仓）在打架&lt;/strong&gt;。求职市场同构：实时岗位 Flink 是硬通货，离线岗位 Spark/Hive 是基础盘。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一句话收拢：&lt;strong&gt;Spark 回答&amp;quot;这批数据算出来是什么&amp;quot;，Flink 回答&amp;quot;这件事现在发生了什么&amp;quot;&lt;/strong&gt;；先想清楚你的问题是哪一种，再选工具——反过来拿着工具定义问题，是大数据平台翻车的最常见起点。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考：Apache Spark 与 Apache Flink 官方文档；Structured Streaming 与 Flink DataStream 的设计文档。Netflix 场景参考其技术博客公开的数据平台架构。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>千人千面 &#43; 无限瀑布流：推荐 Feed 的完整实现架构</title>
      <link>https://ftmao.com/blog/posts/personalized-infinite-feed-architecture/</link>
      <pubDate>Fri, 02 Oct 2026 18:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/personalized-infinite-feed-architecture/</guid>
      <description>&lt;h1 id=&#34;千人千面--无限瀑布流推荐-feed-的完整实现架构&#34;&gt;千人千面 + 无限瀑布流：推荐 Feed 的完整实现架构&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：三类人——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;后端工程师&lt;/strong&gt;：要实现一个推荐 Feed，想知道每个组件的职责边界（看&amp;quot;召回/粗排/精排/重排&amp;quot;四章和&amp;quot;离线 vs 在线分工总表&amp;quot;）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架构师&lt;/strong&gt;：在做 Feed 系统选型，需要 push/pull、offset/cursor 这些关键决策的对比依据（看各节的对比表）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;前端/客户端工程师&lt;/strong&gt;：关心无限滚动的分页协议和预取时机（看&amp;quot;游标分页&amp;quot;和&amp;quot;预取&amp;quot;两节）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;阅读前提：不需要推荐算法背景，算法部分只讲工程接口，不讲模型内部。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上一篇讲 Netflix 的 Flink 扩缩容时提过一个三层结构：离线训练、近线特征、在线排序。有读者问：**首页千人千面，是每个用户打开时都跑一遍计算吗？**答案是否定的，而完整的答案就是这篇文章的主题——把微博、X 那种&amp;quot;每个人看到的都不一样、而且永远拉不到头&amp;quot;的推荐瀑布流，从客户端一路拆到数据闭环。&lt;/p&gt;
&lt;p&gt;先把两个需求拆开，因为它们的难点完全不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;千人千面&lt;/strong&gt;：难在&amp;quot;算&amp;quot;——几亿用户每人一套候选，怎么做到几十毫秒出结果；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无限瀑布流&lt;/strong&gt;：难在&amp;quot;协议&amp;quot;——内容在不断新增、排序在不断变化，怎么保证用户连续下拉时不重复、不漏看、永远有货。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;总体架构重活在用户到来之前干完&#34;&gt;总体架构：重活在用户到来之前干完&lt;/h2&gt;
&lt;p&gt;整个系统的核心思想只有一句话：&lt;strong&gt;请求路径上只做轻活，所有重计算都提前发生在事件流和离线任务里&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph 提前计算[&amp;#34;提前计算（用户到来之前）&amp;#34;]
        A[&amp;#34;离线层&amp;lt;br/&amp;gt;批量训练模型&amp;lt;br/&amp;gt;计算全量候选池&amp;#34;] --&amp;gt; D[&amp;#34;在线存储&amp;lt;br/&amp;gt;画像 特征 候选集&amp;#34;]
        B[&amp;#34;近线层&amp;lt;br/&amp;gt;行为事件经 Kafka&amp;lt;br/&amp;gt;Flink 实时更新画像特征&amp;#34;] --&amp;gt; D
    end
    subgraph 请求路径[&amp;#34;请求路径（用户下拉这一刻）&amp;#34;]
        C[&amp;#34;在线服务&amp;lt;br/&amp;gt;召回 排序 组装&amp;lt;br/&amp;gt;几十毫秒返回&amp;#34;] --&amp;gt; E[&amp;#34;客户端&amp;lt;br/&amp;gt;渲染一屏&amp;#34;]
    end
    D --&amp;gt; C
    E -.-&amp;gt;|&amp;#34;曝光 点击 停留事件&amp;#34;| B&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;离线层&lt;/strong&gt;（小时/天级）：Spark 类批处理训练推荐模型，算出全量候选——比如&amp;quot;每个用户最可能感兴趣的一万条内容&amp;quot;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;近线层&lt;/strong&gt;（秒级）：用户的每一次曝光、点击、停留、点赞都作为事件进 Kafka，流处理作业（如 Flink）实时更新用户画像和特征。你上一次刷的行为，下一刷就生效——&amp;ldquo;越刷越懂你&amp;quot;的体感来自这一层；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在线层&lt;/strong&gt;（几十毫秒）：你下拉刷新这一刻，服务只做&amp;quot;取候选 → 排序 → 组装一页&amp;rdquo;，所有重活的结果早已躺在在线存储里。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这三层 Netflix 在 2013 年就公开过（offline/nearline/online 的经典划分），至今仍是推荐系统的主流形态。上一篇 &lt;a href=&#34;https://ftmao.com/blog/blog/posts/flink-autoscaling-netflix-two-autoscalers/&#34;&gt;Flink 自动扩缩容&lt;/a&gt;里 Netflix 的&amp;quot;个性化自定义作业&amp;quot;，干的就是近线层的活。&lt;/p&gt;
&lt;h2 id=&#34;为什么是这个分工kafkasparkflink-各自的角色&#34;&gt;为什么是这个分工：Kafka、Spark、Flink 各自的角色&lt;/h2&gt;
&lt;p&gt;架构图里三个组件的角色容易被误解，值得单独说清。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Kafka 不是&amp;quot;记录数据&amp;quot;的，是事件总线。&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;strong&gt;削峰&lt;/strong&gt;：曝光事件的洪峰来临时，消费端可以按自己的节奏追，消息堆在 Kafka 里不会丢；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可回放&lt;/strong&gt;：特征算错了、逻辑改了，把昨天的事件流重新放一遍就能重算——消息队列变成时间机器；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多消费者&lt;/strong&gt;：同一份事件流同时喂给画像更新（Flink）、数仓落盘、实时监控，各取所需互不干扰。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;没有它的替代方案是客户端打点直连 Flink——Flink 一挂数据就丢。Kafka 是缓冲带加保险箱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spark 和 Flink 看着像，血统不同：&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;Spark&lt;/th&gt;
					&lt;th&gt;Flink&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;Structured Streaming 是微批（攒一批算一次）&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;/td&gt;
					&lt;td&gt;数仓（Hive/Iceberg）、MLlib、Python 数据科学工具链&lt;/td&gt;
					&lt;td&gt;有状态流计算：keyed state、事件时间水位线、exactly-once&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;训练用 Spark，因为它是吞吐型任务&lt;/strong&gt;：几 TB 历史数据扫几遍、小时级跑完即可，不在乎单条延迟；而且机器学习工具链都在它这边。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;画像更新用 Flink，因为近线层的 KPI 是&amp;quot;行为发生后多久生效&amp;quot;&lt;/strong&gt;：要求秒级，且要维护滚动状态（最近 N 次行为的加权向量）——逐条处理加状态管理正是 Flink 的主场。用 Spark 微批也能跑，代价是生效延迟从秒变分钟，&amp;ldquo;越刷越懂你&amp;quot;的体感就钝了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要诚实补一句：&lt;strong&gt;边界在模糊化&lt;/strong&gt;。Spark 能流、Flink 能批，用 Spark Streaming 做近线的公司也不少。这个分工一半是技术判断，一半是生态和组织惯性——团队的历史栈是什么，往往和&amp;quot;哪个理论最优&amp;quot;同等重要。&lt;/p&gt;
&lt;h2 id=&#34;用户偏好怎么算出来交互矩阵与-embedding&#34;&gt;用户偏好怎么算出来：交互矩阵与 embedding&lt;/h2&gt;
&lt;p&gt;讲四层漏斗之前，先看&amp;quot;偏好&amp;quot;这个东西在系统里的物理形态——它决定了每层能用什么数据。&lt;/p&gt;
&lt;p&gt;一切从一张&lt;strong&gt;用户-物品交互矩阵&lt;/strong&gt;开始：行是用户，列是内容（视频、微博、商品），格子里的值是行为强度。显式反馈（评分、点赞）稀疏但明确；隐式反馈（点击、停留时长、完播、划走速度）量大但噪声多——工业界主要靠后者。&lt;/p&gt;
&lt;p&gt;矩阵太大太稀疏（几亿用户 × 几亿内容，99.99% 是空格），没法直接用，于是有了两条经典技术路线，都是十多年前就成熟的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;矩阵分解（Matrix Factorization）&lt;/strong&gt;：把大矩阵分解成两个小矩阵——每个用户一个向量、每个物品一个向量，维度几十到几百。用户对物品的偏好 ≈ 两个向量的点积。2006-2009 年 Netflix Prize 竞赛把它打成主流，用 ALS（交替最小二乘）在集群上批量求解。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;embedding（嵌入向量）&lt;/strong&gt;：矩阵分解的现代化身。item2vec（2016）把物品当&amp;quot;词&amp;rdquo;、用户行为序列当&amp;quot;句子&amp;quot;套 word2vec；双塔模型（DSSM 系）用两个神经网络分别把用户和物品映射到同一个向量空间。&lt;strong&gt;殊途同归：最终产物都是&amp;quot;每个用户一个向量、每个物品一个向量，点积或余弦相似度就是偏好分&amp;quot;&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;：物品向量（内容发布后批量算一次，更新频率低）、用户长期兴趣向量（按天/小时批量更新）、物品相似度矩阵；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;近线算&lt;/strong&gt;：用户短期兴趣向量——最常见的实现是把用户最近 N 次行为对应的物品向量做&lt;strong&gt;加权平均&lt;/strong&gt;（行为类型加权：完播 &amp;gt; 点赞 &amp;gt; 点击 &amp;gt; 秒划过；时间衰减：越久的行为权重越低）。Flink 作业消费行为事件、滚动更新这个向量，秒级生效；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在线算&lt;/strong&gt;：只剩查表和点积——拿用户向量去向量库里找最近邻，或用画像特征给候选打分。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是说，&lt;strong&gt;&amp;ldquo;用户偏好&amp;quot;在请求到来前就已经被压缩成几百维的向量躺在存储里了&lt;/strong&gt;。下面四层漏斗里所有&amp;quot;懂用户&amp;quot;的操作，本质都是在读这份预先算好的压缩包。&lt;/p&gt;
&lt;h2 id=&#34;四层排序漏斗总览&#34;&gt;四层排序漏斗总览&lt;/h2&gt;
&lt;p&gt;在线服务收到一次下拉请求，要在几十毫秒内从海量内容里选出这一屏的 20 条。对全量内容逐一打分是不可能的，工程上的标准解法是&lt;strong&gt;漏斗&lt;/strong&gt;：每一层用更贵的模型处理更少的候选。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;召回&amp;lt;br/&amp;gt;万级候选&amp;lt;br/&amp;gt;多路召回： 协同过滤 向量 热门 关注&amp;#34;] --&amp;gt; B[&amp;#34;粗排&amp;lt;br/&amp;gt;千级&amp;lt;br/&amp;gt;轻量模型快速打分&amp;#34;]
    B --&amp;gt; C[&amp;#34;精排&amp;lt;br/&amp;gt;百级&amp;lt;br/&amp;gt;深度排序模型融合全部特征&amp;#34;]
    C --&amp;gt; D[&amp;#34;重排 混排&amp;lt;br/&amp;gt;二十条一页&amp;lt;br/&amp;gt;多样性打散 插入广告 业务规则&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;先说清一个术语问题：&lt;strong&gt;这套漏斗的主力技术是深度学习排序模型，不是大语言模型（LLM）&lt;/strong&gt;。协同过滤 2000 年代就有，矩阵分解是 2009 年 Netflix Prize 的明星，GBDT+LR 是 Facebook 2014 年的论文，Wide &amp;amp; Deep 是 Google 2016 年的——整个栈在十年前就完整存在。LLM 近两年开始进入推荐链路（内容理解、生成标签、甚至生成式召回），但在线排序的主力依然是经典深度模型。别把&amp;quot;推荐系统&amp;quot;和&amp;quot;大模型&amp;quot;混为一谈。&lt;/p&gt;
&lt;h2 id=&#34;召回离线算相似度在线做检索&#34;&gt;召回：离线算相似度，在线做检索&lt;/h2&gt;
&lt;p&gt;任务：从几亿内容里捞出万级候选。关键词是&lt;strong&gt;覆盖率&lt;/strong&gt;——宁多勿漏，漏了的后面几层救不回来。&lt;/p&gt;
&lt;p&gt;召回是&amp;quot;多路并行&amp;quot;的，每一路是独立的候选来源，技术路径和离线/在线分工各不相同：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一路：协同过滤（itemCF 为主）&lt;/strong&gt;。逻辑是&amp;quot;和你喜欢过的东西相似的东西&amp;rdquo;。物品两两之间的相似度由共现矩阵算出（喜欢 A 的人也多大比例喜欢 B，余弦相似度），这个矩阵&lt;strong&gt;全离线算&lt;/strong&gt;，按天或按小时更新，结果存 KV：&lt;code&gt;item_id → 相似物品 TopK 列表&lt;/code&gt;。在线时只做一件事：拿用户最近行为过的物品列表去 KV 里批量查，合并候选。&lt;strong&gt;在线路径上没有任何矩阵运算，全是查表。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二路：向量召回（双塔 + ANN 检索）&lt;/strong&gt;。物品向量离线批量算好，灌进向量索引库（Faiss、HNSW 这类近似最近邻索引，百万级向量检索只要几毫秒）；用户向量由近线层滚动更新。在线时拿用户向量在索引里查 TopK——这一步是召回里唯一称得上&amp;quot;在线计算&amp;quot;的环节，但它也只是一次&lt;strong&gt;索引检索&lt;/strong&gt;，不是模型推理（双塔的用户塔也可以在线跑一次，把实时上下文特征算进去，看延迟预算）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第三路：运营类候选&lt;/strong&gt;。热门榜、新内容池、同城/地理围栏、关注关系产出——这些本质是&lt;strong&gt;离线或近线算好的榜单&lt;/strong&gt;，在线直接读。它们的作用不是精准，而是兜底和探索：新用户没有画像（冷启动）时，热门榜就是全部；老用户的候选里也固定掺一部分，保证 Feed 有新鲜感。&lt;/p&gt;
&lt;p&gt;多路结果合并、去重、按路配额截断，凑齐万级候选交给下一层。&lt;/p&gt;
&lt;h2 id=&#34;粗排夹在中间的性价比层&#34;&gt;粗排：夹在中间的性价比层&lt;/h2&gt;
&lt;p&gt;任务：万级 → 千级。它存在的唯一理由是&lt;strong&gt;精排太贵&lt;/strong&gt;——精排模型打一万条候选，延迟和算力都吃不消，但召回结果直接用又太糙。&lt;/p&gt;
&lt;p&gt;粗排的模型选型讲究&amp;quot;够用就好&amp;quot;：早年用 LR（逻辑回归）或 GBDT，特征只取最强的一批（物品热度、用户类目偏好分、历史 CTR）；近年常用&lt;strong&gt;简化版双塔&lt;/strong&gt;——向量点积打分，几乎就是向量召回的打分器复用。特征从特征存储里批量读，模型在线跑，但都是毫秒级的小模型。&lt;/p&gt;
&lt;p&gt;工程上粗排是可选层：候选量小的业务（百万级内容池）召回直送精排也扛得住；亿级内容池才必须用它护住精排的延迟。&lt;/p&gt;
&lt;h2 id=&#34;精排深度学习但不是大模型&#34;&gt;精排：深度学习，但不是大模型&lt;/h2&gt;
&lt;p&gt;任务：对千级候选逐条打分，预测&amp;quot;这个用户看到这条内容，会不会点、会看多久&amp;quot;。这是&amp;quot;千人千面&amp;quot;真正被算出来的地方，也是算法团队的主战场。&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;2010 前后&lt;/td&gt;
					&lt;td&gt;LR&lt;/td&gt;
					&lt;td&gt;基线：特征加权和，可解释、可工程化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2012&lt;/td&gt;
					&lt;td&gt;FM / FFM&lt;/td&gt;
					&lt;td&gt;自动学习特征两两交叉（&amp;ldquo;男性 × 游戏类&amp;quot;这种组合特征不用人工配）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2014&lt;/td&gt;
					&lt;td&gt;GBDT + LR（Facebook）&lt;/td&gt;
					&lt;td&gt;GBDT 自动做特征组合和离散化，输出喂给 LR&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2016&lt;/td&gt;
					&lt;td&gt;Wide &amp;amp; Deep（Google）&lt;/td&gt;
					&lt;td&gt;Wide 侧记忆规则（&amp;ldquo;已安装的应用&amp;rdquo;），Deep 侧泛化（embedding 学没见过组合）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2017-2018&lt;/td&gt;
					&lt;td&gt;DeepFM / DIN（阿里）&lt;/td&gt;
					&lt;td&gt;DeepFM 把 FM 嵌进网络；DIN 引入注意力——给用户历史行为按&amp;quot;和当前候选的相关性&amp;quot;加权，你刷运动鞋时，你上周看鞋的权重就压过看手机的&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;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;候选列表 千级&amp;#34;] --&amp;gt; B[&amp;#34;特征拼接&amp;lt;br/&amp;gt;用户画像 物品特征&amp;lt;br/&amp;gt;实时上下文: 时间 设备 网络&amp;#34;]
    B --&amp;gt; C[&amp;#34;特征存储 Feature Store&amp;lt;br/&amp;gt;离线特征读 KV&amp;lt;br/&amp;gt;近线特征读 Flink 产出&amp;#34;]
    C --&amp;gt; D[&amp;#34;深度排序模型&amp;lt;br/&amp;gt;批量推理 输出 CTR/时长预测&amp;#34;]
    D --&amp;gt; E[&amp;#34;按预估分排序&amp;lt;br/&amp;gt;截断到百级&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键组件是&lt;strong&gt;特征存储（Feature Store）&lt;/strong&gt;：离线特征（用户长期画像、物品属性）和近线特征（最近一小时的点击序列、实时曝光计数）在请求时按 key 批量读出、和实时上下文（现在是几点、什么设备）拼成特征向量。&lt;strong&gt;特征的一致性是这个环节最大的坑&lt;/strong&gt;：离线训练用的特征和在线服务用的特征必须同一份定义，否则&amp;quot;训练时很准、上线后翻车&amp;rdquo;——特征平台存在的意义就是管这份一致性。&lt;/p&gt;
&lt;h2 id=&#34;重排混排从预测喜欢到设计一页&#34;&gt;重排/混排：从&amp;quot;预测喜欢&amp;quot;到&amp;quot;设计一页&amp;quot;&lt;/h2&gt;
&lt;p&gt;精排输出的是分数，但分数最高的 20 条直接端给用户往往体验很差：可能十条都是同一个作者、同一类题材。重排层的职责变了——&lt;strong&gt;不再预测&amp;quot;你多喜欢&amp;quot;，而是设计&amp;quot;这一页长什么样&amp;quot;&lt;/strong&gt;。它几乎全是规则和策略，在线执行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;多样性打散&lt;/strong&gt;：相邻 N 条不允许同作者/同类目；更讲究的用 MMR（每次选&amp;quot;分高且与已选差异大&amp;quot;的）或 DPP（行列式点过程，数学上保证选出的集合整体多样）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;混排&lt;/strong&gt;：把广告、关注更新、运营位按规则插进自然结果。广告有自己的排序逻辑——eCPM = 出价 × 预估点击率，既要用户可能点、又要广告主出价高；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负反馈过滤&lt;/strong&gt;：你点过&amp;quot;不感兴趣&amp;quot;的作者和类目，在这里被硬性屏蔽；&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=&#34;离线-vs-在线一张分工总表&#34;&gt;离线 vs 在线：一张分工总表&lt;/h2&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;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;td&gt;模型仓库&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;物品向量&lt;/td&gt;
					&lt;td&gt;内容的 embedding&lt;/td&gt;
					&lt;td&gt;离线（发布时 + 定期刷新）&lt;/td&gt;
					&lt;td&gt;向量索引库（Faiss/HNSW）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;物品相似度&lt;/td&gt;
					&lt;td&gt;itemCF 共现矩阵&lt;/td&gt;
					&lt;td&gt;离线（天/小时）&lt;/td&gt;
					&lt;td&gt;KV&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;td&gt;KV&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;td&gt;KV / 特征存储&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;用户短期兴趣&lt;/td&gt;
					&lt;td&gt;最近行为的加权向量&lt;/td&gt;
					&lt;td&gt;近线（秒级，Flink）&lt;/td&gt;
					&lt;td&gt;特征存储&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;召回&lt;/td&gt;
					&lt;td&gt;查相似物品、ANN 检索、读榜单&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;/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;/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;/td&gt;
					&lt;td&gt;用户看过什么&lt;/td&gt;
					&lt;td&gt;在线读写，近线聚合&lt;/td&gt;
					&lt;td&gt;KV / 布隆过滤器&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;规律一目了然：&lt;strong&gt;越靠近&amp;quot;知识沉淀&amp;quot;（模型参数、向量、相似度）的越离线，越靠近&amp;quot;当下上下文&amp;quot;（你刚点了什么、现在几点）的越在线&lt;/strong&gt;。在线路径上真正的计算只有两样——向量检索和模型推理，其余全是查表。&lt;/p&gt;
&lt;p&gt;注意一个工程事实：&lt;strong&gt;漏斗每一层都是&amp;quot;算法团队调模型、工程团队保接口&amp;quot;&lt;/strong&gt;。接口契约是稳定的（输入候选列表和特征，输出打分排序后的列表），模型怎么迭代都不影响上下游——这是推荐系统能持续进化的架构前提。&lt;/p&gt;
&lt;h2 id=&#34;无限瀑布流一分页协议为什么必须是游标&#34;&gt;无限瀑布流（一）：分页协议为什么必须是游标&lt;/h2&gt;
&lt;p&gt;无限下拉遇到的第一个坑是分页。直觉做法是 offset 分页（&amp;ldquo;给我第 41-60 条&amp;rdquo;），在推荐流场景下它有三个致命伤：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;offset 分页&lt;/th&gt;
					&lt;th&gt;cursor 游标分页&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;新内容插入头部&lt;/td&gt;
					&lt;td&gt;第 2 页和第 1 页之间内容错位，用户看到重复&lt;/td&gt;
					&lt;td&gt;游标锚定&amp;quot;上次最后一条&amp;quot;，新内容插进来也不影响&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;排序实时变化&lt;/td&gt;
					&lt;td&gt;每次请求重新排序，offset 语义直接失效&lt;/td&gt;
					&lt;td&gt;游标携带上次的排序上下文&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;深翻页性能&lt;/td&gt;
					&lt;td&gt;数据库 OFFSET 越大越慢&lt;/td&gt;
					&lt;td&gt;从锚点继续，代价恒定&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;所以推荐流的分页协议长这样：&lt;strong&gt;每次请求带一个游标（cursor），响应返回一页内容 + 下一页的游标&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;请求:  GET /feed?cursor=eyJscCI6...&amp;amp;count=20
响应:  { &amp;#34;items&amp;#34;: [...20条...], &amp;#34;next_cursor&amp;#34;: &amp;#34;eyJs...&amp;#34; }&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;游标是个不透明字符串（服务端编码，客户端不许解析），里面通常装着：上一页最后一条的标识和排序分、本次推荐的批次号、过期时间。&lt;strong&gt;&amp;ldquo;不透明&amp;quot;是关键设计&lt;/strong&gt;：服务端可以随时改游标格式和分页逻辑，客户端零感知。&lt;/p&gt;
&lt;h2 id=&#34;无限瀑布流二一次下拉请求的完整链路&#34;&gt;无限瀑布流（二）：一次下拉请求的完整链路&lt;/h2&gt;
&lt;p&gt;把上面两节合起来，用户下拉一次的完整链路：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant U as 客户端
    participant F as Feed 服务
    participant S as 在线存储
    participant R as 排序服务
    U-&amp;gt;&amp;gt;F: 下拉 带上次游标
    F-&amp;gt;&amp;gt;S: 查用户画像和已曝光记录
    F-&amp;gt;&amp;gt;S: 取候选集 多路召回合并
    F-&amp;gt;&amp;gt;R: 粗排 精排 重排
    R--&amp;gt;&amp;gt;F: 排序后的候选
    F-&amp;gt;&amp;gt;F: 过滤已曝光 取一页&amp;lt;br/&amp;gt;写入已曝光记录
    F--&amp;gt;&amp;gt;U: 返回 20 条 + 下一页游标
    U-&amp;gt;&amp;gt;U: 渲染 上报曝光事件&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;链路里有两个不那么显眼但缺一不可的组件：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;已曝光去重（seen filter）&lt;/strong&gt;。用户刷过的内容不能再出现，否则无限下拉就成了无限重播。实现上是在线存储里维护每用户的已曝光集合——请求时过滤、响应后写入。数据量大时用布隆过滤器压缩内存（允许极低误判率，误判的代价不过是少推一条内容），或者按天滚动窗口只记最近 N 天的曝光。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;预取（prefetch）&lt;/strong&gt;。如果等用户滑到底再发请求，每一页都有加载等待，无限流就成了&amp;quot;有停顿流&amp;rdquo;。客户端的标准做法：滚动到当前列表 70%-80% 位置时就预取下一页；网络差时加大预取提前量。用户感知到的&amp;quot;永远流畅&amp;quot;，一半是服务端架构，一半是这一个提前量。&lt;/p&gt;
&lt;h2 id=&#34;永远拉不到头怎么兑现&#34;&gt;&amp;ldquo;永远拉不到头&amp;quot;怎么兑现&lt;/h2&gt;
&lt;p&gt;这是无限瀑布流真正的核心问题：个性化候选再多也是有限的，用户刷 200 屏之后呢？答案是&lt;strong&gt;分层供给，逐级降级&lt;/strong&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;探索流量（exploration）&lt;/strong&gt;：故意混入你没表现出过兴趣的类目——推荐系统必须留探索比例，否则画像会收敛到死胡同（信息茧房在工程上的成因就是探索为零）。这层对用户是&amp;quot;刷到了意外之喜&amp;rdquo;，对系统是&amp;quot;采集新兴趣信号&amp;quot;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;热门与新内容兜底&lt;/strong&gt;：当个性化候选耗尽，降级到全站热门、最新发布——这些内容池每天都在被离线任务刷新，事实上是无限的；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间衰减&lt;/strong&gt;：同一条内容的推荐价值随时间衰减，昨天刷过的爆款明天权重下降，保证&amp;quot;明天再来还有新东西&amp;quot;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以&amp;quot;永远拉不到头&amp;quot;不是因为算出了无限多的个性化结果，而是因为&lt;strong&gt;个性化耗尽之前，系统会不露痕迹地滑向兜底池&lt;/strong&gt;。用户刷到第 100 屏时看到的&amp;quot;千人千面&amp;quot;，成分里热门内容的占比早已悄悄超过个性化——体验连续，供给无限。&lt;/p&gt;
&lt;h2 id=&#34;数据闭环这一刷决定下一刷&#34;&gt;数据闭环：这一刷决定下一刷&lt;/h2&gt;
&lt;p&gt;最后把环闭上。客户端上报的每一条曝光、点击、停留时长、划走速度，都经 Kafka 进入近线层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;停留 5 秒以上 → 画像里对应类目的权重上调；&lt;/li&gt;
&lt;li&gt;秒划走 → 权重下调；&lt;/li&gt;
&lt;li&gt;点了的广告、取关的话题 → 写入负反馈特征。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下一刷的召回和排序用的就是更新后的画像。&lt;strong&gt;推荐系统的&amp;quot;智能&amp;quot;不在某一次排序多准，而在于这个闭环转得多快&lt;/strong&gt;——从行为发生到画像生效的延迟，直接决定用户&amp;quot;它懂我&amp;quot;的体感。这也是为什么 Netflix 那样的公司要养三万个流处理作业、还专门给它们做自动扩缩容。&lt;/p&gt;
&lt;h2 id=&#34;和关注流的关系&#34;&gt;和关注流的关系&lt;/h2&gt;
&lt;p&gt;顺带澄清一个常见混淆：微博、X 都有两种流——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;关注流（关注的人发的内容，按时间排序）&lt;/strong&gt;：经典问题是写扩散（发微博时推给所有粉丝）还是读扩散（刷的时候现拉），粉丝量大的大 V 是写扩散的噩梦；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推荐流（本文讨论的）&lt;/strong&gt;：没有&amp;quot;关注&amp;quot;这个约束，内容来自全站，靠排序漏斗筛选，游标分页。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现代产品里两者早已混合：推荐流里插关注内容、关注流里插推荐内容，都在重排混排那一层完成。&lt;/p&gt;
&lt;h2 id=&#34;落地清单&#34;&gt;落地清单&lt;/h2&gt;
&lt;p&gt;如果要自己实现一套，按这个顺序检查：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;骨架&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 三层分离：离线训练、近线特征、在线排序，请求路径上只留轻活；&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 排序漏斗四层：召回保覆盖、粗排保性价比、精排保准确、重排保体验，层间接口契约稳定。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;无限流协议&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 游标分页，游标不透明、带过期时间；&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 已曝光去重：在线集合或布隆过滤器，按天滚动窗口；&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 客户端 70%-80% 位置预取下一页。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&amp;ldquo;拉不完&amp;quot;的保障&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 分层供给：个性化 → 探索 → 热门/新内容兜底，降级要无感；&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 探索流量比例显式配置，别让它收敛到零；&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 内容时间衰减，保证&amp;quot;明天还有新东西&amp;rdquo;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;闭环&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 曝光/点击/停留全量上报，经消息队列进近线层；&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; 监控&amp;quot;行为 → 画像生效&amp;quot;的端到端延迟，这个数就是用户体感的下限。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;一句话收拢：千人千面不是&amp;quot;为每个用户算一遍&amp;quot;，而是&lt;strong&gt;把计算摊到用户到来之前的每一秒里&lt;/strong&gt;；无限瀑布流不是&amp;quot;有无限内容&amp;quot;，而是&lt;strong&gt;个性化耗尽之前，系统早已悄悄换档&lt;/strong&gt;。两个魔术的机关，都在用户看不见的那一侧。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;想直观感受这套漏斗里的数据长什么样，可以看姊妹篇&lt;a href=&#34;https://ftmao.com/blog/blog/posts/tiktok-recommendation-math-walkthrough/&#34;&gt;《从余弦定理到 TikTok：一个用户的推荐流是怎么被算出来的》&lt;/a&gt;——造一个用户，带着他一晚上的行为数据走完召回、粗排、精排、重排。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;参考：Netflix 技术博客的个性化系统架构（offline/nearline/online 三层划分）；推荐系统排序漏斗与游标分页为业界通用架构实践。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>三万个 Flink 作业怎么自动扩缩容：Netflix 两代 Autoscaler 的教训</title>
      <link>https://ftmao.com/blog/posts/flink-autoscaling-netflix-two-autoscalers/</link>
      <pubDate>Fri, 02 Oct 2026 17:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/flink-autoscaling-netflix-two-autoscalers/</guid>
      <description>&lt;h1 id=&#34;三万个-flink-作业怎么自动扩缩容netflix-两代-autoscaler-的教训&#34;&gt;三万个 Flink 作业怎么自动扩缩容：Netflix 两代 Autoscaler 的教训&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;目标读者：三类人——&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;平台/数据工程师&lt;/strong&gt;：正在或准备给 Flink 作业做自动扩缩容，想知道选哪个方案、坑在哪（看&amp;quot;背景二&amp;quot;和&amp;quot;三个工程缺口&amp;quot;）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;架构师&lt;/strong&gt;：在给实时计算平台做选型，需要&amp;quot;外部指标 vs 内部推理&amp;quot;两条路线的完整对比（看两代方案的对比表）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CTO/技术管理者&lt;/strong&gt;：关心自研和开源的边界，Netflix 从自建收敛到开源的过程是一个现成的决策样本（看&amp;quot;三条可以带走的教训&amp;quot;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;没跑过 Flink 也能读：正文前两节是背景知识；跑过的可以直接跳到&amp;quot;第一代&amp;quot;开始看。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;之前那篇&lt;a href=&#34;https://ftmao.com/blog/blog/posts/how-to-get-reliable-tech-knowledge-in-ai-era/&#34;&gt;《软件知识的分层与获取地图》&lt;/a&gt;说过：方案层的知识没有教材，只有案例，大厂技术博客是主力来源。最近去读了 Netflix 技术博客的 &lt;a href=&#34;https://netflixtechblog.com/a-tale-of-two-flink-autoscalers-e9f6a1b1492b&#34;&gt;A Tale of Two Flink Autoscalers&lt;/a&gt;，讲的是他们怎么做&lt;strong&gt;流处理作业的自动扩缩容&lt;/strong&gt;。这篇是我的学习笔记，把里面的架构知识点整理出来。&lt;/p&gt;
&lt;p&gt;问题的形状很普遍：作业负载随昼夜、活动、故障转移大幅波动，按峰值配资源浪费，按均值配资源高峰期 lag 爆炸。Netflix 的答案是先后造了两代 autoscaler，现在正在从自研收敛到开源。两代的设计差异和迁移教训，比算法本身更有价值。&lt;/p&gt;
&lt;p&gt;不过进入正题前，先补两段背景：Flink 是什么、扩缩容在这上面为什么天然就贵；以及开源社区现在有哪些现成方案、谁更好。&lt;/p&gt;
&lt;h2 id=&#34;背景一flink-是什么扩缩容为什么贵&#34;&gt;背景一：Flink 是什么，扩缩容为什么贵&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Apache Flink 是开源的流处理框架&lt;/strong&gt;，核心能力是&amp;quot;对无界数据流做有状态计算&amp;quot;：数据像水管里的水一样源源不断地来（Kafka 消息、日志、点击流），Flink 一边流一边算，算出实时结果（实时报表、实时推荐、风控告警）。&lt;/p&gt;
&lt;p&gt;一个 Flink 作业（job）在逻辑上是一张&lt;strong&gt;算子组成的有向无环图（DAG）&lt;/strong&gt;：数据从 source 进来，经过 map、keyBy、window、join 这些算子的处理，最后写到 sink。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;source&amp;lt;br/&amp;gt;读 Kafka&amp;#34;] --&amp;gt; B[&amp;#34;map / filter&amp;lt;br/&amp;gt;转换清洗&amp;#34;]
    B --&amp;gt; C[&amp;#34;keyBy + window&amp;lt;br/&amp;gt;分组聚合 带状态&amp;#34;]
    C --&amp;gt; D[&amp;#34;sink&amp;lt;br/&amp;gt;写下游&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;要理解扩缩容为什么难，需要知道四个机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;并行度（parallelism）&lt;/strong&gt;：每个算子可以拆成多个并行实例（叫 subtask），分摊在集群的 TaskManager 进程里跑。所谓&amp;quot;扩缩容&amp;quot;，本质就是调整各算子的并行度、增减 TaskManager。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;状态（state）&lt;/strong&gt;：keyBy 之后的窗口、聚合结果都存在状态里，大作业的状态可以到 TB 级。状态按 key 分布在各个并行实例上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;checkpoint / savepoint&lt;/strong&gt;：Flink 定期把状态整体快照下来，用于故障恢复。&lt;strong&gt;改并行度意味着状态要在新数量的实例间重新分配&lt;/strong&gt;——所以扩缩容的标准动作是&amp;quot;做 savepoint → 停作业 → 按新并行度重启 → 重新加载状态&amp;quot;，状态越大恢复越慢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;背压（backpressure）&lt;/strong&gt;：下游算子处理不过来，会沿着数据流反向&amp;quot;顶住&amp;quot;上游。背压是判断&amp;quot;瓶颈在哪个算子&amp;quot;的关键信号，也是第二代 autoscaler 的数据基础。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一句话：&lt;strong&gt;无状态服务扩缩容是加台机器的事；有状态流处理扩缩容是一次&amp;quot;带着全部家当搬家&amp;quot;&lt;/strong&gt;。这就是为什么&amp;quot;自动扩缩容&amp;quot;在无状态微服务上早就普及，在 Flink 上却直到这两年才有成熟方案。&lt;/p&gt;
&lt;h2 id=&#34;背景二开源社区有什么方案谁更好&#34;&gt;背景二：开源社区有什么方案，谁更好&lt;/h2&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;th&gt;现状&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Flink Reactive Mode（1.13 起）&lt;/td&gt;
					&lt;td&gt;作业自适应集群里现有的 TaskManager 数量，配合 K8s HPA 按 CPU 伸缩容器&lt;/td&gt;
					&lt;td&gt;整个作业一刀切&lt;/td&gt;
					&lt;td&gt;简单可用，但按容器指标决策，看不见作业内部瓶颈&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Apache Flink Autoscaler（Flink Kubernetes Operator 内置）&lt;/td&gt;
					&lt;td&gt;从作业内部逐算子算&amp;quot;真实处理速率&amp;quot;，按需逐顶点调并行度&lt;/td&gt;
					&lt;td&gt;逐算子&lt;/td&gt;
					&lt;td&gt;社区主力方向，本文的主角&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;云厂商托管服务（如 AWS Managed Service for Apache Flink）&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;各家自研（Netflix 第一代、当年 Twitter Heron 的 Dhalion 等）&lt;/td&gt;
					&lt;td&gt;外部指标（CPU、lag、背压）+ 自研策略&lt;/td&gt;
					&lt;td&gt;整个作业一刀切&lt;/td&gt;
					&lt;td&gt;大多是&amp;quot;社区没有成熟方案时&amp;quot;的历史产物&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;谁更好？&lt;strong&gt;对 2026 年刚开始做这件事的团队，答案基本收敛了&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;作业跑在 K8s 上 → 直接用 Flink Kubernetes Operator 自带的 autoscaler，顶点级、背压感知，是当前最通用的最优解；&lt;/li&gt;
&lt;li&gt;平台是自建的（不在 K8s 上、有自己的控制面）→ 核心算法库已经独立出来，可以像 Netflix 一样只接四个接口（下文详述），不必自建；&lt;/li&gt;
&lt;li&gt;作业简单、无状态、量不大 → Reactive Mode 或云托管的粗粒度伸缩就够用，别上重武器；&lt;/li&gt;
&lt;li&gt;从零自研外部指标 autoscaler → 2019 年的合理选择，2026 年不太成立——Netflix 自己正在从这条路迁走，这就是用脚投票。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有了这个背景，Netflix 这两代方案的故事就好读了：它几乎把上面每条路线都趟了一遍。&lt;/p&gt;
&lt;h2 id=&#34;为什么这个问题在-netflix-躲不掉&#34;&gt;为什么这个问题在 Netflix 躲不掉&lt;/h2&gt;
&lt;p&gt;先看规模：2026 年，Netflix 在多个 AWS 区域跑着 &lt;strong&gt;3 万多个 Flink 作业&lt;/strong&gt;（2017 年开始用 Flink）。这决定了&amp;quot;人肉运维&amp;quot;这条路根本不存在。&lt;/p&gt;
&lt;p&gt;这三万个作业到底在干什么业务？按原文的分类可以拆成两大块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;大头是托管平台 Data Mesh 自动生成的数据管道&lt;/strong&gt;——所以这三万个作业的主体确实是&amp;quot;大数据相关&amp;quot;，不是在跑在线业务服务。Data Mesh 是 Netflix 的托管数据搬运/处理平台（2023 年他们在博客公开过架构）：用户在界面上声明&amp;quot;数据从哪来、到哪去、中间做什么转换&amp;quot;，平台自动生成 Flink 作业——典型场景是 Kafka 之间的数据搬运、数据库变更（CDC）实时同步到数仓（Iceberg）、事件路由分发。用户大多根本不直接接触 Flink，自然也不会去管扩缩容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;少数但增长快的是各团队自建自运维的自定义作业&lt;/strong&gt;，承载真正的实时业务逻辑：个性化（实时推荐特征计算）、广告（2022 年广告套餐上线后的曝光与计费遥测）、直播活动（体育赛事直播的实时监控）等。原文里靠 autoscaler 省下 110 万美元的案例，就来自做客户端遥测和日志的团队。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;形态差异也很大：从 Kafka 搬运工式的单算子作业，到带分支、join、TB 级状态的复杂流水线都有——记住这个差异，后面两代 autoscaler 的能力边界正好卡在这里。&lt;/p&gt;
&lt;p&gt;顺带澄清一个容易误解的点：&lt;strong&gt;首页千人千面的推荐，不是每个用户打开时都走一遍 Flink&lt;/strong&gt;。Flink 是流处理引擎，干的是&amp;quot;事件来了就算&amp;quot;，不在用户请求链路上。个性化系统是经典的三层结构（Netflix 早在 2013 年就公开过这套架构）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;离线 offline&amp;lt;br/&amp;gt;批量训练模型&amp;lt;br/&amp;gt;计算全量候选集&amp;#34;] --&amp;gt; D[&amp;#34;在线存储&amp;lt;br/&amp;gt;画像 特征 候选集&amp;#34;]
    B[&amp;#34;近线 nearline&amp;lt;br/&amp;gt;行为事件进 Kafka&amp;lt;br/&amp;gt;Flink 实时更新画像特征&amp;#34;] --&amp;gt; D
    D --&amp;gt; C[&amp;#34;在线 online&amp;lt;br/&amp;gt;打开首页时排序组装&amp;lt;br/&amp;gt;几十毫秒返回&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;你在观看、暂停、评分时产生的行为事件，由近线层的 Flink 作业实时消化、更新画像；你打开首页那一瞬间，在线服务只做&amp;quot;查 + 排&amp;quot;，几十毫秒返回。&lt;strong&gt;把重计算从请求路径挪到事件流上&lt;/strong&gt;，正是个性化系统扛住全球用户规模的关键设计——本文说的&amp;quot;个性化&amp;quot;自定义作业，干的就是近线这一层的活。&lt;/p&gt;
&lt;p&gt;更麻烦的是约束条件：&lt;strong&gt;在这个平台上，扩缩容不是免费的&lt;/strong&gt;。默认动作是&amp;quot;做 savepoint → 优雅停掉作业 → 按新规模重启&amp;quot;，大状态作业一次要几分钟。这意味着扩缩容策略不能冲动——每次动作都是分钟级的中断。&lt;/p&gt;
&lt;p&gt;于是一个真正难的问题摆在面前：&lt;strong&gt;如何在没有人的情况下，让每个作业在需要时拿到需要的资源，同时不把任何东西搞挂？&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;第一代从外面看的-autoscaler2019&#34;&gt;第一代：从外面看的 autoscaler（2019）&lt;/h2&gt;
&lt;p&gt;Netflix 的第一个答案本身就是一条流处理作业：跑在自家 Mantis 平台上，消费遥测系统 Atlas 吐出的集群级实时指标——每个作业的 CPU、网络、Kafka lag、摄入速率、消费速率。&lt;/p&gt;
&lt;p&gt;决策逻辑是几种信号的组合：lag 推导出的追赶时间、CPU/网络利用率阈值、历史性能基线、对近期摄入速率做回归预测，然后决定扩容，或者判断&amp;quot;更小的集群也扛得住未来窗口&amp;quot;从而缩容。&lt;/p&gt;
&lt;p&gt;这个设计有两个当时看来很聪明、事后定义了天花板的选择：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;它独立于 Flink 平台运行&lt;/strong&gt;。Flink 自己出问题时扩缩容不受牵连；而且按&amp;quot;流处理作业&amp;quot;的形态来构建，天然好扩展——每个 autoscaler 节点负责一部分 Flink 作业的指标，从没写过什么分片协调逻辑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成绩单不差&lt;/strong&gt;：在几千个托管流水线上稳定砍下 25%-45% 的资源用量（Flink Forward 2020 分享过）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但&amp;quot;从外面看&amp;quot;是有代价的，原文叫它 ceiling（天花板）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;粗粒度 + 单一旋钮&lt;/strong&gt;。它通过容器级指标推理整个集群，而且只能调一个数——TaskManager 总数，也就是作业里所有算子同比例缩放。对单算子简单流水线够用，对多算子、有状态的 DAG（广告、推荐、游戏团队越来越多拿来的正是这种）就无能为力——而这恰恰是它推理不了的作业类型。每接一种新 case 就得加定制逻辑，形不成通用能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指标会骗你&lt;/strong&gt;。autoscaler 的天花板就是外部指标系统的天花板。有的作业明明忙得团团转，CPU 利用率上却毫无体现，作业卡在降级状态里，scaler 根本看不见。更狠的一次：一次网络迁移悄悄改变了部分流量的上报口径，Atlas 里 scaler 依赖的一组指标开始漏报——&lt;strong&gt;这个缺口在生产环境潜伏了很久才暴露&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;到这一步，build vs buy 被重新摆上了桌面。&lt;/p&gt;
&lt;h2 id=&#34;第二代从里面推理的-autoscaler开源方案&#34;&gt;第二代：从里面推理的 autoscaler（开源方案）&lt;/h2&gt;
&lt;p&gt;2019 年自建时，Flink 社区没有成熟的 autoscaler；等 Netflix 重新评估时，已经有了：Apache Flink Autoscaler。思路发生了根本变化——&lt;strong&gt;不从外面看容器，从作业内部推理&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;核心概念是&lt;strong&gt;真实处理速率（TPR, True Processing Rate）&lt;/strong&gt;：一个算子如果满负荷运转，能扛住的吞吐是多少。&lt;/p&gt;
&lt;p&gt;Flink 会按 subtask 上报&amp;quot;每秒真正干活的时间占比&amp;quot;（和背压时间、空闲时间分开统计）。用观测吞吐除以忙碌占比，就能把容量外推到满负荷：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一个算子实测每秒处理 700 条记录、忙碌占比 70%，TPR = 700 / 0.7 = 1000 条/秒。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;有了这个数，autoscaler 从 source 开始遍历整个作业 DAG，结合每个算子的 TPR、输入输出比率和目标利用率，&lt;strong&gt;逐顶点计算需要的并行度&lt;/strong&gt;——保证没有任何一个算子成为瓶颈，而不是把整个集群当整体伸缩。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;Flink 上报&amp;lt;br/&amp;gt;每个 subtask 的忙碌占比&amp;#34;] --&amp;gt; B[&amp;#34;TPR = 观测吞吐 / 忙碌占比&amp;lt;br/&amp;gt;外推满负荷容量&amp;#34;]
    B --&amp;gt; C[&amp;#34;从 source 遍历作业 DAG&amp;lt;br/&amp;gt;结合输入输出比率&amp;#34;]
    C --&amp;gt; D[&amp;#34;逐顶点计算目标并行度&amp;lt;br/&amp;gt;没有算子成为瓶颈&amp;#34;]&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;/td&gt;
					&lt;td&gt;容器级指标（Atlas）&lt;/td&gt;
					&lt;td&gt;作业内 per-vertex 指标&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;决策单元&lt;/td&gt;
					&lt;td&gt;整个集群一个旋钮&lt;/td&gt;
					&lt;td&gt;DAG 逐顶点并行度&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;适用作业&lt;/td&gt;
					&lt;td&gt;单算子简单流水线&lt;/td&gt;
					&lt;td&gt;多算子有状态 DAG&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;对 Netflix 来说，决定性的就是后两行：开源方案能扩缩的恰好是自研系统天生处理不了的那类作业，而且每个作业可以带自己的配置（稳定期、阈值等扩缩行为参数）——这正是那些一直被人工扩缩容的自定义作业需要的。&lt;/p&gt;
&lt;h2 id=&#34;落到三万个作业三个工程缺口&#34;&gt;落到三万个作业：三个工程缺口&lt;/h2&gt;
&lt;p&gt;算法层面拿来即用，社区已经把难的部分做完了。Netflix 的工作是让它在自己的体系里可靠运转——这才是&amp;quot;社区能跑&amp;quot;和&amp;quot;Netflix 规模能跑&amp;quot;之间的距离。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;接入方式：独立库 + 四个接口。&lt;/strong&gt; 开源 autoscaler 最初架构在 Flink Kubernetes Operator 里，而 Netflix 的 Flink 平台跑在自己的控制面上。社区后来做了一个关键决策：把核心逻辑抽成独立库，只留四个通用接口——携带作业元数据和 REST 信息的 context、状态存储、事件处理器、执行扩缩动作的 realizer。Netflix 的接入服务是一个 Spring Boot 应用，编排跑在 Temporal（持久化工作流引擎）上：一个编排工作流每分钟轮询一次控制面，为每个开启扩缩容的作业启动一个独立的长运行工作流。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;Temporal 编排工作流&amp;lt;br/&amp;gt;每分钟轮询控制面&amp;#34;] --&amp;gt; B[&amp;#34;每个作业一个&amp;lt;br/&amp;gt;独立长运行工作流&amp;#34;]
    B --&amp;gt; C[&amp;#34;从 JobManager 拉&amp;lt;br/&amp;gt;per-vertex 指标&amp;#34;]
    C --&amp;gt; D[&amp;#34;跑开源评估算法&amp;#34;]
    D --&amp;gt; E[&amp;#34;realizer 通过控制面&amp;lt;br/&amp;gt;执行扩缩&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&amp;ldquo;每个作业一个工作流&amp;quot;是直接被 pain 教育出来的设计：最初用单一批处理循环评估所有作业，&lt;strong&gt;一个慢作业或异常作业能卡住它后面所有作业的指标采集和扩缩&lt;/strong&gt;。拆开之后，单个作业出问题只在自己的工作流里失败重试，爆炸半径被隔离，接入更多作业时运行时水平扩展即可。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三个必须自己补的工程缺口：&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;高并行度下的指标采集&lt;/strong&gt;。大作业上从 JobManager 拉指标成为瓶颈，部分原因在 Flink 运行时本身。Netflix 改了 JobManager：缓存瞬时指标名、统一清理而不是每次拉取都重扫，并加了服务端过滤让 autoscaler 只拉需要的指标。效果：可用规模从约 1000 个 subtask 提到 3000 个。这些改动在内部 fork 里，部分已贡献回上游（如 FLINK-36172）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;保住 forward chaining&lt;/strong&gt;。由 forward 边连接的两个顶点必须同并行度——记录是在内存里走固定本地通道交接的。如果只扩一个，Flink 不会报错，&lt;strong&gt;它会悄悄把这条边转成网络 shuffle&lt;/strong&gt;（性能完全另一回事）。Netflix 的 fork 会检测 forward 连接的子图，把它们当整体扩缩。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;尊重 sink 的容量上限&lt;/strong&gt;。有些 sink 写入能力有限，把上游作业扩上去只会把自己顶死。fork 里加了 async-sink 背压检测，阻止 autoscaler 把作业扩到 sink 吃不下。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;最后还有 realizer 的&lt;strong&gt;安全检查&lt;/strong&gt;：比如公司级区域故障转移期间，拒绝在被撤离的区域缩容；执行前确认新集群磁盘放得下作业的 checkpoint 状态；大集群再加一点 standby 缓冲。&lt;/p&gt;
&lt;h2 id=&#34;效果与调校&#34;&gt;效果与调校&lt;/h2&gt;
&lt;p&gt;开源方案的 autoscaler 去年在 Netflix 自定义作业上 GA，早期结果里有个硬数字：客户端遥测日志团队的 Flink 计算支出&lt;strong&gt;年化下降 58%，约省 110 万美元&lt;/strong&gt;。效率来自三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;静态配置永远要按峰值留量，autoscaler 吃掉了昼夜和周末低谷；&lt;/li&gt;
&lt;li&gt;性能优化或节后流量回落之后，不再依赖团队手工降配，autoscaler 持续跟进；&lt;/li&gt;
&lt;li&gt;统一容器规格带来更好的装箱（bin-packing）和更细的扩缩粒度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但&amp;quot;缩得太狠&amp;quot;是另一个坑：砍过头 CPU 打满、lag 飙升，而系统没法立刻反应——每次重启后指标窗口和稳定期都要重建。Netflix 现在把&lt;strong&gt;目标利用率定在 0.45，低于社区默认的 0.7&lt;/strong&gt;，故意用一点效率换稳定：对大型有状态作业，更少、更平稳的扩缩动作值这个价。&lt;/p&gt;
&lt;p&gt;剩下的最大成本已经不是 scaler 的逻辑，而是&lt;strong&gt;重启和状态恢复本身&lt;/strong&gt;。Flink 2 的分离式状态架构（状态放外部存储而不是本地磁盘）有望让扩缩代价与状态大小解耦——Netflix 已开始支持 Flink 2.2，准备实验新 state backend。&lt;/p&gt;
&lt;h2 id=&#34;三条可以带走的教训&#34;&gt;三条可以带走的教训&lt;/h2&gt;
&lt;p&gt;文章结尾总结的三条，泛化到 Flink 之外依然成立：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;指标选择比算法精巧更重要。&lt;/strong&gt; 最有价值的调试很少是关于扩缩数学的，而是&amp;quot;该信哪个信号&amp;rdquo;。调算法之前，先搞懂你的指标——口径会变、会漏报、会测不到真正的瓶颈（第一代 scaler 的两次事故都是指标层的）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认值要合理，但留出调节空间。&lt;/strong&gt; 托管作业相似度够高，一套好默认值覆盖大多数——这正是平台的意义。但把单一配置强加给所有作业，是在惩罚不适配的那些：默认值 + 每作业覆盖，同时把需要深专业知识的旋钮藏起来。大多数团队永远不该需要思考 autoscaler 的存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先采用，再扩展（Adopt, then extend）。&lt;/strong&gt; 2019 年自建是因为没有成熟选项；社区方案出现后，正确的动作既不是永远捍卫自己的投资，也不是连夜拆掉，而是：&lt;strong&gt;新工作负载先用开源的，把修复贡献回上游，再有计划地迁移存量&lt;/strong&gt;。自研基础设施的真实成本，要在&amp;quot;本来可以采用现成方案&amp;quot;的对照下衡量。&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;一句话收拢：自动扩缩容的难点从来不在&amp;quot;扩缩&amp;quot;二字，而在于&lt;strong&gt;你凭什么相信指标、怎么隔离故障、以及什么时候承认社区已经做得比你好了&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;来源：Samuel Yeboah, Francesco Di Chiara, Mingliang Liu, &amp;ldquo;&lt;a href=&#34;https://netflixtechblog.com/a-tale-of-two-flink-autoscalers-e9f6a1b1492b&#34;&gt;A Tale of Two Flink Autoscalers&lt;/a&gt;&amp;rdquo;, Netflix TechBlog。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>Netflix 服务拓扑系统笔记：实时依赖地图是怎么被生产环境捶出来的</title>
      <link>https://ftmao.com/blog/posts/netflix-service-topology-at-scale/</link>
      <pubDate>Mon, 13 Jul 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-service-topology-at-scale/</guid>
      <description>&lt;h1 id=&#34;netflix-服务拓扑系统笔记实时依赖地图是怎么被生产环境捶出来的&#34;&gt;Netflix 服务拓扑系统笔记：实时依赖地图是怎么被生产环境捶出来的&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/building-service-topology-at-scale-architecture-challenges-and-lessons-learned-f4b792f3f0d8&#34;&gt;Building Service Topology at Scale: Architecture, Challenges, and Lessons Learned&lt;/a&gt;
目标读者：做过或准备做流式数据管线的后端工程师；被 Kafka 消费滞后、JVM GC、热点节点折磨过的人读起来会格外有体感。
阅读目标：搞清楚这套系统的三个核心决策（流式优先、三级聚合管线、动态一致性哈希）各自解决了什么问题，以及 V1 到 V2 连环踩坑的顺序——那个顺序比任何单个方案都值得记住。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;开场白比正文值钱&#34;&gt;〇、开场白比正文值钱&lt;/h2&gt;
&lt;p&gt;这个系列第一篇讲的是 what 和 why：Netflix 工程师需要统一的实时服务依赖视图来加速排障、评估爆炸半径，方案是把 eBPF 网络流、IPC 指标、分布式追踪三个数据源做成物理隔离的三层图。这篇讲的是 how——工程现实。&lt;/p&gt;
&lt;p&gt;作者的开场自白很诚实：&lt;strong&gt;V1 在本地环境完美运行，上生产就是另一个故事&lt;/strong&gt;。Kafka 消费者落后，实例内存耗尽，有的节点流量是其他节点的 100 倍，GC 停顿吃掉的 CPU 比业务逻辑还多。这篇文章不是成功故事，是一趟踩坑复盘。&lt;/p&gt;
&lt;h2 id=&#34;一流式优先新鲜度不是锦上添花&#34;&gt;一、流式优先：新鲜度不是锦上添花&lt;/h2&gt;
&lt;p&gt;传统拓扑系统是批处理：按小时或按天聚合，存全量快照。小规模没问题，但凌晨三点出事故时，一小时前的依赖图是考古，不是可观测性。&lt;/p&gt;
&lt;p&gt;决策是 streaming-first：多区域 Kafka 流和 IPC 指标以 Server-Sent Events 持续摄入，反应式管线处理，拓扑更新延迟在几十分钟量级，对比批处理的小时级、天级。事故响应、变更验证这些场景等不了下一个小时级批次。&lt;/p&gt;
&lt;p&gt;流式处理绕不开一个根本问题：下游慢了怎么办？三个传统方案都不行——无界队列会缓冲到内存爆掉；丢弃式流控会让拓扑残缺，丢的正是连接信息；批处理来不及。他们的答案是&lt;strong&gt;背压&lt;/strong&gt;：Stage 3 写图数据库写不动，通知 Stage 2 减速，Stage 2 通知 Stage 1，Stage 1 让 Kafka consumer 暂停拉取，数据就在 Kafka 里等着。任何一级被打爆（流量尖峰、GC 停顿、外部系统变慢），整条管线自动降到可持续的速率。数据晚到几秒几分钟，但既不丢也不崩。代价是反应式流本身的复杂度，这个后面会还回来。&lt;/p&gt;
&lt;h2 id=&#34;二三层物理隔离&#34;&gt;二、三层物理隔离&lt;/h2&gt;
&lt;p&gt;多源方案落地为三个物理隔离的层，各自配不同的存储：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;网络层&lt;/strong&gt;：eBPF 流日志，存图数据库分区。覆盖全，但没有应用上下文。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPC 层&lt;/strong&gt;：应用指标，存另一个与网络层隔离的图数据库。端点信息丰富，但只覆盖插桩了的服务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;追踪层&lt;/strong&gt;：分布式追踪，存列式存储（Parquet）。是真实请求路径，但有采样。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;物理隔离的意义是各层可以按自己的吞吐、查询模式和演进节奏独立优化。查询时并行查相关存储再合并结果，做到亚秒延迟。&lt;/p&gt;
&lt;h2 id=&#34;三三级聚合管线一个架构解两个题&#34;&gt;三、三级聚合管线：一个架构解两个题&lt;/h2&gt;
&lt;p&gt;这是全文的核心。网络流日志有个根本缺陷：&lt;strong&gt;它只能看到单个网络跳，看不到应用级连接&lt;/strong&gt;。云环境里流量很少直连，中间隔着负载均衡器、NAT 网关、API 网关、代理。流日志里 App A 到 LB 是一条流，LB 到 App B 是另一条流，但工程师要的是 App A 到 App B 这条逻辑依赖。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    K[&amp;#34;多区域 Kafka&amp;lt;br/&amp;gt;eBPF 流日志&amp;#34;] --&amp;gt; S1[&amp;#34;Stage 1 初始聚合&amp;lt;br/&amp;gt;过滤非法流&amp;lt;br/&amp;gt;5 分钟时间窗聚合&amp;lt;br/&amp;gt;一致性哈希分发&amp;#34;]
    S1 --&amp;gt;|&amp;#34;SSE 流&amp;#34;| S2[&amp;#34;Stage 2 中间件解析&amp;lt;br/&amp;gt;按中间件分组&amp;lt;br/&amp;gt;双跳拼成直连边&amp;lt;br/&amp;gt;再次一致性哈希&amp;#34;]
    S2 --&amp;gt;|&amp;#34;SSE 流&amp;#34;| S3[&amp;#34;Stage 3 最终聚合与富化&amp;lt;br/&amp;gt;查 KV 补元数据&amp;lt;br/&amp;gt;转成图实体&amp;lt;br/&amp;gt;限流写库&amp;#34;]
    S3 --&amp;gt; DB[&amp;#34;图数据库&amp;#34;]
    S3 -.-&amp;gt;|&amp;#34;背压: 写不动就减速&amp;#34;| S2
    S2 -.-&amp;gt;|&amp;#34;背压传播&amp;#34;| S1
    S1 -.-&amp;gt;|&amp;#34;暂停拉取&amp;lt;br/&amp;gt;数据留在 Kafka&amp;#34;| K
    style S2 fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三个要点：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么三级而不是一级？数据局部性。&lt;/strong&gt; 要把 App A 到 LB 加 LB 到 App B 拼成 App A 到 App B，必须让同一个中间件的所有流落在同一台实例上做 join。而 Stage 1 里流是按 Kafka 分区散在各实例上的。所以 Stage 2 的关键职能是按中间件标识重新分发——经典 map-reduce：Stage 1 做 map，Stage 2 做 shuffle 加 reduce，Stage 3 做最终聚合。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph RAW[&amp;#34;解析前 两条单跳流&amp;#34;]
        A1[&amp;#34;App A&amp;#34;] --&amp;gt; LB[&amp;#34;Load Balancer&amp;#34;]
        LB --&amp;gt; B1[&amp;#34;App B&amp;#34;]
    end
    subgraph RES[&amp;#34;解析后 逻辑依赖&amp;#34;]
        A2[&amp;#34;App A&amp;#34;] --&amp;gt;|&amp;#34;合并双跳指标&amp;lt;br/&amp;gt;记录途经的中间件&amp;#34;| B2[&amp;#34;App B&amp;#34;]
    end
    RAW ==&amp;gt;|&amp;#34;按中间件 shuffle 到同一实例&amp;lt;br/&amp;gt;join 入流与出流&amp;#34;| RES
    style RES fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;为什么三级而不是两级？&lt;/strong&gt; 他们最初就是两级，测试能跑，生产翻车：中间件解析要求把某个热门中间件的所有流集中到一台实例，热点应用的实例成了热节点；而数据富化（查外部存储拿健康状态和元数据）又意味着最忙的实例同时 I/O 最重。拆成三级后，解析（计算密集）和富化持久化（I/O 密集）隔离，每条流被分发、解析、再分发、最后持久化——&lt;strong&gt;负载被两轮哈希打散&lt;/strong&gt;，即使中间件流量是平均值的 100 倍，也没有单实例成为瓶颈。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么 SSE 而不是 gRPC？&lt;/strong&gt; 最初用的 gRPC，结果序列化开销、连接池管理、流式响应的内存压力吃掉的 CPU 比业务逻辑还多。消息队列则白白增加基础设施复杂度。SSE 轻量、HTTP 基础、和反应式流的背压天然集成。教训原话：行业最佳实践不普遍适用，测量，别假设。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么 IPC 层一级就够？&lt;/strong&gt; IPC 指标本来就是应用级的，没有中间件要解析；而且数据从源头就按一致性哈希分对了。这带出一条普适原则：&lt;strong&gt;数据的分区方式决定处理架构&lt;/strong&gt;——进来时分对了就直接聚合，分不对（比如网络流需要中间件解析）就得加 shuffle 级。&lt;/p&gt;
&lt;h2 id=&#34;四动态一致性哈希白嫖现有基础设施&#34;&gt;四、动态一致性哈希：白嫖现有基础设施&lt;/h2&gt;
&lt;p&gt;ASG 动态扩缩容时怎么决定哪个实例收哪个 aggregator？传统做法假设静态集群，扩缩容要显式再平衡、协调服务或手工搬数据。他们的做法：每个实例查服务注册中心拿当前健康的实例列表，排序（保证所有实例视图一致），然后用 &lt;code&gt;findOwnerInstance(aggregator.primaryKey)&lt;/code&gt; 路由。注册中心本来就为健康检查跟踪着 ASG 成员，拿它当集群成员的事实来源，&lt;strong&gt;动态成员资格是白来的&lt;/strong&gt;。一致性哈希保证成员变化时大部分 aggregator 留在原实例，排序列表保证视图一致。扩容时新实例立即接到自己那份流量，部署时 aggregator 无缝漂移到健康实例，全程无协调协议、无人工干预。&lt;/p&gt;
&lt;h2 id=&#34;五v1-的四个坑一个连一个&#34;&gt;五、V1 的四个坑，一个连一个&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;坑一：Kafka 消费滞后。&lt;/strong&gt; lag 从秒涨到分钟再到小时。插桩后发现四个原因：分区数对消费组规模偏少、单次 fetch 拉取记录太少、socket 缓冲区没按吞吐调、跨区域读有额外延迟。对症：加分区、调大每次 fetch 的记录数（拿单条延迟换吞吐）、加大接收缓冲。结果 lag 降到峰值时也在一分钟内。&lt;strong&gt;然后新瓶颈暴露了&lt;/strong&gt;：实例自己吃不下更高的摄入速率——修一个瓶颈必然暴露下一个，这是分布式系统的常态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;坑二：热点节点与数据放大。&lt;/strong&gt; 最严重的问题。认证、推荐这类热门服务被几百个服务调用，流记录多几个数量级；最初按目的服务做一致性哈希归属，于是拥有热门服务的实例流量是别人的 100 倍。更糟的是洗牌时的数据放大：10 个实例都有本地客户端调某个服务，各自把 aggregator 发给 owner，owner 要合并 10 份。async-profiler 加 heap dump 的结论很清楚：热实例 CPU 大头在 GC，内存压力导致 GC 抖动，抖动吃 CPU，CPU 慢了内存压力更大——恶性循环直到实例宕机，负载转移引发级联故障。解法就是前面那套三级管线：Stage 1 先在内存里做 5 分钟窗口的在线聚合，原始流尽快丢弃被 GC，网络上传的只有压缩后的聚合结果；再经两轮哈希打散。为一个需求（中间件解析）做的架构决策，顺手解了另一个问题（负载不均）——作者说这叫有益的副作用。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;坑三：内存与 GC。&lt;/strong&gt; 修完热点还有高堆占用。原因有三：对象要等 5 分钟窗口攒完、数据在各级间做不必要的类型转换、以及——最有争议的一条——按 Scala 最佳实践用不可变数据结构做 aggregator，每次更新都造新对象，每秒百万记录下垃圾回收器直接被淹没。务实选择：&lt;strong&gt;热路径上改用可变 aggregator&lt;/strong&gt;（别处保持不可变），堆分配降了 50% 以上，GC 停顿从几百毫秒降到几十毫秒。教训：最佳实践是起点不是戒律，要用测量来背书，并且清醒地知道自己交换了什么。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;坑四：反应式流的复杂度。&lt;/strong&gt; Pekko Streams 管线会莫名其妙 stall，没有堆栈可查；&lt;code&gt;.async&lt;/code&gt; 操作符一开始用错，过度并行的流开销大于收益。应对：投入时间真正学懂需求驱动模型、把流图简化为线性流优先、在流边界加监控（缓冲区大小、元素吞吐、背压事件）、团队内部沉淀文档。强力抽象需要投资，别假设自己会了。&lt;/p&gt;
&lt;h2 id=&#34;六v2把能跑的打磨成生产级&#34;&gt;六、V2：把能跑的打磨成生产级&lt;/h2&gt;
&lt;p&gt;V1 解决了架构级问题，V2 是持续优化，模式都一样——生产暴露假设，profiling 定位根因，定点修复：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;aggregator 全程直达&lt;/strong&gt;：之前每级之间都把 aggregator 转成完整图实体，其实下级只需要压缩状态。改为只在 Stage 3 落库前转换一次，砍掉两次中间转换。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;序列化统一 JSON&lt;/strong&gt;：自定义序列化导致难以复现的偶发错误。JSON 比二进制略低效，但人可读让调试容易得多，一致性直接消灭了一整类 bug。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流参数调优&lt;/strong&gt;：继续调并行度、缓冲区、&lt;code&gt;.async&lt;/code&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;：应用健康、归属等元数据在 Stage 3 落库前补齐，避免查询时的 join 开销。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;七时间旅行任意时刻的拓扑重建&#34;&gt;七、时间旅行：任意时刻的拓扑重建&lt;/h2&gt;
&lt;p&gt;&amp;ldquo;事故发生时调用图长什么样？&amp;ldquo;全量快照存储成本爆炸，事件溯源回放太慢。方案是三件套组合：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;时间窗 aggregator 快照&lt;/strong&gt;：每个 aggregator 带 &lt;code&gt;startTs&lt;/code&gt; 和 &lt;code&gt;endTs&lt;/code&gt;，按 entity_id 加时间戳存进图数据库，相当于每 5 分钟一个检查点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;属性级变更追踪&lt;/strong&gt;：图数据库只存变化过的属性加时间戳，比全实体拷贝省得多，还提供了窗口边界之内的精度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查询时重建&lt;/strong&gt;：查时间范围内的 mutation 历史，按序应用，重建拓扑状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;外加一个聪明设计：&lt;strong&gt;查询时再聚合&lt;/strong&gt;复用摄入时的同一套 aggregator 类，可以做没预计算过的任意 groupby 维度（可用性层级、业务域、部署集群），支持探索式分析而不撑爆存储。&lt;/p&gt;
&lt;h2 id=&#34;八原文给的三条通用教训&#34;&gt;八、原文给的三条通用教训&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;规模定性地改变一切&lt;/strong&gt;：每秒 100 次可行的东西在每秒 10 万次失败，变化不是线性的是质变的。不可变结构、单级聚合、gRPC，在小规模下全是对的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一次只优化一个瓶颈&lt;/strong&gt;：修 Kafka lag 暴露热点，修热点暴露 GC，修 GC 暴露序列化。这不是失败，是复杂系统的本性。按影响排序，修透一个再修下一个。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分布是扩展性的关键&lt;/strong&gt;：一致性哈希能分散负载，但挡不住数据本身的幂律分布倾斜。倾斜数据在规模下要多级处理、逐级重新分布。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;背压是降级策略，不是性能特性。&lt;/strong&gt; 它的价值不在快，在于&amp;quot;慢了也不死不丢&amp;rdquo;。做异步管线时先想清楚：下游慢了，数据去哪？答案如果是&amp;quot;内存里堆着&amp;quot;或&amp;quot;丢掉&amp;rdquo;，早晚要还。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据分区方式决定架构形状。&lt;/strong&gt; IPC 一级聚合、网络流三级聚合，差异全在数据进来时分没分对。设计管线前先问：处理这个 key 需要的数据，天然在一起吗？不在，就认命加 shuffle 级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;对&amp;quot;最佳实践&amp;quot;保持工程师式的不敬。&lt;/strong&gt; 不可变数据结构、gRPC、二进制序列化，三个正确常识在这套系统里全被推翻了——但每次推翻都有 profiling 数据背书，且范围克制（可变只用于热路径）。违反常识不可怕，凭感觉违反才可怕。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优化是一条链，不是一次战役。&lt;/strong&gt; 修一个瓶颈暴露下一个，这是吞吐上升的必然结果。所以别指望&amp;quot;性能优化专项&amp;quot;一劳永逸，把 profiling 工具链（async-profiler、heap dump、GC 日志）和边界监控变成日常设施才是正解。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;白嫖现有基础设施是高级的架构品味。&lt;/strong&gt; 拿服务注册中心当集群成员来源，零协调成本拿到动态一致性哈希。新组件引入前先盘一遍手里已有的东西能替代什么——他们连消息队列都因此省掉了。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心架构&lt;/strong&gt;：流式优先加背压（Kafka 当缓冲池，慢下来但不丢不崩）；网络、IPC、追踪三层物理隔离存储，查询时并行合并。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三级聚合管线&lt;/strong&gt;：Stage 1 时间窗聚合、Stage 2 按中间件 shuffle 并解析双跳、Stage 3 富化持久化。一个设计同时解决中间件解析和热点节点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键妥协&lt;/strong&gt;：热路径可变数据结构（堆分配降 50%+）、gRPC 换 SSE、序列化统一 JSON——全是测量驱动的反常识决策。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间旅行&lt;/strong&gt;：窗口快照加属性级 mutation 加查询时重建，任意时刻拓扑可查，还能查询时再聚合做任意维度 groupby。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方法论&lt;/strong&gt;：规模改变结论；瓶颈是瀑布，修一个露一个；优化是连续过程，测量是唯一指南针。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/building-service-topology-at-scale-architecture-challenges-and-lessons-learned-f4b792f3f0d8&#34;&gt;Building Service Topology at Scale: Architecture, Challenges, and Lessons Learned&lt;/a&gt;（Netflix TechBlog，作者 Parth Jain、Rakesh Sukumar、Yingwu Zhao、Renzo Sanchez-Silva、Nathan Fisher）。&lt;/li&gt;
&lt;li&gt;文中涉及的关键技术：&lt;a href=&#34;https://pekko.apache.org/&#34;&gt;Apache Pekko&lt;/a&gt;（反应式流实现，Akka fork）、&lt;a href=&#34;https://github.com/async-profiler/async-profiler&#34;&gt;async-profiler&lt;/a&gt;（JVM 采样分析器，热点与 GC 根因定位的主力工具）、&lt;a href=&#34;https://www.reactive-streams.org/&#34;&gt;Reactive Streams&lt;/a&gt;（背压协议规范）。&lt;/li&gt;
&lt;li&gt;系列第一篇介绍多源拓扑的 what 与 why，第三篇将讲追踪层与统一查询，均在 Netflix TechBlog 连载。&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Netflix 实时服务拓扑图笔记：把「谁依赖谁」从考古变成活地图</title>
      <link>https://ftmao.com/blog/posts/netflix-service-topology-map/</link>
      <pubDate>Fri, 29 May 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-service-topology-map/</guid>
      <description>&lt;h1 id=&#34;netflix-实时服务拓扑图笔记把谁依赖谁从考古变成活地图&#34;&gt;Netflix 实时服务拓扑图笔记：把「谁依赖谁」从考古变成活地图&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/from-silos-to-service-topology-why-netflix-built-a-real-time-service-map-0165ba13a7bc&#34;&gt;From Silos to Service Topology: Why Netflix Built a Real-Time Service Map&lt;/a&gt;（Netflix Tech Blog，2026-05-29，作者 Parth Jain 等）
目标读者：维护过微服务系统、半夜被告警叫醒过、对着一堆监控面板猜「到底谁挂了」的后端工程师。
阅读目标：搞清楚 Netflix 为什么认为「依赖关系」值得单独立项做一套系统，三路数据源怎么互补，以及那套「网络中间人解析」管线在解决什么问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一问题信号很多地图没有&#34;&gt;一、问题：信号很多，地图没有&lt;/h2&gt;
&lt;p&gt;原文开场是经典的凌晨三点剧本：工程师被 page 醒，某个关键服务错误率升高，用户观影体验受损。在几千个微服务支撑的系统里，这时候要回答的问题其实非常朴素，但谁也答不快。&lt;/p&gt;
&lt;p&gt;Netflix 做了一件值得学的事：&lt;strong&gt;他们没有拍脑袋立项，而是分析了四年间工程师提交的数千条支持请求&lt;/strong&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;为什么这个服务在我的指标里显示为「Unknown」？&lt;/li&gt;
&lt;li&gt;最近我的调用链路上有什么变化，能解释这个行为吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;全是依赖问题。而传统可观测性三件套各自只给碎片：指标告诉你症状，日志告诉你单个服务在干嘛，追踪告诉你单次请求怎么流的——&lt;strong&gt;没有一样东西能告诉你稳态下整张依赖网长什么样&lt;/strong&gt;。工程师得在凌晨三点用脑子把多个工具的信息缝起来，慢、易错、压力大。&lt;/p&gt;
&lt;p&gt;这里有个判断很准：缺的不是信号，是&lt;strong&gt;关系&lt;/strong&gt;。信号再多，回答不了「谁连着谁」这种结构性问题。&lt;/p&gt;
&lt;h2 id=&#34;二踩过的坑五条教训&#34;&gt;二、踩过的坑：五条教训&lt;/h2&gt;
&lt;p&gt;Netflix 不是第一次干这件事。原文坦承他们试过外部图数据库、商业平台、各种存储和内部原型，每轮迭代换来一条教训：&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;：能装几千节点的方案，到 Netflix 的服务数量和流量规模会撞墙。&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;：网络连接数据缺应用上下文，应用指标只覆盖做了埋点的服务，必须多源合并。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这五条几乎条条是钱烧出来的，尤其是第四条——&lt;strong&gt;一个会撒谎的地图比没有地图更害人&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;三核心设计三个真相来源三张物理隔离的图&#34;&gt;三、核心设计：三个真相来源，三张物理隔离的图&lt;/h2&gt;
&lt;p&gt;这是全文最有分量的一个架构决策。Netflix 的结论是：&lt;strong&gt;没有任何单一数据源能讲完整的依赖故事&lt;/strong&gt;，所以他们用三路互补的数据各自建一张图：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph SRC[&amp;#34;三路数据源&amp;#34;]
        EB[&amp;#34;eBPF 网络流&amp;lt;br/&amp;gt;内核态抓真实连接&amp;#34;]
        IPC[&amp;#34;IPC 指标&amp;lt;br/&amp;gt;应用埋点 gRPC GraphQL REST&amp;#34;]
        TR[&amp;#34;端到端追踪&amp;lt;br/&amp;gt;采样后的真实请求路径&amp;#34;]
    end
    subgraph G[&amp;#34;三张物理隔离的图&amp;#34;]
        G1[&amp;#34;网络图&amp;lt;br/&amp;gt;图数据库分区一&amp;lt;br/&amp;gt;覆盖全 但无应用语义&amp;#34;]
        G2[&amp;#34;应用图&amp;lt;br/&amp;gt;图数据库分区二&amp;lt;br/&amp;gt;有端点细节 但只覆盖已埋点服务&amp;#34;]
        G3[&amp;#34;请求图&amp;lt;br/&amp;gt;列式存储&amp;lt;br/&amp;gt;真实行为 但有采样盲区&amp;#34;]
    end
    EB --&amp;gt; G1
    IPC --&amp;gt; G2
    TR --&amp;gt; G3
    G1 --&amp;gt; U[&amp;#34;统一视图&amp;lt;br/&amp;gt;并行查询三路 合并节点与边&amp;lt;br/&amp;gt;亚秒级返回&amp;#34;]
    G2 --&amp;gt; U
    G3 --&amp;gt; U
    style U fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三路的价值与短板正好互补：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;eBPF 网络流（网络层）&lt;/strong&gt;：在内核层抓连接记录，&lt;strong&gt;不管应用有没有埋点，每个服务都在图上&lt;/strong&gt;——这是完整性的地基。短板是没有应用上下文：你知道 A 连了 B 的 IP 和协议，但不知道调的是哪个 API 端点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPC 指标（应用层）&lt;/strong&gt;：埋点服务发出的进程间通信指标，能看到具体端点、错误率、延迟分布、协议细节。短板是&lt;strong&gt;只覆盖做了埋点的服务&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端到端追踪（请求层）&lt;/strong&gt;：不是「A 可以调 B」，而是「A 在处理这个具体请求时确实调了 B」，能捕获条件分支和特性开关带来的真实运行时行为。短板是&lt;strong&gt;采样&lt;/strong&gt;——冷门代码路径在聚合视图里可能看不见。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工程实现上有个细节值得注意：三张图&lt;strong&gt;物理隔离&lt;/strong&gt;——网络层和 IPC 层各在图数据库的一个分区，追踪层用列式存储（为分析型查询优化）。隔离换来两个好处：各层独立演进，查询时可以三路并行、最后合并结果，即使三层全合并也能做到亚秒返回。&lt;/p&gt;
&lt;p&gt;这个设计翻译过来就是一句话：&lt;strong&gt;完整性靠网络流兜底，语义靠 IPC 补齐，行为真实性靠追踪验证&lt;/strong&gt;。任何单源方案都在这三者里缺一角。&lt;/p&gt;
&lt;h2 id=&#34;四数据管线三级聚合与中间人解析&#34;&gt;四、数据管线：三级聚合与「中间人解析」&lt;/h2&gt;
&lt;p&gt;架构层面，流日志从 Kafka 到可查的图要走这样一条管线：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    K[&amp;#34;多区域 Kafka&amp;lt;br/&amp;gt;持续消费流日志&amp;#34;] --&amp;gt; P[&amp;#34;Pekko Streams&amp;lt;br/&amp;gt;分布式处理 自动分区&amp;lt;br/&amp;gt;天然背压&amp;#34;]
    P --&amp;gt; S1[&amp;#34;第一级 初始聚合&amp;lt;br/&amp;gt;从 Kafka 进来的原始流&amp;#34;]
    S1 --&amp;gt; S2[&amp;#34;第二级 中间人解析&amp;lt;br/&amp;gt;识别 LB NAT 网关 代理&amp;lt;br/&amp;gt;把两跳拼回一跳&amp;#34;]
    S2 --&amp;gt; S3[&amp;#34;第三级 最终聚合&amp;lt;br/&amp;gt;叠加健康状态&amp;#34;]
    S3 --&amp;gt; DB[&amp;#34;图数据库&amp;lt;br/&amp;gt;建在分布式 KV 之上&amp;lt;br/&amp;gt;高吞吐多跳遍历&amp;#34;]
    DB --&amp;gt; API[&amp;#34;gRPC API&amp;lt;br/&amp;gt;多跳遍历 按层过滤&amp;lt;br/&amp;gt;分页 亚秒响应&amp;#34;]
    style S2 fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三个点值得展开：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;处理框架用 Apache Pekko Streams&lt;/strong&gt;（Akka 的 fork），看重的是分布式容错、自动把负载分区到 Auto Scaling Group、以及响应式流自带的背压处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三级聚合里最聪明的是第二级&lt;/strong&gt;。网络流日志记录的是逐跳的物理连接：&lt;code&gt;App A → 负载均衡器 → App B&lt;/code&gt;，或者经过 NAT 网关。工程师要的不是这个，是 &lt;code&gt;App A → App B&lt;/code&gt; 这条应用级直连边。第二级负责识别出负载均衡器、NAT 网关、API 网关、代理这些「网络中间人」，把它们的入向流和出向流拼起来，还原出真正的应用到应用路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分三级还有个副作用：防热点&lt;/strong&gt;。当某些应用或中间人的流量是其他节点的上百倍时，把聚合分散到多个阶段执行，热点不会压垮单点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;查询侧是 gRPC API：多跳遍历、按可用性等级（Tier 0、Tier 1 之类）和业务域过滤、大结果集分页、亚秒响应。UI 和 API 并重——&lt;strong&gt;工程师用界面排查，自动化系统（韧性框架、爆炸半径计算器、事故响应机器人）走 API&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;五工程师拿到了什么&#34;&gt;五、工程师拿到了什么&lt;/h2&gt;
&lt;p&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;跳信号&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;：拓扑上直接叠加哪些服务正在出问题，快速判断「我看到的故障是不是从别处传过来的」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;程序化查询&lt;/strong&gt;：Platform Modernization 团队用它验证直播关键服务的整条依赖链都打了正确的可用性等级标记——拓扑成了合规校验的数据源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间旅行&lt;/strong&gt;：查历史上某个时间点的拓扑长什么样。实现上没有每个时间片存一份（存储会爆炸），而是用&lt;strong&gt;各层专用的聚合器跨时间窗口累积&lt;/strong&gt;，需要历史视图时再重建。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后这条「时间旅行」是我觉得最有味道的：事故排查的黄金问题「&lt;strong&gt;故障开始前，我的依赖图变了什么&lt;/strong&gt;」终于被变成一条查询，而不是靠翻部署记录和记忆。&lt;/p&gt;
&lt;h2 id=&#34;六活的地图和下一步&#34;&gt;六、活的地图，和下一步&lt;/h2&gt;
&lt;p&gt;Netflix 反复强调「living map」这个词：新调用关系出现就近实时上图，调用停止的边会从图里淡出，部署改变行为就改变图。关键承诺是——&lt;strong&gt;工程师可以相信图上画的就是现实，而不是某个人对架构的想象&lt;/strong&gt;。&lt;/p&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;：一个智能体持续爬拓扑图、跨依赖关联故障、理解历史模式、自动给出疑似根因——&lt;strong&gt;拓扑图是这类智能自动化的知识图谱地基&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第三条值得划线：他们不是在说「接个 LLM 上去问答」，而是在说「先把依赖关系建成结构化、实时、可查询的图，智能体才有爬的地面」。这个顺序是对的。&lt;/p&gt;
&lt;h2 id=&#34;七工程实践联想&#34;&gt;七、工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;立项之前先做求助记录考古&lt;/strong&gt;。Netflix 拿四年的支持工单说话，高频问题清单就是需求文档。多数内部平台立项靠的是平台团队自己的痛感，而不是用户的——「我觉得你需要」和「数据说你在问」做出来的东西，采纳率天差地别。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「信号够多」不等于「可观测」&lt;/strong&gt;。指标、日志、追踪三件套回答的都是单点或单次的问题，依赖关系这种结构性知识需要单独建模、单独存储。如果你的团队排查故障还在靠「老张知道这条链路」，那就是缺这一层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多源互补是处理「讲不全」问题的标准答案&lt;/strong&gt;。任何单一采集手段都有盲区：埋点有覆盖率问题，内核抓包有语义问题，采样有冷门路径问题。与其追求一个完美的源，不如让几个不完美的源互相兜底——这个思路对做 AI Agent 的上下文采集同样成立。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「中间人解析」是被低估的脏活&lt;/strong&gt;。基础设施里到处是 LB、网关、代理，原始数据永远带着这些噪音。谁能把物理路径还原成逻辑路径，谁的数据才可行动。做链路分析、做 RAG 的数据清洗，都会遇到同构的问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图建好了，Agent 才有事可做&lt;/strong&gt;。Netflix 把自动根因分析放在路线图的末尾而不是开头，因为智能体的前提是有一张实时、准确、可编程查询的图。这对「AI + 运维」的喧嚣是一盆冷水：先修地基，再谈智能。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：微服务规模下，凌晨三点的工程师回答不了「谁依赖谁、爆炸半径多大、故障源在哪」；四年数千条工单证明这是最痛的缺口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案&lt;/strong&gt;：三路数据源各建一图、物理隔离、并行查询后合并——eBPF 网络流保完整性，IPC 指标保应用语义，分布式追踪保真实行为。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管线&lt;/strong&gt;：多区域 Kafka → Pekko Streams → 三级聚合（核心是第二级的网络中间人解析，把 LB/NAT/代理的两跳还原成应用级直连边）→ 图数据库（建在分布式 KV 上）→ 亚秒级 gRPC API。&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;：这是系列第一篇，下一篇讲规模化落地的工程硬题——Kafka 消费延迟、GC 停顿、响应式流调试、热点治理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://netflixtechblog.com/from-silos-to-service-topology-why-netflix-built-a-real-time-service-map-0165ba13a7bc&#34;&gt;From Silos to Service Topology: Why Netflix Built a Real-Time Service Map&lt;/a&gt;（Netflix Technology Blog，2026-05-29，原文）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://ebpf.io/&#34;&gt;eBPF 官方文档&lt;/a&gt;（内核态可编程观测技术，网络层数据源的基础）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pekko.apache.org/&#34;&gt;Apache Pekko&lt;/a&gt;（Akka 的开源 fork，文中分布式流处理框架）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://opentelemetry.io/docs/concepts/signals/traces/&#34;&gt;OpenTelemetry Tracing&lt;/a&gt;（分布式追踪的通用概念背景，文中请求层数据源对应物）&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>视频系统 DDD 实战：巴西 IPTV 项目的领域拆分</title>
      <link>https://ftmao.com/blog/posts/iptv/</link>
      <pubDate>Fri, 23 Aug 2024 17:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/iptv/</guid>
      <description>&lt;h1 id=&#34;视频系统-ddd-实战巴西-iptv-项目的领域拆分&#34;&gt;视频系统 DDD 实战：巴西 IPTV 项目的领域拆分&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;2021 年到 2025 年，我负责面向巴西市场的 IPTV 视频点播系统。这是一个用户量过百万、业务持续演进的系统。在几次大的产品迭代中，我们引入了领域驱动设计（DDD）来控制系统复杂度。这篇文章会完整复盘当时的领域拆分过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一项目背景&#34;&gt;一、项目背景&lt;/h2&gt;
&lt;p&gt;系统核心功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;视频上传：支持 tus / FTP / 网盘 / m3u8 等多种来源&lt;/li&gt;
&lt;li&gt;视频转码：h264 / h265 多码率输出&lt;/li&gt;
&lt;li&gt;DRM 加密：永新 / 自研加密方案&lt;/li&gt;
&lt;li&gt;元数据管理：同步 IMDb / TMDB / PP 等外部数据源&lt;/li&gt;
&lt;li&gt;播放列表：专题、分类、推荐&lt;/li&gt;
&lt;li&gt;播放鉴权：用户权限、设备管理、防盗链&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务用户：&lt;strong&gt;200 万+&lt;/strong&gt;，峰值日活 &lt;strong&gt;40 万&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二为什么需要-ddd&#34;&gt;二、为什么需要 DDD？&lt;/h2&gt;
&lt;h3 id=&#34;21-早期的问题&#34;&gt;2.1 早期的问题&lt;/h3&gt;
&lt;p&gt;系统最初是一个单体应用，模块按技术分层组织：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;controller/
service/
dao/
entity/&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;业务逻辑散落在 Service 中，相似功能重复实现&lt;/li&gt;
&lt;li&gt;转码、鉴权、元数据互相耦合，改一个模块影响多个模块&lt;/li&gt;
&lt;li&gt;新成员上手慢，需要全局理解才敢改代码&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;22-ddd-带来的改变&#34;&gt;2.2 DDD 带来的改变&lt;/h3&gt;
&lt;p&gt;DDD 不是银弹，但它帮我们做了两件事：&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;hr&gt;
&lt;h2 id=&#34;三领域划分&#34;&gt;三、领域划分&lt;/h2&gt;
&lt;h3 id=&#34;31-核心域识别&#34;&gt;3.1 核心域识别&lt;/h3&gt;
&lt;p&gt;我们先识别出系统的核心业务域：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    content[&amp;#34;内容域 (Content)&amp;lt;br/&amp;gt;媒资管理、视频文件、转码任务、DRM 加密&amp;#34;]
    metadata[&amp;#34;元数据域 (Metadata)&amp;lt;br/&amp;gt;影片信息、演员、分类、标签、外部数据源同步&amp;#34;]
    playback[&amp;#34;播放域 (Playback)&amp;lt;br/&amp;gt;播放鉴权、播放地址生成、防盗链、码率适配&amp;#34;]
    user[&amp;#34;用户域 (User)&amp;lt;br/&amp;gt;用户账户、订阅、设备、播放历史&amp;#34;]
    recommendation[&amp;#34;推荐域 (Recommendation)&amp;lt;br/&amp;gt;播放列表、专题、个性化推荐&amp;#34;]
    billing[&amp;#34;计费域 (Billing)&amp;lt;br/&amp;gt;套餐、订单、支付、用量统计&amp;#34;]
    content --- metadata --- playback --- user --- recommendation --- billing&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;32-域间关系&#34;&gt;3.2 域间关系&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    playback[&amp;#34;播放域&amp;#34;] -- 依赖 --&amp;gt; content[&amp;#34;内容域&amp;#34;]
    playback -- 依赖 --&amp;gt; user[&amp;#34;用户域&amp;#34;]
    recommendation[&amp;#34;推荐域&amp;#34;] -- 依赖 --&amp;gt; metadata[&amp;#34;元数据域&amp;#34;]
    recommendation -- &amp;#34;依赖（获取热门数据）&amp;#34; --&amp;gt; playback
    billing[&amp;#34;计费域&amp;#34;] -- 依赖 --&amp;gt; user
    metadata -- 依赖 --&amp;gt; external[&amp;#34;外部数据源&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;依赖方向尽量保持单向，避免循环依赖。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四内容域详细设计&#34;&gt;四、内容域详细设计&lt;/h2&gt;
&lt;p&gt;内容域是最复杂的域之一，我们以它为例展开。&lt;/p&gt;
&lt;h3 id=&#34;41-聚合根设计&#34;&gt;4.1 聚合根设计&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    root[&amp;#34;MediaAsset（媒资聚合根）&amp;#34;]
    file[&amp;#34;MediaFile（视频文件）&amp;#34;]
    raw[&amp;#34;原始文件&amp;#34;]
    transcoded[&amp;#34;转码后文件（多码率）&amp;#34;]
    drmfile[&amp;#34;DRM 加密文件&amp;#34;]
    job[&amp;#34;TranscodingJob（转码任务）&amp;#34;]
    license[&amp;#34;DrmLicense（DRM 许可证信息）&amp;#34;]
    root --&amp;gt; file
    file --&amp;gt; raw
    file --&amp;gt; transcoded
    file --&amp;gt; drmfile
    root --&amp;gt; job
    root --&amp;gt; license&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;MediaAsset&lt;/code&gt; 是聚合根，外部系统只能通过它访问内部实体。&lt;/p&gt;
&lt;h3 id=&#34;42-关键领域事件&#34;&gt;4.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;th&gt;订阅方&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;MediaAssetUploaded&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;TranscodingCompleted&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;DrmEncryptionCompleted&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;DRM 加密完成&lt;/td&gt;
					&lt;td&gt;播放域&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;MetadataSynced&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;MediaAssetPublished&lt;/code&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;h3 id=&#34;43-状态流转&#34;&gt;4.3 状态流转&lt;/h3&gt;
&lt;p&gt;媒资从上传到发布的状态机：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    UPLOADED --&amp;gt; PENDING_TRANSCODE --&amp;gt; TRANSCODING --&amp;gt; PENDING_DRM --&amp;gt; DRM_ENCRYPTED --&amp;gt; PENDING_METADATA --&amp;gt; METADATA_READY --&amp;gt; FINAL[&amp;#34;PUBLISHED / REJECTED&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;每个状态变更都通过领域事件触发下一步操作。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五分层架构&#34;&gt;五、分层架构&lt;/h2&gt;
&lt;p&gt;每个领域内部采用 DDD 经典四层架构：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    ui[&amp;#34;用户接口层&amp;lt;br/&amp;gt;REST Controller / DTO&amp;#34;]
    app[&amp;#34;应用层&amp;lt;br/&amp;gt;Application Service / 用例编排&amp;#34;]
    domain[&amp;#34;领域层&amp;lt;br/&amp;gt;Entity / Value Object / Domain Service / Repository Interface / Domain Event&amp;#34;]
    infra[&amp;#34;基础设施层&amp;lt;br/&amp;gt;Repository Impl / MQ Client / External API Client / DB / Cache&amp;#34;]
    ui --- app --- domain --- infra&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;51-领域层示例&#34;&gt;5.1 领域层示例&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MediaAsset&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; MediaAssetId id;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; String title;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; MediaStatus status;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;MediaFile&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; files;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;TranscodingJob&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; transcodingJobs;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;upload&lt;/span&gt;(MediaFile file) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;files&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(file);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; MediaStatus.&lt;span style=&#34;color:#a6e22e&#34;&gt;UPLOADED&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        registerEvent(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; MediaAssetUploadedEvent(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;completeTranscoding&lt;/span&gt;(TranscodingJob job) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;transcodingJobs&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(job);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; MediaStatus.&lt;span style=&#34;color:#a6e22e&#34;&gt;TRANSCODING_COMPLETED&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        registerEvent(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; TranscodingCompletedEvent(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;, job.&lt;span style=&#34;color:#a6e22e&#34;&gt;getOutputFiles&lt;/span&gt;()));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-防腐层设计&#34;&gt;5.2 防腐层设计&lt;/h3&gt;
&lt;p&gt;与外部系统（IMDb / TMDB / PP）对接时，使用防腐层（Anti-Corruption Layer）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MetadataSourceClient&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ExternalMetadata &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(String externalId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// IMDb 适配器&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ImdbClientAdapter&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;implements&lt;/span&gt; MetadataSourceClient {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; ImdbApiClient imdbClient;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; ExternalMetadata &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(String externalId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ImdbResponse response &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; imdbClient.&lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(externalId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; MetadataConverter.&lt;span style=&#34;color:#a6e22e&#34;&gt;fromImdb&lt;/span&gt;(response);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这样外部 API 的数据结构变化不会直接影响到我们内部的元数据模型。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六微服务拆分过程&#34;&gt;六、微服务拆分过程&lt;/h2&gt;
&lt;h3 id=&#34;61-第一次拆分按技术边界&#34;&gt;6.1 第一次拆分：按技术边界&lt;/h3&gt;
&lt;p&gt;早期我们按技术栈拆：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;upload-service&lt;/li&gt;
&lt;li&gt;transcoding-service&lt;/li&gt;
&lt;li&gt;metadata-service&lt;/li&gt;
&lt;li&gt;playback-service&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结果导致一个业务操作要跨多个服务，分布式事务复杂。&lt;/p&gt;
&lt;h3 id=&#34;62-第二次拆分按领域边界&#34;&gt;6.2 第二次拆分：按领域边界&lt;/h3&gt;
&lt;p&gt;调整后按 DDD 领域拆分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;content-service（内容域）&lt;/li&gt;
&lt;li&gt;metadata-service（元数据域）&lt;/li&gt;
&lt;li&gt;playback-service（播放域）&lt;/li&gt;
&lt;li&gt;user-service（用户域）&lt;/li&gt;
&lt;li&gt;recommendation-service（推荐域）&lt;/li&gt;
&lt;li&gt;billing-service（计费域）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个服务有清晰的业务边界和聚合根。&lt;/p&gt;
&lt;h3 id=&#34;63-服务间通信&#34;&gt;6.3 服务间通信&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;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;RocketMQ&lt;/td&gt;
					&lt;td&gt;异步、最终一致&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;同步查询&lt;/td&gt;
					&lt;td&gt;HTTP / gRPC&lt;/td&gt;
					&lt;td&gt;强一致查询&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;配置/字典数据&lt;/td&gt;
					&lt;td&gt;Redis 缓存&lt;/td&gt;
					&lt;td&gt;高频读取&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七领域事件实现&#34;&gt;七、领域事件实现&lt;/h2&gt;
&lt;h3 id=&#34;71-事件定义&#34;&gt;7.1 事件定义&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TranscodingCompletedEvent&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;extends&lt;/span&gt; DomainEvent {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; MediaAssetId mediaAssetId;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;FileUrl&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; outputFiles;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; Instant completedAt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;72-事件发布&#34;&gt;7.2 事件发布&lt;/h3&gt;
&lt;p&gt;领域层只负责注册事件，应用层统一发布：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Transactional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;handleTranscodingCallback&lt;/span&gt;(TranscodingJobId jobId, List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;FileUrl&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; outputs) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    MediaAsset asset &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; mediaAssetRepository.&lt;span style=&#34;color:#a6e22e&#34;&gt;findByJobId&lt;/span&gt;(jobId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    asset.&lt;span style=&#34;color:#a6e22e&#34;&gt;completeTranscoding&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; TranscodingJob(jobId, outputs));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    mediaAssetRepository.&lt;span style=&#34;color:#a6e22e&#34;&gt;save&lt;/span&gt;(asset);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    eventPublisher.&lt;span style=&#34;color:#a6e22e&#34;&gt;publish&lt;/span&gt;(asset.&lt;span style=&#34;color:#a6e22e&#34;&gt;getDomainEvents&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    asset.&lt;span style=&#34;color:#a6e22e&#34;&gt;clearDomainEvents&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;73-事件消费&#34;&gt;7.3 事件消费&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PlaybackEventListener&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@RocketMQMessageListener&lt;/span&gt;(topic &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;content-events&amp;#34;&lt;/span&gt;, selectorExpression &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TranscodingCompleted&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Listener&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;implements&lt;/span&gt; RocketMQListener&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;TranscodingCompletedEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onMessage&lt;/span&gt;(TranscodingCompletedEvent event) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            playbackService.&lt;span style=&#34;color:#a6e22e&#34;&gt;preparePlayUrls&lt;/span&gt;(event.&lt;span style=&#34;color:#a6e22e&#34;&gt;getMediaAssetId&lt;/span&gt;(), event.&lt;span style=&#34;color:#a6e22e&#34;&gt;getOutputFiles&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;八几次关键演进&#34;&gt;八、几次关键演进&lt;/h2&gt;
&lt;h3 id=&#34;81-从单体到领域化单体&#34;&gt;8.1 从单体到领域化单体&lt;/h3&gt;
&lt;p&gt;第一步不是拆微服务，而是先在单体内部按领域组织代码包：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;com.ftmao.iptv.content
com.ftmao.iptv.metadata
com.ftmao.iptv.playback&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这样可以在不引入分布式复杂度的前提下，先验证领域划分是否合理。&lt;/p&gt;
&lt;h3 id=&#34;82-从同步调用到事件驱动&#34;&gt;8.2 从同步调用到事件驱动&lt;/h3&gt;
&lt;p&gt;转码完成后，原本播放域通过接口轮询内容域查询状态。后来改为内容域发布 &lt;code&gt;TranscodingCompleted&lt;/code&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;系统扩展性更好&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;83-引入-cqrs-处理播放查询&#34;&gt;8.3 引入 CQRS 处理播放查询&lt;/h3&gt;
&lt;p&gt;播放接口对性能要求很高，我们单独做了读模型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写模型：MediaAsset 聚合根&lt;/li&gt;
&lt;li&gt;读模型：Redis 中的播放信息缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当内容发布或更新时，通过事件同步更新读模型。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九踩过的坑&#34;&gt;九、踩过的坑&lt;/h2&gt;
&lt;h3 id=&#34;91-领域划分过细&#34;&gt;9.1 领域划分过细&lt;/h3&gt;
&lt;p&gt;早期我们划分了 10 多个领域，导致服务数量爆炸，运维成本剧增。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：领域数量要匹配团队规模。小团队 4-6 个领域足够。&lt;/p&gt;
&lt;h3 id=&#34;92-领域事件丢失&#34;&gt;9.2 领域事件丢失&lt;/h3&gt;
&lt;p&gt;消息队列配置不当，出现过领域事件丢失，导致播放域看不到新发布的内容。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：关键领域事件必须持久化，支持重放和补偿。&lt;/p&gt;
&lt;h3 id=&#34;93-贫血领域模型&#34;&gt;9.3 贫血领域模型&lt;/h3&gt;
&lt;p&gt;很多「DDD」项目最后变成了 Transaction Script 套壳，实体只有 getter/setter。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：要把业务规则和状态变更放在领域对象里，而不是 Service 里。&lt;/p&gt;
&lt;h3 id=&#34;94-过度设计聚合根&#34;&gt;9.4 过度设计聚合根&lt;/h3&gt;
&lt;p&gt;一开始想把所有内容相关的东西都放到 &lt;code&gt;MediaAsset&lt;/code&gt; 聚合根里，结果聚合根过大，事务边界混乱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：聚合根要小而聚焦，跨聚合用领域事件协作。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十效果与反思&#34;&gt;十、效果与反思&lt;/h2&gt;
&lt;h3 id=&#34;101-量化效果&#34;&gt;10.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;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;40 个&lt;/td&gt;
					&lt;td&gt;20 个&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;线上因耦合导致的 Bug&lt;/td&gt;
					&lt;td&gt;10 次/月&lt;/td&gt;
					&lt;td&gt;2 次/月&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;新成员独立上手时间&lt;/td&gt;
					&lt;td&gt;3 周&lt;/td&gt;
					&lt;td&gt;1 周&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;102-核心反思&#34;&gt;10.2 核心反思&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DDD 不是先拆分微服务，而是先拆清楚业务边界&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;，先有一个可用的模型，再迭代优化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DDD 需要团队共识&lt;/strong&gt;，不是架构师一个人画几张图就行的。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十一总结&#34;&gt;十一、总结&lt;/h2&gt;
&lt;p&gt;巴西 IPTV 项目让我对 DDD 有了真实的体感。它不是教科书上的概念，而是解决复杂业务系统演化问题的工程方法。&lt;/p&gt;
&lt;p&gt;对于视频系统这类业务复杂、流程长、多团队协作的系统，DDD 能带来的最大价值是：&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;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你也在做 DDD 实践，欢迎在评论区交流领域划分和事件驱动的经验。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>starmall：一个微信小程序电商系统的全栈实战——预约、支付与订单架构解析</title>
      <link>https://ftmao.com/blog/posts/starmall-wechat-miniprogram-ecommerce-fullstack/</link>
      <pubDate>Tue, 20 Jul 2021 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/starmall-wechat-miniprogram-ecommerce-fullstack/</guid>
      <description>&lt;h1 id=&#34;starmall一个微信小程序电商系统的全栈实战预约支付与订单架构解析&#34;&gt;starmall：一个微信小程序电商系统的全栈实战——预约、支付与订单架构解析&lt;/h1&gt;
&lt;p&gt;这是我 2021 年做的一套完整电商系统 &lt;strong&gt;starmall&lt;/strong&gt;（六爻星辰）：微信小程序端 + Java 后端 + PC 管理后台，支持商品购买、老师咨询时段预约、择吉日查询、在线咨询等业务。整理开源出来，写篇文章做一次完整的技术复盘。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前端（uni-app 小程序）：&lt;a href=&#34;https://gitee.com/soulnew/starmall&#34;&gt;https://gitee.com/soulnew/starmall&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;后端（Java + 管理端）：&lt;a href=&#34;https://gitee.com/soulnew/crm_server&#34;&gt;https://gitee.com/soulnew/crm_server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;技术栈：Spring Boot 2.2 + MyBatis-Plus + MySQL + Redis / uni-app + uView / Vue 2 + Element UI（管理端）&lt;/li&gt;
&lt;li&gt;说明：后端是在开源商城 &lt;strong&gt;CRMEB Java 版&lt;/strong&gt; 基础上二次开发的，分销/秒杀/拼团/砍价等营销能力来自底座，预约系统、择吉日、名片、咨询聊天等是自研部分&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一整体架构&#34;&gt;一、整体架构&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    MP[&amp;#34;微信小程序端&amp;lt;br/&amp;gt;starmall uni-app + uView&amp;#34;] --&amp;gt;|&amp;#34;https://ftmao.com/api/front/**&amp;#34;| API[&amp;#34;SpringBoot 单体后端&amp;lt;br/&amp;gt;62 个 Mapper / 23 个前台 Controller&amp;#34;]
    H5[&amp;#34;H5 / App&amp;lt;br/&amp;gt;uni-app 多端编译&amp;#34;] --&amp;gt; API
    ADMIN[&amp;#34;PC 管理后台&amp;lt;br/&amp;gt;Vue 2 + Element UI + ECharts&amp;#34;] --&amp;gt;|&amp;#34;后台接口&amp;#34;| API
    API --&amp;gt; MYSQL[&amp;#34;MySQL 5.7&amp;lt;br/&amp;gt;62 张表 Druid 连接池&amp;#34;]
    API --&amp;gt; REDIS[&amp;#34;Redis&amp;lt;br/&amp;gt;token / 订单缓存 / 任务队列 / 配置缓存&amp;#34;]
    API --&amp;gt; WX[&amp;#34;微信开放平台&amp;lt;br/&amp;gt;登录 / 支付 V2 / 订阅消息&amp;#34;]
    API --&amp;gt; QN[&amp;#34;七牛云 OSS&amp;lt;br/&amp;gt;图片上传&amp;#34;]
    API --&amp;gt; QY[&amp;#34;企业微信机器人&amp;lt;br/&amp;gt;部署结果通知&amp;#34;]
    MP --&amp;gt;|&amp;#34;wss://... websocket&amp;#34;| CHAT[&amp;#34;咨询聊天服务&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;代码规模：后端约 850 个 Java 文件、12.6 万行；小程序前端业务代码约 1.2 万行；数据库初始化 SQL 全量在仓库里，共 62 张表。后端按业务分包：&lt;code&gt;front&lt;/code&gt;（小程序接口）、&lt;code&gt;order/payment&lt;/code&gt;、&lt;code&gt;marketing&lt;/code&gt;、&lt;code&gt;seckill&lt;/code&gt;、&lt;code&gt;task&lt;/code&gt;（定时任务）、&lt;code&gt;wechat&lt;/code&gt;，以及自研的 &lt;code&gt;appoint&lt;/code&gt;（预约）、&lt;code&gt;lunar&lt;/code&gt;（黄历）等模块。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二功能介绍&#34;&gt;二、功能介绍&lt;/h2&gt;
&lt;h3 id=&#34;21-商品购买全流程&#34;&gt;2.1 商品购买全流程&lt;/h3&gt;
&lt;p&gt;标准的商城闭环：商品详情（多规格 SKU）→ 立即购买/加购物车 → 确认订单（优惠券、积分抵扣、运费计算）→ 创建订单 → 微信支付 → 轮询支付结果 → 收货评价 → 退款/售后。&lt;strong&gt;支付是真实对接的微信支付 V2&lt;/strong&gt;——后端手工实现 XML 报文 + MD5 签名调统一下单（&lt;code&gt;WeChatPayServiceImpl&lt;/code&gt;，746 行），前端弹起 &lt;code&gt;wx.requestPayment&lt;/code&gt;，不是 mock。&lt;/p&gt;
&lt;p&gt;此外还支持：余额支付、运费模板（满额包邮/指定城市包邮/按件数重量体积三种首续费）、物流查询、自提核销码、再次下单。营销玩法（秒杀、砍价、拼团、优惠券）来自 CRMEB 底座，一应俱全。&lt;/p&gt;
&lt;h3 id=&#34;22-老师咨询时段预约自研核心&#34;&gt;2.2 老师咨询时段预约（自研核心）&lt;/h3&gt;
&lt;p&gt;这是这个项目区别于普通商城的地方——卖的不只是实物商品，还有&amp;quot;老师的时间&amp;quot;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;商品分类为&amp;quot;老师类&amp;quot;时，商品详情页出现一个&lt;strong&gt;时段选择器&lt;/strong&gt;（每天 6 个时段：9/11/13/15/17/19 点）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;teacher_plan&lt;/code&gt; 表按「日期 × 时段」预生成号源，被约走的时段前端置灰；&lt;/li&gt;
&lt;li&gt;下单时携带 &lt;code&gt;teacherPlanId&lt;/code&gt;，&lt;strong&gt;创建订单的同一个事务里&lt;/strong&gt;把该时段标记为已约——号源占用和订单创建要么一起成功要么一起回滚，不会出现&amp;quot;钱付了时段没了&amp;quot;；&lt;/li&gt;
&lt;li&gt;号源由定时任务 &lt;code&gt;TeacherPlanTask&lt;/code&gt; 每 8 小时自动向前补齐未来 8 天，并清理过期的空闲号源。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;23-其他自研功能&#34;&gt;2.3 其他自研功能&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;择吉日查询&lt;/strong&gt;：4.7 万行黄历数据入库（&lt;code&gt;lunar&lt;/code&gt; 系列表），按&amp;quot;宜&amp;quot;事反查吉日，比如查&amp;quot;最近适合搬家的日子&amp;quot;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在线咨询&lt;/strong&gt;：基于 WebSocket 长连接的实时聊天，支持文字/语音/图片消息（详见第三章「长连接聊天」）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;老师名片&lt;/strong&gt;、文章资讯、签到积分、充值、小程序订阅消息提醒；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管理后台&lt;/strong&gt;：商品/订单/营销/分销/财务/短信/按钮级权限/数据统计大屏（ECharts）。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三后端能力全景&#34;&gt;三、后端能力全景&lt;/h2&gt;
&lt;p&gt;后端虽然是 SpringBoot 单体，但能力面相当完整：前台 23 个 Controller、62 个 MyBatis Mapper，按业务分包组织：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    subgraph M1[&amp;#34;业务模块&amp;#34;]
        A1[&amp;#34;front 前台接口&amp;lt;br/&amp;gt;23 个 Controller&amp;#34;]
        A2[&amp;#34;order / payment&amp;lt;br/&amp;gt;订单与支付&amp;#34;]
        A3[&amp;#34;marketing / seckill / bargain / combination&amp;lt;br/&amp;gt;优惠券 秒杀 砍价 拼团&amp;#34;]
        A4[&amp;#34;appoint / lunar / businessCard&amp;lt;br/&amp;gt;预约 择吉日 名片（自研）&amp;#34;]
        A5[&amp;#34;finance / statistics / export&amp;lt;br/&amp;gt;充值提现 数据统计 Excel 导出&amp;#34;]
        A6[&amp;#34;wechat / wechatMessage / sms&amp;lt;br/&amp;gt;微信生态与短信&amp;#34;]
        A7[&amp;#34;task&amp;lt;br/&amp;gt;定时任务体系&amp;#34;]
    end
    subgraph M2[&amp;#34;公共层&amp;#34;]
        B1[&amp;#34;拦截器链&amp;lt;br/&amp;gt;前台 Token / 后台 Token+权限 / Swagger&amp;#34;]
        B2[&amp;#34;工具层 RedisUtil / WxPayUtil / CrmebUtil&amp;#34;]
        B3[&amp;#34;ThreadLocal 上下文传递当前用户&amp;#34;]
    end
    A1 --&amp;gt; B1
    A2 --&amp;gt; B2
    B1 --&amp;gt; B3&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;：&lt;code&gt;wechatMessage&lt;/code&gt; 模块支持文字/语音/图片/图文四种回复类型，关注、取关、扫码等事件都有对应处理；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;模板消息与订阅消息&lt;/strong&gt;：小程序订阅消息、公众号模板消息的增删查与群发，且发送走异步任务（&lt;code&gt;AsyncWeChatProgramTempMessage&lt;/code&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;：POI 实现的 Excel 导出、交易/商品/用户多维统计接口（配合管理端 ECharts 大屏）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;短信与物流&lt;/strong&gt;：短信验证码/通知、快递物流轨迹查询；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口文档&lt;/strong&gt;：Swagger 在线文档 + 仓库内置 admin/app/公共三份离线接口文档。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;31-长连接聊天websocket架构&#34;&gt;3.1 长连接聊天（WebSocket）架构&lt;/h3&gt;
&lt;p&gt;在线咨询是这个系统的&amp;quot;第二前台&amp;quot;，采用的是 &lt;strong&gt;WebSocket 长连接 + HTTP 接口&lt;/strong&gt;的组合方案。部署上两个服务跑在同一台服务器，由 nginx 按路径前缀分流：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    MP[&amp;#34;小程序聊天页&amp;lt;br/&amp;gt;pages/chat&amp;#34;] --&amp;gt;|&amp;#34;wss://.../websocket/userId/sessionId&amp;#34;| NGX[&amp;#34;nginx 按路径分流&amp;#34;]
    MP --&amp;gt;|&amp;#34;HTTP /chat/session/**  /chat/msg/**&amp;#34;| NGX
    MP --&amp;gt;|&amp;#34;HTTP /api/front/**&amp;#34;| NGX
    NGX --&amp;gt; CHAT[&amp;#34;聊天服务&amp;lt;br/&amp;gt;WebSocket 长连接 + 会话/消息接口&amp;#34;]
    NGX --&amp;gt; MALL[&amp;#34;商城后端 crm_server&amp;lt;br/&amp;gt;SpringBoot 18080&amp;#34;]&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;：WebSocket 握手 URL 直接携带 &lt;code&gt;userId/sessionId&lt;/code&gt;，连接建立的同时就完成了身份识别与会话路由，不需要额外的登录帧；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时走长连接，持久化走 HTTP&lt;/strong&gt;：收发消息走 WebSocket（消息体为 JSON）；会话绑定（&lt;code&gt;session/bindSession&lt;/code&gt;）、会话列表、删除会话、历史消息拉取（&lt;code&gt;msg/msgList&lt;/code&gt;）都走 &lt;code&gt;/chat/&lt;/code&gt; 前缀的 HTTP 接口——长连接管&amp;quot;实时推送&amp;quot;，HTTP 管&amp;quot;可查询、可回溯&amp;quot;，职责分得很干净；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;统一的消息类型协议&lt;/strong&gt;：&lt;code&gt;contentType&lt;/code&gt; 1=文字、2=语音（带 &lt;code&gt;contentDuration&lt;/code&gt; 时长）、3=图片（先经 &lt;code&gt;user/upload/image&lt;/code&gt; 上传拿到 URL，再把 URL 作为消息内容发出）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语音消息的移动端交互&lt;/strong&gt;：按住说话（&lt;code&gt;touchstart&lt;/code&gt;/&lt;code&gt;touchend&lt;/code&gt; 控制录音起止）、上滑取消（&lt;code&gt;touchmove&lt;/code&gt;/&lt;code&gt;touchcancel&lt;/code&gt;），体验和微信原生聊天对齐；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;发送体验乐观化&lt;/strong&gt;：消息发出后先本地 push 进消息列表立即上屏，&lt;code&gt;send&lt;/code&gt; 的 fail 回调再 toast 兜底提示。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;需要如实说明：聊天服务端的代码没有随这两个仓库开源（它部署在同一台服务器上，由 nginx 分流），仓库里能看到的生产实现是小程序端；&lt;code&gt;crm_server&lt;/code&gt; 里的 &lt;code&gt;com/im/SocketServer.java&lt;/code&gt; 只是当时练手的原生 TCP Socket demo（12345 端口），并非线上实现。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四技术亮点&#34;&gt;四、技术亮点&lt;/h2&gt;
&lt;h3 id=&#34;41-下单幂等两段式--唯一键兜底&#34;&gt;4.1 下单幂等：两段式 + 唯一键兜底&lt;/h3&gt;
&lt;p&gt;小程序网络环境下用户双击、重试、断网重发是常态，下单接口必须幂等。这里用了一个两段式设计：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TD
    A[&amp;#34;order/confirm 确认订单页&amp;#34;] --&amp;gt; B[&amp;#34;订单数据序列化存 Redis&amp;lt;br/&amp;gt;key = md5 当前时间&amp;lt;br/&amp;gt;有效期 60 分钟&amp;#34;]
    B --&amp;gt; C[&amp;#34;order/create/orderKey 创建订单&amp;#34;]
    C --&amp;gt; D{&amp;#34;第一步:查库判重&amp;lt;br/&amp;gt;StoreOrder.unique = orderKey&amp;#34;}
    D --&amp;gt;|&amp;#34;已存在&amp;#34;| E[&amp;#34;直接返回已有订单&amp;lt;br/&amp;gt;不重复创建&amp;#34;]
    D --&amp;gt;|&amp;#34;不存在&amp;#34;| F{&amp;#34;第二步:校验 Redis 缓存&amp;lt;br/&amp;gt;是否过期/被用过&amp;#34;}
    F --&amp;gt;|&amp;#34;过期&amp;#34;| G[&amp;#34;拒绝 提示重新确认&amp;#34;]
    F --&amp;gt;|&amp;#34;有效&amp;#34;| H[&amp;#34;事务内创建订单&amp;lt;br/&amp;gt;成功后删除缓存&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;unique&lt;/code&gt; 字段是最后的兜底——即使 Redis 挂了、缓存丢了，数据库唯一约束也保证同一个 orderKey 不会建出两张单。&lt;/p&gt;
&lt;h3 id=&#34;42-原子扣库存防超卖&#34;&gt;4.2 原子扣库存防超卖&lt;/h3&gt;
&lt;p&gt;库存扣减没有用&amp;quot;先查后改&amp;quot;，而是一条 SQL 完成：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// setSql + 条件改写，数据库层面原子完成&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;stock &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; stock &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; n   且要求   stock &lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt; n &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;=&lt;/span&gt; 0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;数据库单行更新的原子性天然挡住并发超卖，不需要分布式锁。秒杀/砍价/拼团场景更复杂：同一事务内要扣&lt;strong&gt;活动表 + 活动 SKU 表 + 商品表 + 商品 SKU 表&lt;/strong&gt;四份库存，任何一步失败整体回滚。&lt;/p&gt;
&lt;h3 id=&#34;43-编程式事务包住整个建单过程&#34;&gt;4.3 编程式事务包住整个建单过程&lt;/h3&gt;
&lt;p&gt;创建订单是个多写操作：扣库存、占预约时段、写订单主表、写订单详情、核销优惠券、写订单日志。代码用 &lt;code&gt;TransactionTemplate&lt;/code&gt; 编程式事务（而不是 &lt;code&gt;@Transactional&lt;/code&gt; 注解）把这一串操作包起来——好处是事务边界精确可控，不会把整个方法（包括不需要事务的远程调用）都圈进去。订单核心逻辑集中在 &lt;code&gt;OrderUtils.java&lt;/code&gt;（1500+ 行）和 &lt;code&gt;OrderServiceImpl.java&lt;/code&gt;（近 1000 行）。&lt;/p&gt;
&lt;h3 id=&#34;44-支付回调防重--异步化&#34;&gt;4.4 支付回调：防重 + 异步化&lt;/h3&gt;
&lt;p&gt;微信支付的回调处理（&lt;code&gt;CallbackServiceImpl.weChat&lt;/code&gt;，448 行）有几个关键点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;解析 XML、校验 &lt;code&gt;return_code&lt;/code&gt;/&lt;code&gt;result_code&lt;/code&gt;，按 &lt;code&gt;attach&lt;/code&gt; 里携带的 userId + type 区分是订单支付还是充值；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;已支付直接返回 SUCCESS&lt;/strong&gt;——微信会多次重推回调，幂等是回调接口的生死线；&lt;/li&gt;
&lt;li&gt;事务内完成 &lt;code&gt;updatePaid&lt;/code&gt;（置订单已支付）+ 扣积分 + 拼团状态处理；&lt;/li&gt;
&lt;li&gt;后续的&amp;quot;慢动作&amp;quot;——发短信、推订阅消息、分销结算——&lt;strong&gt;不放在回调里同步做&lt;/strong&gt;，而是 &lt;code&gt;lPush&lt;/code&gt; 进 Redis 队列异步执行，回调接口尽快给微信返回，避免超时重推。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;45-没有-mqredis-list--定时任务的务实替代&#34;&gt;4.5 没有 MQ：Redis List + 定时任务的务实替代&lt;/h3&gt;
&lt;p&gt;项目没有引入 RabbitMQ/RocketMQ，而是用 &lt;strong&gt;Redis List 当队列 + Spring &lt;code&gt;@Scheduled&lt;/code&gt; 定时任务消费&lt;/strong&gt;，撑起了一整套异步任务体系：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[&amp;#34;下单成功&amp;lt;br/&amp;gt;lPush ORDER_AUTO_CANCEL_KEY&amp;#34;] --&amp;gt; Q[&amp;#34;Redis List 队列&amp;#34;]
    P[&amp;#34;支付成功回调&amp;lt;br/&amp;gt;lPush 后续动作&amp;#34;] --&amp;gt; Q
    Q --&amp;gt; T1[&amp;#34;OrderAutoCancelTask&amp;lt;br/&amp;gt;每分钟消费&amp;lt;br/&amp;gt;超时未支付自动取消 + 回滚库存&amp;#34;]
    Q --&amp;gt; T2[&amp;#34;自动收货 / 自动好评&amp;#34;]
    Q --&amp;gt; T3[&amp;#34;优惠券过期 / 砍价截止&amp;lt;br/&amp;gt;佣金解冻&amp;#34;]
    Q --&amp;gt; T4[&amp;#34;TeacherPlanTask&amp;lt;br/&amp;gt;每8小时补齐号源&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;task/order/&lt;/code&gt; 下共有 8 个定时任务。对这个体量的系统，省掉一个 MQ 的部署运维成本，换来的是完全够用的可靠性——这是小项目技术选型上我很认可的一个取舍。&lt;/p&gt;
&lt;h3 id=&#34;46-鉴权链路uuid-token--redis--threadlocal&#34;&gt;4.6 鉴权链路：UUID Token + Redis + ThreadLocal&lt;/h3&gt;
&lt;p&gt;没有用 JWT，而是更传统的方案：登录后签发 UUID 存 Redis（按模块区分 key 前缀），拦截器 &lt;code&gt;FrontTokenInterceptor&lt;/code&gt; → &lt;code&gt;CheckFrontToken&lt;/code&gt; → &lt;code&gt;TokenManagerImpl&lt;/code&gt; 逐级校验，通过后把当前用户放进 &lt;code&gt;ThreadLocal&lt;/code&gt; 供全链路取用。管理端是 Token + 权限双拦截器。好处是&lt;strong&gt;服务端可随时踢人下线&lt;/strong&gt;（删 Redis key 即可），这是纯 JWT 做不到的。&lt;/p&gt;
&lt;h3 id=&#34;47-其他值得提的实现&#34;&gt;4.7 其他值得提的实现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;订单快照&lt;/strong&gt;：下单时把购物车内容序列化成 JSON 存进 &lt;code&gt;StoreOrderInfo.info&lt;/code&gt;，之后商品改价改名都不影响历史订单的展示；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运费计算引擎&lt;/strong&gt;：&lt;code&gt;getOrderPriceGroup&lt;/code&gt; 支持满额包邮、指定城市包邮（件数 + 金额双条件）、按件数/重量/体积三种首续计费模式；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;XSS 转义&lt;/strong&gt;：用户输入统一 &lt;code&gt;escapeHtml4&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;部署脚本&lt;/strong&gt;：&lt;code&gt;start.sh&lt;/code&gt; nohup 启动 + 轮询日志判断启动成功 + 企业微信机器人推送部署结果——单机部署也搞出了仪式感。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五复盘与遗憾&#34;&gt;五、复盘与遗憾&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;底座与自研的边界&lt;/strong&gt;：秒杀/拼团/砍价/分销这些&amp;quot;重&amp;quot;能力直接复用 CRMEB 底座，把精力集中在业务差异化（预约、择吉日、聊天）上——对个人开发者来说这是正确的杠杆；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单体的局限&lt;/strong&gt;：所有东西在一个 SpringBoot 应用里，定时任务、支付、前台接口互相影响，量大了必然要拆；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原子扣库存的返回值没检查&lt;/strong&gt;：&lt;code&gt;setSql&lt;/code&gt; 扣减失败（库存不足）时影响的行数为 0，但代码没有校验这个返回值，极端情况下会漏判超卖——这是现在回头看想补的一个洞；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;聊天服务端未开源&lt;/strong&gt;：如第三章所述，长连接聊天的服务端实现没有随仓库开源，小程序端和接口契约是完整的，算是开源的一点遗憾。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六写在最后&#34;&gt;六、写在最后&lt;/h2&gt;
&lt;p&gt;这个项目让我完整走了一遍&amp;quot;真实收钱&amp;quot;的系统该有的样子：幂等、事务、防超卖、回调防重、异步补偿——每一个点背后都是电商系统趟过的坑。代码在 &lt;a href=&#34;https://gitee.com/soulnew/starmall&#34;&gt;https://gitee.com/soulnew/starmall&lt;/a&gt; 和 &lt;a href=&#34;https://gitee.com/soulnew/crm_server&#34;&gt;https://gitee.com/soulnew/crm_server&lt;/a&gt;，建库 SQL、接口文档都在仓库里，希望能给做小程序商城的朋友一个可参照的完整样本。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>消息队列总结：RabbitMQ、Kafka、RocketMQ 与 Redis MQ 选型对比与实践避坑</title>
      <link>https://ftmao.com/blog/posts/mq-comparison-rabbitmq-kafka-rocketmq/</link>
      <pubDate>Tue, 30 Jun 2020 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/mq-comparison-rabbitmq-kafka-rocketmq/</guid>
      <description>&lt;h1 id=&#34;消息队列总结rabbitmqkafkarocketmq-与-redis-mq-选型对比与实践避坑&#34;&gt;消息队列总结：RabbitMQ、Kafka、RocketMQ 与 Redis MQ 选型对比与实践避坑&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;消息队列是分布式系统里最常见的「异步管道」。这篇笔记把 RabbitMQ、Kafka、RocketMQ 和 Redis MQ 放在一起比较，重点回答：什么时候该用哪个？推和拉到底怎么选？长连接有什么影响？以及实际使用中的避坑和异常处理经验。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一为什么需要消息队列&#34;&gt;一、为什么需要消息队列？&lt;/h2&gt;
&lt;p&gt;在没有 MQ 的系统中，A 服务调用 B 服务通常是同步的：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;A ──同步调用──▶ B ──同步调用──▶ C
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     等待返回      等待返回&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;链路越长，响应越慢，容错越差。MQ 的核心价值是把「同步依赖」变成「异步管道」：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;A ──发送消息──▶ MQ ──接收消息──▶ B
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   立即返回         异步处理&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;11-mq-解决的三大问题&#34;&gt;1.1 MQ 解决的三大问题&lt;/h3&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;：把非核心链路（发短信、记日志、发券）从主流程剥离，降低接口 RT。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;削峰填谷&lt;/strong&gt;：突发流量先写入队列，下游按自身能力消费，避免被压垮。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;12-什么时候必须引入-mq&#34;&gt;1.2 什么时候必须引入 MQ？&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;li&gt;大促/活动场景下，需要缓冲突发流量。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二四种-mq-横向对比&#34;&gt;二、四种 MQ 横向对比&lt;/h2&gt;
&lt;p&gt;RabbitMQ、Kafka、RocketMQ 是主流企业级 MQ，Redis 也能当轻量 MQ 用，但定位不同。&lt;/p&gt;
&lt;h3 id=&#34;21-核心定位&#34;&gt;2.1 核心定位&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;RocketMQ&lt;/th&gt;
					&lt;th&gt;RabbitMQ&lt;/th&gt;
					&lt;th&gt;Kafka&lt;/th&gt;
					&lt;th&gt;Redis MQ&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;/td&gt;
					&lt;td&gt;Erlang 实现，面向通用企业消息&lt;/td&gt;
					&lt;td&gt;LinkedIn 开源，面向日志采集与流处理&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;Topic + Queue，支持 Tag 过滤&lt;/td&gt;
					&lt;td&gt;Exchange + Queue，路由灵活&lt;/td&gt;
					&lt;td&gt;Topic + Partition，分区有序&lt;/td&gt;
					&lt;td&gt;List / Stream / PubSub&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;默认消费方式&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Push（长轮询封装）+ Pull&lt;/td&gt;
					&lt;td&gt;Push（AMQP 推送）&lt;/td&gt;
					&lt;td&gt;Pull（消费者主动拉取）&lt;/td&gt;
					&lt;td&gt;Push（PubSub）/ Pull（BLPOP）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;顺序消息&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;分片内有序，Sharding Key 控制&lt;/td&gt;
					&lt;td&gt;单 Queue 有序&lt;/td&gt;
					&lt;td&gt;Partition 内有序&lt;/td&gt;
					&lt;td&gt;单 Stream/列表有序&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;延迟消息&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;原生支持 18 个级别&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;strong&gt;事务消息&lt;/strong&gt;&lt;/td&gt;
					&lt;td&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;strong&gt;消息持久化&lt;/strong&gt;&lt;/td&gt;
					&lt;td&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;strong&gt;消息轨迹&lt;/strong&gt;&lt;/td&gt;
					&lt;td&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;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;22-性能对比&#34;&gt;2.2 性能对比&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;RocketMQ&lt;/th&gt;
					&lt;th&gt;RabbitMQ&lt;/th&gt;
					&lt;th&gt;Kafka&lt;/th&gt;
					&lt;th&gt;Redis MQ&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;10 万级 TPS&lt;/td&gt;
					&lt;td&gt;万级 TPS&lt;/td&gt;
					&lt;td&gt;百万级 TPS（批量场景）&lt;/td&gt;
					&lt;td&gt;十万级 TPS（内存操作）&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;td&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;td&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;td&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;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kafka 吞吐数字通常来自批量、压缩、顺序写的日志场景；单条小消息低延迟不如 RocketMQ。&lt;/li&gt;
&lt;li&gt;RabbitMQ 延迟低，但高吞吐和海量堆积不是强项。&lt;/li&gt;
&lt;li&gt;RocketMQ 在「中等吞吐 + 低延迟 + 强堆积」之间取得了较好平衡。&lt;/li&gt;
&lt;li&gt;Redis MQ 性能高，但消息堆积、持久化、事务、延迟消息都不完善，适合轻量、临时场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;23-成本与生态&#34;&gt;2.3 成本与生态&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;RocketMQ&lt;/th&gt;
					&lt;th&gt;RabbitMQ&lt;/th&gt;
					&lt;th&gt;Kafka&lt;/th&gt;
					&lt;th&gt;Redis MQ&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;中等（NameServer + Broker）&lt;/td&gt;
					&lt;td&gt;低（单节点即可运行）&lt;/td&gt;
					&lt;td&gt;高（ZooKeeper/KRaft + Broker）&lt;/td&gt;
					&lt;td&gt;低（Redis 单实例/集群）&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;低，但 Erlang 问题排查门槛高&lt;/td&gt;
					&lt;td&gt;高，调优项多&lt;/td&gt;
					&lt;td&gt;低，Redis 团队普遍熟悉&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;云服务成熟度&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;阿里云 RocketMQ 成熟&lt;/td&gt;
					&lt;td&gt;各云厂商都有托管版&lt;/td&gt;
					&lt;td&gt;各云厂商 Kafka 托管版成熟&lt;/td&gt;
					&lt;td&gt;云 Redis 托管版成熟&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;td&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;一般，可与 Flink/Spark 对接&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;strong&gt;语言客户端&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Java 最完善，其他语言有社区版&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;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;24-适用场景推荐&#34;&gt;2.4 适用场景推荐&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;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;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;延迟低、事务消息、顺序消息、监控完善&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;异步任务、分布式 Worker、视频渲染&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;PushConsumer 自动负载均衡，失败重试完善&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;传统企业应用、RPC 解耦、路由复杂&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;简单易用，Exchange 路由灵活，快速上线&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;日志采集、埋点、实时数仓、流处理&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&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;strong&gt;RabbitMQ / Kafka&lt;/strong&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;strong&gt;RocketMQ&lt;/strong&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;strong&gt;Redis MQ&lt;/strong&gt;&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;strong&gt;Redis MQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;作为过渡方案或辅助队列可用&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;25-一句话选型&#34;&gt;2.5 一句话选型&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;可靠交易、事务消息、顺序消息、在线业务  →  RocketMQ
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;快速搭建、路由灵活、团队规模小          →  RabbitMQ
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;日志/流处理/大数据管道、吞吐优先          →  Kafka
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;轻量通知、临时队列、已有 Redis 生态       →  Redis MQ&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;很多大厂内部是「Kafka 做日志流 + RocketMQ 做业务消息 + RabbitMQ 做轻量任务 + Redis 做缓存/通知」组合使用，不是单选题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三底层架构与核心差异&#34;&gt;三、底层架构与核心差异&lt;/h2&gt;
&lt;h3 id=&#34;31-四种-mq-架构对比&#34;&gt;3.1 四种 MQ 架构对比&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph RocketMQ
        P1[Producer] --&amp;gt; NS[NameServer]
        NS --&amp;gt; B1[Broker Master]
        B1 --&amp;gt; B2[Broker Slave]
        B1 --&amp;gt; C1[Consumer]
    end

    subgraph Kafka
        P2[Producer] --&amp;gt; KZ[ZooKeeper / KRaft]
        KZ --&amp;gt; KB1[Broker 1]
        KZ --&amp;gt; KB2[Broker 2]
        KB1 --&amp;gt; C2[Consumer]
    end

    subgraph RabbitMQ
        P3[Producer] --&amp;gt; EX[Exchange]
        EX --&amp;gt; Q1[Queue A]
        EX --&amp;gt; Q2[Queue B]
        Q1 --&amp;gt; C3[Consumer A]
        Q2 --&amp;gt; C4[Consumer B]
    end

    subgraph RedisMQ
        P4[Producer] --&amp;gt; R[(Redis)]
        R --&amp;gt; C5[Consumer BLPOP]
        R --&amp;gt; C6[Subscriber]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;32-存储模型&#34;&gt;3.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;th&gt;核心特点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;CommitLog + ConsumeQueue&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;所有 Topic 的消息顺序写入同一个 CommitLog，再用 ConsumeQueue 存每个 Queue 的偏移索引。顺序写磁盘，I/O 效率高。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Partition Segment + Index&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每个 Partition 是独立日志目录，顺序写入 Segment，配合稀疏索引。Partition 间物理隔离。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Queue 独立存储&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每个 Queue 维护独立索引和存储，Queue 越多元数据开销越大。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Redis MQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;内存 List / Stream&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;基于内存操作，速度快，但容量受内存限制，持久化可靠性不如专业 MQ。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TB
    subgraph RocketMQ_Storage[&amp;#34;RocketMQ 存储模型&amp;#34;]
        CL[CommitLog&amp;lt;br/&amp;gt;所有消息顺序写]
        CQ1[ConsumeQueue A&amp;lt;br/&amp;gt;偏移索引]
        CQ2[ConsumeQueue B&amp;lt;br/&amp;gt;偏移索引]
        CL --&amp;gt; CQ1
        CL --&amp;gt; CQ2
    end

    subgraph Kafka_Storage[&amp;#34;Kafka 存储模型&amp;#34;]
        KP1[TopicA-P0 Log&amp;lt;br/&amp;gt;独立 Partition]
        KP2[TopicA-P1 Log&amp;lt;br/&amp;gt;独立 Partition]
    end

    subgraph RabbitMQ_Storage[&amp;#34;RabbitMQ 存储模型&amp;#34;]
        MQ1[Queue A&amp;lt;br/&amp;gt;独立索引]
        MQ2[Queue B&amp;lt;br/&amp;gt;独立索引]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;33-路由与元数据管理&#34;&gt;3.3 路由与元数据管理&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;th&gt;设计哲学&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;NameServer&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;无状态集群，Broker 主动上报路由，Producer/Consumer 定时拉取。最终一致，轻量。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;ZooKeeper / KRaft&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;老版本依赖 ZooKeeper 存元数据、选 Controller；新版本用 KRaft 自洽协议。强一致要求更高。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;内置 Mnesia 数据库&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;集群节点共享元数据，Erlang 进程模型天然分布式，但集群扩展性相对有限。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Redis MQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;无&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;依赖 Redis 自身的 Cluster/哨兵做高可用，没有独立路由层。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;34-复制与高可用&#34;&gt;3.4 复制与高可用&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;th&gt;特点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Master-Slave 同步/异步复制&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;同步复制保证不丢消息，异步复制性能更高。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Partition 多副本 + ISR 机制&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;自动 Failover，成熟度高。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&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;strong&gt;Redis MQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;主从 + 哨兵 / Cluster&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Redis 自身的高可用机制，不是为消息队列专门设计。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四推拉模式与长连接&#34;&gt;四、推、拉模式与长连接&lt;/h2&gt;
&lt;h3 id=&#34;41-push-vs-pull-对比&#34;&gt;4.1 Push vs Pull 对比&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;Push（推）&lt;/th&gt;
					&lt;th&gt;Pull（拉）&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;在 Broker&lt;/td&gt;
					&lt;td&gt;在 Consumer&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;实时性&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;高，Broker 有消息就发&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;较弱，Consumer 处理慢会堆积&lt;/td&gt;
					&lt;td&gt;强，Consumer 自己控制速率&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;高，需自管 offset、拉取循环&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;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;42-四种-mq-的推拉实现&#34;&gt;4.2 四种 MQ 的推拉实现&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;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;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;长轮询 Pull 封装成 Push&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;是&lt;/td&gt;
					&lt;td&gt;Consumer 与 Broker 保持长连接，Broker hold 请求 5 秒左右，有新消息立即返回。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;消费者主动 Pull&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;否（可保持连接但非必需）&lt;/td&gt;
					&lt;td&gt;Consumer 自己决定何时拉、拉多少，灵活性最高。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Broker 真 Push（AMQP）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;是&lt;/td&gt;
					&lt;td&gt;Channel 长连接，Broker 把消息塞给 Consumer，处理完 ACK。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Redis MQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;PubSub 推 / BLPOP 拉&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;是&lt;/td&gt;
					&lt;td&gt;PubSub 是长连接推送；BLPOP 是阻塞拉取，也算长连接。&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;43-长连接的影响&#34;&gt;4.3 长连接的影响&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 长连接优势
        A1[低延迟]
        A2[实时推送]
        A3[减少建连开销]
    end

    subgraph 长连接代价
        B1[连接数消耗大]
        B2[需要心跳保活]
        B3[网络断开感知延迟]
        B4[Consumer 慢会拖垮 Broker]
    end&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;消息到达可以立刻推送给 Consumer，延迟低。&lt;/li&gt;
&lt;li&gt;避免频繁建立/断开 TCP 连接的开销。&lt;/li&gt;
&lt;li&gt;适合在线业务、实时通知类场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;长连接的代价&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Broker 需要维护大量连接，连接数成为重要资源。&lt;/li&gt;
&lt;li&gt;必须有心跳机制，否则无法及时发现 Consumer 宕机。&lt;/li&gt;
&lt;li&gt;Consumer 处理慢时，消息会堆积在 Broker 内存或磁盘，容易把 Broker 压垮。&lt;/li&gt;
&lt;li&gt;网络闪断后需要重连、重平衡，可能带来消费抖动。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;44-选择原则&#34;&gt;4.4 选择原则&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[选择消费模式] --&amp;gt; B{实时性要求高?}
    B --&amp;gt;|是| C{消费逻辑轻量?}
    B --&amp;gt;|否| D[Pull 模式]
    C --&amp;gt;|是| E[Push 模式]
    C --&amp;gt;|否| F[Pull 模式 或 限流 Push]
    D --&amp;gt; G[批量处理/ETL/数据同步]
    E --&amp;gt; H[在线业务/实时通知]
    F --&amp;gt; I[重任务/视频渲染/大数据]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;五使用注意点与避坑指南&#34;&gt;五、使用注意点与避坑指南&lt;/h2&gt;
&lt;h3 id=&#34;51-rocketmq-避坑&#34;&gt;5.1 RocketMQ 避坑&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;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;同一个 ConsumerGroup 订阅不同 Topic/Tag，重平衡混乱&lt;/td&gt;
					&lt;td&gt;一个 ConsumerGroup 只订阅一个 Topic + 一套 Tag&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Tag 用得太细&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;管理混乱，消费者过滤复杂&lt;/td&gt;
					&lt;td&gt;Topic 按业务域划分，Tag 做粗粒度过滤&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;消息体过大&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;超过 4MB 默认限制，发送失败&lt;/td&gt;
					&lt;td&gt;消息体只传 ID 和必要字段，完整数据从 DB/OSS 拉取&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;只在真正需要局部顺序的场景用 Sharding Key&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;NameServer 路由延迟&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Broker 扩缩容后发送到已下线节点&lt;/td&gt;
					&lt;td&gt;理解最终一致性，扩缩容留足等待时间&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;自动创建 Topic&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;生产环境订阅失败或路由错误&lt;/td&gt;
					&lt;td&gt;提前创建 Topic 和 ConsumerGroup&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;52-kafka-避坑&#34;&gt;5.2 Kafka 避坑&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;th&gt;解决&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Partition 数设置不当&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;太少并行度不够，太多元数据压力大&lt;/td&gt;
					&lt;td&gt;根据吞吐和 Consumer 数合理规划&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;消费者 Rebalance 频繁&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;消费停滞，组员加入/退出频繁&lt;/td&gt;
					&lt;td&gt;避免频繁扩缩容，调整 session.timeout.ms&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Offset 提交策略不当&lt;/strong&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;strong&gt;Producer  acks=0/1&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;丢消息&lt;/td&gt;
					&lt;td&gt;关键业务用 acks=all&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;单 Partition 热点&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;某个 Partition 消费滞后&lt;/td&gt;
					&lt;td&gt;合理设计 Key，避免 Key 倾斜&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;53-rabbitmq-避坑&#34;&gt;5.3 RabbitMQ 避坑&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;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;/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;Broker 内存/磁盘告警，性能下降&lt;/td&gt;
					&lt;td&gt;控制生产速度，及时扩容 Consumer&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;ACK 模式用错&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;消息丢失或重复&lt;/td&gt;
					&lt;td&gt;手动 ACK，处理完再确认&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Exchange 路由复杂&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;Connection/Channel 滥用&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;连接数耗尽&lt;/td&gt;
					&lt;td&gt;Channel 复用，避免每个消息新建连接&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;54-redis-mq-避坑&#34;&gt;5.4 Redis MQ 避坑&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;th&gt;解决&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;PubSub 消息丢失&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Consumer 断线期间的消息收不到&lt;/td&gt;
					&lt;td&gt;重要消息用 Stream 或专业 MQ&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;List 做队列无 ACK&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Consumer 处理失败消息丢失&lt;/td&gt;
					&lt;td&gt;用 Stream 的 ACK 机制&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;内存不足&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Redis OOM&lt;/td&gt;
					&lt;td&gt;设置合理过期时间，控制队列长度&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;把 Redis 当核心 MQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;数据丢失、无事务、无延迟消息&lt;/td&gt;
					&lt;td&gt;核心交易链路用 RocketMQ/Kafka&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;55-通用最佳实践&#34;&gt;5.5 通用最佳实践&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;消息体尽量小&lt;/strong&gt;：传 ID 不传大对象。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;幂等设计&lt;/strong&gt;：所有 Consumer 都要做幂等，MQ 只保证至少一次投递。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;监控到位&lt;/strong&gt;：堆积量、消费 TPS、延迟、死信队列、Broker 资源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;版本一致&lt;/strong&gt;：服务端和客户端版本不要差距太大。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;提前创建资源&lt;/strong&gt;：生产环境不要依赖自动创建 Topic/Queue。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;区分可重试与不可重试异常&lt;/strong&gt;：网络抖动可以重试，参数错误直接进死信。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六异常场景处理分-mq-说明&#34;&gt;六、异常场景处理（分 MQ 说明）&lt;/h2&gt;
&lt;p&gt;下面列出的异常场景在 RocketMQ、RabbitMQ、Kafka、Redis MQ 中都会遇到，但每种 MQ 提供的机制和最佳实践不同。这一节把四种 MQ 放在一起对比说明。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    P[Producer 发送层] --&amp;gt;|发送失败/超时| B[Broker 存储层]
    B --&amp;gt;|刷盘/复制失败| C[Consumer 消费层]
    C --&amp;gt;|重复/失败/卡住| Biz[业务异常兜底]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;61-消息丢失各-mq-如何防范&#34;&gt;6.1 消息丢失：各 MQ 如何防范&lt;/h3&gt;
&lt;p&gt;消息丢失通常发生在三个环节：&lt;strong&gt;Producer 发送失败&lt;/strong&gt;、&lt;strong&gt;Broker 存储失败&lt;/strong&gt;、&lt;strong&gt;Consumer 消费失败未 ACK&lt;/strong&gt;。四种 MQ 的防范方式如下：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;环节&lt;/th&gt;
					&lt;th&gt;RocketMQ&lt;/th&gt;
					&lt;th&gt;RabbitMQ&lt;/th&gt;
					&lt;th&gt;Kafka&lt;/th&gt;
					&lt;th&gt;Redis MQ&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Producer 发送&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;同步发送 &lt;code&gt;SYNC&lt;/code&gt; + 失败重试；避免 &lt;code&gt;ONEWAY&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;使用 &lt;code&gt;confirm&lt;/code&gt; 模式等待 Broker ACK；开启 Publisher Confirms&lt;/td&gt;
					&lt;td&gt;设置 &lt;code&gt;acks=all&lt;/code&gt; + 重试；避免 &lt;code&gt;acks=0/1&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;无发送确认机制，调用成功不代表持久化成功；重要消息不要用 Redis PubSub&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Broker 存储&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;同步刷盘 &lt;code&gt;SYNC_FLUSH&lt;/code&gt; + 同步复制 &lt;code&gt;SYNC_MASTER&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;队列持久化 + 镜像队列复制；消息标记 &lt;code&gt;persistent&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;replication.factor &amp;gt;= 3&lt;/code&gt; + &lt;code&gt;min.insync.replicas&lt;/code&gt; 控制&lt;/td&gt;
					&lt;td&gt;AOF/ RDB 持久化，但默认异步；主从切换可能丢数据&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Consumer 消费&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;先处理业务再返回 &lt;code&gt;CONSUME_SUCCESS&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;手动 ACK，处理完再 &lt;code&gt;basic.ack&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;处理完再 commit offset&lt;/td&gt;
					&lt;td&gt;Stream 用 &lt;code&gt;XACK&lt;/code&gt; 确认；List/ PubSub 无 ACK&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;td&gt;业务对账 + 补偿表&lt;/td&gt;
					&lt;td&gt;不建议做核心兜底，Redis 本身不是可靠 MQ&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;ul&gt;
&lt;li&gt;&lt;strong&gt;RocketMQ&lt;/strong&gt;：&lt;code&gt;SYNC_MASTER&lt;/code&gt; + &lt;code&gt;SYNC_FLUSH&lt;/code&gt; 配置最安全，但性能最低；在线交易通常用 &lt;code&gt;SYNC_MASTER&lt;/code&gt; + &lt;code&gt;ASYNC_FLUSH&lt;/code&gt; 折中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;：默认自动 ACK 最容易丢消息，生产环境一定要改手动 ACK。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kafka&lt;/strong&gt;：&lt;code&gt;acks=all&lt;/code&gt; 配合 &lt;code&gt;min.insync.replicas&lt;/code&gt; 才能保证写入 ISR 多数副本；只配置 &lt;code&gt;acks=all&lt;/code&gt; 不够。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redis MQ&lt;/strong&gt;：Redis 本身不是为消息可靠性设计的，&lt;strong&gt;不要把核心交易消息的防丢失交给 Redis&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;62-消息重复各-mq-的投递语义与幂等&#34;&gt;6.2 消息重复：各 MQ 的投递语义与幂等&lt;/h3&gt;
&lt;p&gt;四种 MQ 大多只保证「至少一次投递」（at-least-once），重复消费要靠业务层幂等。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;MQ&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;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;At-least-once&lt;/td&gt;
					&lt;td&gt;网络超时重发、Consumer 重启、Broker 重试&lt;/td&gt;
					&lt;td&gt;消息 &lt;code&gt;keys&lt;/code&gt; 做唯一业务 Key + 数据库唯一索引&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;At-least-once&lt;/td&gt;
					&lt;td&gt;连接断开、ACK 未到达 Broker、Consumer 重启&lt;/td&gt;
					&lt;td&gt;消息 &lt;code&gt;message_id&lt;/code&gt; 或业务 Key + 幂等表&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;At-least-once&lt;/td&gt;
					&lt;td&gt;Consumer Rebalance、Offset 提交失败、Producer 重试&lt;/td&gt;
					&lt;td&gt;业务 Key 去重；Kafka 幂等 Producer 只能去重单会话内重复&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Redis MQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;不保证&lt;/td&gt;
					&lt;td&gt;PubSub 不持久化，断线即丢；Stream 相对可靠但无原生重试&lt;/td&gt;
					&lt;td&gt;业务侧用 Redis &lt;code&gt;SETNX&lt;/code&gt; 或数据库唯一索引&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 通用幂等伪代码，适用于四种 MQ&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;consume&lt;/span&gt;(msg):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    biz_key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; extract_biz_key(msg)  &lt;span style=&#34;color:#75715e&#34;&gt;# RocketMQ keys / RabbitMQ message_id / Kafka key&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        idempotent_record&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;insert(biz_key&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;biz_key, status&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;processing&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; UniqueViolation:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; SUCCESS  &lt;span style=&#34;color:#75715e&#34;&gt;# 已处理过，直接成功&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        do_business(msg)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        idempotent_record&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update(biz_key, status&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;done&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; SUCCESS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; RetryableException:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        idempotent_record&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update(biz_key, status&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;failed&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; RETRY
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; NonRetryableException:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        idempotent_record&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update(biz_key, status&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;failed&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; DEAD_LETTER&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;63-消费失败重试与死信机制&#34;&gt;6.3 消费失败：重试与死信机制&lt;/h3&gt;
&lt;p&gt;消费失败后，不同 MQ 的重试和死信（DLQ）机制差异很大：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;MQ&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;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;默认最多 16 次，间隔阶梯递增（10s、30s、1m&amp;hellip;）&lt;/td&gt;
					&lt;td&gt;自动进入 &lt;code&gt;%DLQ%consumer_group&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;可区分 &lt;code&gt;RECONSUME_LATER&lt;/code&gt; 重试和直接成功；重试次数可在 Broker 配置&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;无内置重试，需业务侧或插件实现；常用 TTL + 死信交换器（DLX）做延迟重试&lt;/td&gt;
					&lt;td&gt;通过 &lt;code&gt;x-dead-letter-exchange&lt;/code&gt; 配置 DLX/DLQ&lt;/td&gt;
					&lt;td&gt;手动 ACK 失败或 NACK + requeue=false 进入死信&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;无内置重试次数，失败时 Consumer 自己决定是否重试或跳过&lt;/td&gt;
					&lt;td&gt;无原生 DLQ，需业务侧或 Kafka Connect 实现&lt;/td&gt;
					&lt;td&gt;常见做法：失败消息写入专用 Error Topic，由补偿服务处理&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Redis MQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;无内置重试&lt;/td&gt;
					&lt;td&gt;无原生 DLQ&lt;/td&gt;
					&lt;td&gt;Stream 可配合 Consumer Group 的 Pending List 人工处理；List/ PubSub 无重试能力&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[Consumer 处理失败] --&amp;gt; B{MQ 类型}
    B --&amp;gt;|RocketMQ| C[返回 RECONSUME_LATER]
    B --&amp;gt;|RabbitMQ| D[NACK + requeue=false 进 DLX]
    B --&amp;gt;|Kafka| E[写入 Error Topic 或暂停消费]
    B --&amp;gt;|Redis Stream| F[XCLAIM 或人工处理 Pending]
    C --&amp;gt; G{重试超限?}
    G --&amp;gt;|是| H[进入 DLQ]
    G --&amp;gt;|否| C
    D --&amp;gt; H
    E --&amp;gt; I[补偿服务消费 Error Topic]
    F --&amp;gt; J[业务侧补偿]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;可重试异常&lt;/strong&gt;：网络超时、依赖服务短暂不可用、数据库连接池耗尽。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不可重试异常&lt;/strong&gt;：参数非法、依赖返回 4xx、业务规则冲突、数据不存在。&lt;/p&gt;
&lt;h3 id=&#34;64-业务异常处理&#34;&gt;6.4 业务异常处理&lt;/h3&gt;
&lt;p&gt;业务异常（如参数错误、状态冲突、规则校验失败）和系统异常（网络、DB、依赖故障）要分开处理。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[Consumer 捕获异常] --&amp;gt; B{异常类型}
    B --&amp;gt;|系统异常&amp;lt;br/&amp;gt;网络/DB/依赖| C[MQ 重试]
    B --&amp;gt;|业务异常&amp;lt;br/&amp;gt;参数/规则/状态冲突| D[记录失败明细]
    D --&amp;gt; E{能否修复?}
    E --&amp;gt;|能| F[人工补偿/修复后重投]
    E --&amp;gt;|不能| G[进死信队列或丢弃]
    C --&amp;gt; H{重试成功?}
    H --&amp;gt;|否| I[进入死信/Error Topic]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;MQ&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;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;捕获业务异常后，直接返回 &lt;code&gt;CONSUME_SUCCESS&lt;/code&gt; 并记录失败原因，避免无意义重试；或主动放入自定义补偿 Topic。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;业务异常时 &lt;code&gt;basic.nack(requeue=false)&lt;/code&gt; 直接进死信，不要在 RabbitMQ 里反复重试业务错误。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;业务异常写入 Error Topic，由补偿服务按业务规则处理；Consumer 继续消费后续消息，避免阻塞 Partition。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Redis MQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Redis 不适合复杂异常处理；业务异常建议直接记录到数据库或日志，人工介入。&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;ol&gt;
&lt;li&gt;&lt;strong&gt;记录失败明细&lt;/strong&gt;：消息内容、异常原因、时间、Consumer 实例、业务 Key。&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;li&gt;&lt;strong&gt;告警分级&lt;/strong&gt;：死信队列增长、消费停滞、重试频繁分级告警。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要无限重试业务异常&lt;/strong&gt;：参数错误重试 16 次没有意义。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;65-消息卡住消费停滞&#34;&gt;6.5 消息卡住/消费停滞&lt;/h3&gt;
&lt;p&gt;消费停滞是四种 MQ 都会遇到的异常，但原因和处理方式各有侧重：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;MQ&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;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Consumer 实例少、单条处理慢、Consumer 宕机、下游依赖慢&lt;/td&gt;
					&lt;td&gt;扩容 Consumer（不超过 Queue 数）、优化消费逻辑、限流、降级&lt;/td&gt;
					&lt;td&gt;ConsumeProgress 堆积量、消费 TPS&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;队列堆积导致内存/磁盘告警、Consumer 连接断开、Channel 阻塞&lt;/td&gt;
					&lt;td&gt;扩容 Consumer、控制生产速度、启用队列长度限制&lt;/td&gt;
					&lt;td&gt;Queue 深度、内存使用率、磁盘告警&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Partition 消费滞后、Consumer Rebalance 频繁、单 Partition 热点&lt;/td&gt;
					&lt;td&gt;增加 Partition、优化 Key 设计、调整 &lt;code&gt;session.timeout.ms&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Consumer Lag、Rebalance 频率&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Redis MQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;内存不足、Consumer 断线（PubSub 丢消息）、处理慢&lt;/td&gt;
					&lt;td&gt;控制队列长度、设置过期时间、用 Stream 替代 PubSub&lt;/td&gt;
					&lt;td&gt;Redis 内存、Pending List 长度&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[消息堆积] --&amp;gt; B{原因}
    B --&amp;gt;|Consumer 少| C[扩容 Consumer]
    B --&amp;gt;|单条处理慢| D[优化消费逻辑/异步化]
    B --&amp;gt;|Consumer 宕机| E[心跳超时触发重平衡]
    B --&amp;gt;|下游依赖慢| F[限流/降级/异步处理]
    B --&amp;gt;|Partition/Queue 热点| G[调整 Key 或增加分区]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;66-顺序消息异常&#34;&gt;6.6 顺序消息异常&lt;/h3&gt;
&lt;p&gt;顺序消息一旦某条处理卡住，后续消息都会被阻塞，四种 MQ 的处理方式不同：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;MQ&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;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Sharding Key 路由到同一 MessageQueue，单线程消费&lt;/td&gt;
					&lt;td&gt;一条失败会阻塞整个 Queue 的后续消息&lt;/td&gt;
					&lt;td&gt;设置消费超时；失败次数过多时考虑跳过或转移；业务上保证单条处理轻量&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;单 Queue 天然有序，单 Consumer 单线程消费&lt;/td&gt;
					&lt;td&gt;一个 Consumer 卡住会阻塞整个 Queue&lt;/td&gt;
					&lt;td&gt;控制单条处理时间；必要时拆分多个 Queue 做局部顺序&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;同一 Partition 内有序，单 Consumer 消费 Partition&lt;/td&gt;
					&lt;td&gt;一条消息处理慢会阻塞整个 Partition&lt;/td&gt;
					&lt;td&gt;合理设计 Key，避免单个 Partition 成为热点；业务异常时写入 Error Topic 避免阻塞&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Redis MQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Stream/ List 单节点内有序&lt;/td&gt;
					&lt;td&gt;单 Consumer 卡住会阻塞后续消费&lt;/td&gt;
					&lt;td&gt;不建议用 Redis 做强顺序消息；处理异常时直接记录并人工处理&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[Queue/Partition 内顺序消费] --&amp;gt; B[消息 1 处理失败或超慢]
    B --&amp;gt; C[消息 2 等待]
    C --&amp;gt; D[消息 3 等待]
    B --&amp;gt; E{如何处理?}
    E --&amp;gt;|跳过| F[破坏顺序性]
    E --&amp;gt;|重试| G[阻塞后续消息]
    E --&amp;gt;|转移| H[进死信/Error Topic]&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;/li&gt;
&lt;li&gt;单条消息处理要轻量，避免长耗时阻塞队列。&lt;/li&gt;
&lt;li&gt;业务上设计「局部顺序」而不是「全局顺序」。&lt;/li&gt;
&lt;li&gt;顺序消息失败时，优先考虑业务补偿，而不是无限重试或简单跳过。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七总结&#34;&gt;七、总结&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;选型不是单选题&lt;/strong&gt;：RocketMQ 适合在线业务，Kafka 适合日志/流处理，RabbitMQ 适合轻量快速落地，Redis MQ 适合轻量辅助场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推/拉不是对立关系&lt;/strong&gt;：RocketMQ 的 Push 底层是长轮询 Pull；RabbitMQ 是真 Push；Kafka 是真 Pull。选择看「谁来控制消费节奏」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长连接是双刃剑&lt;/strong&gt;：实时性好，但连接维护、背压、心跳都要考虑。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常处理要分层&lt;/strong&gt;：发送层确认、Broker 层持久化、消费层幂等 + 重试 + 死信。&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;hr&gt;
&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://ftmao.com/blog/blog/posts/rocketmq-fundamentals/&#34;&gt;RocketMQ 基础科普：推/拉/通知模式、异常处理与高并发实践&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;《RocketMQ 实战与核心原理》&lt;/li&gt;
&lt;li&gt;《RocketMQ 技术内幕》&lt;/li&gt;
&lt;li&gt;《Kafka 权威指南》&lt;/li&gt;
&lt;li&gt;《RabbitMQ 实战指南》&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果对你有帮助，欢迎在评论区交流你在 MQ 选型或异常处理上的经验。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>RocketMQ 基础科普：推/拉/通知模式、异常处理与高并发实践</title>
      <link>https://ftmao.com/blog/posts/rocketmq-fundamentals/</link>
      <pubDate>Sun, 28 Jun 2020 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/rocketmq-fundamentals/</guid>
      <description>&lt;h1 id=&#34;rocketmq-基础科普推拉通知模式异常处理与高并发实践&#34;&gt;RocketMQ 基础科普：推/拉/通知模式、异常处理与高并发实践&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;这篇文章把 RocketMQ 当成一个「分布式异步任务管道」来理解，重点回答六个问题：RocketMQ 和 RabbitMQ、Kafka 怎么选？它们的底层原理差异是什么？消费者到底该推、该拉还是该通知？消息异常了怎么处理？怎么让它扛住高并发？以及那些年我们踩过的坑。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一rocketmq-是什么一句话定位&#34;&gt;一、RocketMQ 是什么？一句话定位&lt;/h2&gt;
&lt;p&gt;RocketMQ 是阿里巴巴开源的&lt;strong&gt;分布式消息中间件&lt;/strong&gt;，核心定位是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;高吞吐、低延迟、可堆积、可扩展的异步消息传输与解耦基础设施。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&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;：突发流量先写入队列，下游按自身能力消费。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;RocketMQ 的核心角色很简单：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    Producer[&amp;#34;Producer（消息生产者）&amp;#34;]
    NameServer[&amp;#34;NameServer（路由发现）&amp;#34;]
    Broker[&amp;#34;Broker（存储与转发消息）&amp;#34;]
    Consumer[&amp;#34;Consumer（拉取/接收消息）&amp;#34;]

    Producer -.路由发现.-&amp;gt; NameServer
    Producer --&amp;gt; Broker
    Consumer -.路由发现.-&amp;gt; NameServer
    Broker --&amp;gt; Consumer&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Producer&lt;/strong&gt;：消息生产者，负责把业务消息发到 Broker。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Broker&lt;/strong&gt;：消息存储和转发节点，分 Master/Slave，负责持久化、投递、重试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NameServer&lt;/strong&gt;：轻量级路由注册中心，Producer 和 Consumer 通过它发现 Broker。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consumer&lt;/strong&gt;：消息消费者，从 Broker 读取消息并处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二横向对比rocketmq--rabbitmq--kafka&#34;&gt;二、横向对比：RocketMQ / RabbitMQ / Kafka&lt;/h2&gt;
&lt;p&gt;RocketMQ、RabbitMQ、Kafka 是国内最常见的三种消息中间件。它们都能做异步解耦，但设计哲学差异很大，选型时要结合业务场景、团队能力、运维成本一起看。&lt;/p&gt;
&lt;h3 id=&#34;21-核心定位差异&#34;&gt;2.1 核心定位差异&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;RocketMQ&lt;/th&gt;
					&lt;th&gt;RabbitMQ&lt;/th&gt;
					&lt;th&gt;Kafka&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;/td&gt;
					&lt;td&gt;Erlang 实现，面向通用企业消息&lt;/td&gt;
					&lt;td&gt;LinkedIn 开源，面向日志采集与流处理&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;数据模型&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Topic + Queue，支持 Tag 过滤&lt;/td&gt;
					&lt;td&gt;Exchange + Queue，路由灵活&lt;/td&gt;
					&lt;td&gt;Topic + Partition，分区有序&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;默认消费方式&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Push（长轮询封装）+ Pull&lt;/td&gt;
					&lt;td&gt;Push（AMQP 推送）&lt;/td&gt;
					&lt;td&gt;Pull（消费者主动拉取）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;顺序消息&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;分片内有序，Sharding Key 控制&lt;/td&gt;
					&lt;td&gt;单 Queue 有序&lt;/td&gt;
					&lt;td&gt;Partition 内有序&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;延迟消息&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;原生支持 18 个级别&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;事务消息&lt;/strong&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;strong&gt;消息轨迹&lt;/strong&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;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;22-性能对比&#34;&gt;2.2 性能对比&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;RocketMQ&lt;/th&gt;
					&lt;th&gt;RabbitMQ&lt;/th&gt;
					&lt;th&gt;Kafka&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;10 万级 TPS&lt;/td&gt;
					&lt;td&gt;万级 TPS&lt;/td&gt;
					&lt;td&gt;百万级 TPS（批量场景）&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;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;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;td&gt;极高&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;ul&gt;
&lt;li&gt;Kafka 的吞吐数字通常来自批量、压缩、顺序写的日志场景；单条小消息低延迟不如 RocketMQ。&lt;/li&gt;
&lt;li&gt;RabbitMQ 延迟低，但高吞吐和海量堆积不是强项。&lt;/li&gt;
&lt;li&gt;RocketMQ 在「中等吞吐 + 低延迟 + 强堆积」之间取得了较好平衡。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;23-成本对比&#34;&gt;2.3 成本对比&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;RocketMQ&lt;/th&gt;
					&lt;th&gt;RabbitMQ&lt;/th&gt;
					&lt;th&gt;Kafka&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;中等（NameServer + Broker 集群）&lt;/td&gt;
					&lt;td&gt;低（单节点即可运行）&lt;/td&gt;
					&lt;td&gt;高（ZooKeeper/KRaft + Broker）&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;低，但 Erlang 问题排查门槛高&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;阿里云 RocketMQ 成熟&lt;/td&gt;
					&lt;td&gt;各云厂商都有托管版&lt;/td&gt;
					&lt;td&gt;各云厂商 Kafka 托管版成熟&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;td&gt;高&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;24-生态对比&#34;&gt;2.4 生态对比&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;RocketMQ&lt;/th&gt;
					&lt;th&gt;RabbitMQ&lt;/th&gt;
					&lt;th&gt;Kafka&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;Java 最完善，其他语言有社区版&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;大数据生态&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;一般，可与 Flink/Spark 对接&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;监控工具&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;自带控制台 + 阿里云监控&lt;/td&gt;
					&lt;td&gt;自带 Management UI + Prometheus&lt;/td&gt;
					&lt;td&gt;Kafka UI + Prometheus + 自建&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;社区活跃度&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;国内活跃，Apache 项目&lt;/td&gt;
					&lt;td&gt;成熟稳定，Pivotal/VMware 背景&lt;/td&gt;
					&lt;td&gt;极其活跃，Confluent 生态&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;25-适用场景推荐&#34;&gt;2.5 适用场景推荐&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;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;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;延迟低、事务消息、顺序消息、监控完善&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;异步任务、分布式 Worker、视频渲染&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;PushConsumer 自动负载均衡，失败重试完善&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;传统企业应用、RPC 解耦、路由复杂&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;简单易用，Exchange 路由灵活，快速上线&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;日志采集、埋点、实时数仓、流处理&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&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;strong&gt;RabbitMQ / Kafka&lt;/strong&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;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;原生事务消息是差异化能力&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;26-一句话选型建议&#34;&gt;2.6 一句话选型建议&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;要可靠交易、事务消息、顺序消息、在线业务&lt;/strong&gt; → RocketMQ&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要快速搭建、路由灵活、团队规模小&lt;/strong&gt; → RabbitMQ&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;要日志/流处理/大数据管道、吞吐优先&lt;/strong&gt; → Kafka&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;很多大厂内部其实是「Kafka 做日志流 + RocketMQ 做业务消息 + RabbitMQ 做轻量任务」组合使用，而不是单选题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;27-底层原理与核心差异&#34;&gt;2.7 底层原理与核心差异&lt;/h3&gt;
&lt;p&gt;表面的功能差异（延迟消息、事务消息）背后，是三种产品在&lt;strong&gt;存储模型、路由发现、复制机制、消费模型&lt;/strong&gt;上的根本不同。理解这些底层差异，才能真正解释「为什么 Kafka 吞吐高」「为什么 RocketMQ 延迟低」「为什么 RabbitMQ 路由灵活」。&lt;/p&gt;
&lt;h4 id=&#34;存储模型&#34;&gt;存储模型&lt;/h4&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;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;CommitLog + ConsumeQueue&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;所有 Topic 的消息顺序写入同一个 CommitLog，再利用 ConsumeQueue 存储每个 Queue 的偏移索引。读消息时先查 ConsumeQueue 拿到物理偏移，再读 CommitLog。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Partition Segment + Index&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每个 Partition 是一个独立的日志目录，顺序写入 Segment 文件，配合稀疏索引加速消费。Partition 之间物理隔离。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Queue 独立存储&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每个 Queue 维护独立的消息索引和存储，默认写入 Mnesia/ETS，持久化消息写磁盘。Queue 越多，元数据开销越大。&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;ul&gt;
&lt;li&gt;&lt;strong&gt;RocketMQ 的 CommitLog 是全局顺序写&lt;/strong&gt;，不管有多少 Topic/Queue，磁盘写入都是顺序的，因此单机 I/O 效率很高；读取时需要两次 I/O（ConsumeQueue → CommitLog），但 ConsumeQueue 很小，通常能缓存命中。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kafka 的 Partition 是物理隔离的&lt;/strong&gt;，每个 Partition 内部顺序写，但 Partition 多了会形成随机写，所以 Kafka 更适合少量 Topic + 大量 Partition 的日志场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RabbitMQ 的 Queue 是逻辑和物理都独立的&lt;/strong&gt;，每条消息进入 Queue 都要维护索引，高吞吐下容易成为瓶颈。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;RocketMQ 存储模型：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    CL[&amp;#34;CommitLog（顺序写）&amp;lt;br/&amp;gt;[msg1][msg2][msg3][msg4][msg5]...&amp;#34;]
    CQ1[&amp;#34;ConsumeQueue&amp;lt;br/&amp;gt;TopicA-Q0&amp;#34;]
    CQ2[&amp;#34;ConsumeQueue&amp;lt;br/&amp;gt;TopicB-Q0&amp;#34;]

    CL -- &amp;#34;写入后分发偏移索引&amp;#34; --&amp;gt; CQ1
    CL -- &amp;#34;写入后分发偏移索引&amp;#34; --&amp;gt; CQ2&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;Kafka 存储模型：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    P0[&amp;#34;TopicA-P0 Log&amp;lt;br/&amp;gt;[msg1][msg2]...&amp;#34;]
    P1[&amp;#34;TopicA-P1 Log&amp;lt;br/&amp;gt;[msg1][msg2]...&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h4 id=&#34;路由与元数据管理&#34;&gt;路由与元数据管理&lt;/h4&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;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;NameServer&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;无状态集群，Broker 主动上报路由信息，Producer/Consumer 定时拉取。最终一致，轻量。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;ZooKeeper / KRaft&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;老版本依赖 ZooKeeper 存元数据、选 Controller；新版本用 KRaft 自洽协议。强一致要求更高。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;内置 Mnesia 数据库&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;集群节点共享元数据，Erlang 进程模型天然分布式，但集群扩展性相对有限。&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;ul&gt;
&lt;li&gt;&lt;strong&gt;NameServer 是「轻量级最终一致」&lt;/strong&gt;：Broker 启动时向所有 NameServer 注册，Producer 从 NameServer 拿到路由后缓存使用。Broker 宕机后，NameServer 不会主动通知 Producer，而是靠 Producer 心跳/发送失败时刷新路由。这种设计牺牲了一定的实时性，换来了极简的部署和无状态扩展能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kafka 的 Controller 负责元数据一致性&lt;/strong&gt;：分区副本分配、Leader 选举、ISR 维护都由 Controller 处理，更复杂但也更精确。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RabbitMQ 的元数据在节点间同步&lt;/strong&gt;：小集群简单，大集群（几百个 Queue）容易出现脑裂和同步压力。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;复制与高可用&#34;&gt;复制与高可用&lt;/h4&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;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Master-Slave 同步/异步复制&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;一个 Master 对应一个或多个 Slave。同步复制保证不丢消息，异步复制性能更高。Slave 可读可切换（需要人工或脚本介入）。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Partition 多副本 + ISR 机制&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每个 Partition 有 N 个副本，Leader 写，Follower 拉取；只有 ISR 中的副本才能竞选 Leader。自动 Failover，成熟度高。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&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;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;关键差异&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RocketMQ 的复制是 Broker 级别&lt;/strong&gt;，Master 上所有 Queue 一起复制到 Slave；Kafka 是 Partition 级别，复制粒度更细。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kafka 的 ISR 是动态维护的&lt;/strong&gt;，写入延迟受最慢 Follower 影响；RocketMQ 的同步复制是 Master 等待 Slave ACK，延迟相对可控。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RabbitMQ 默认不复制&lt;/strong&gt;，需要显式配置镜像队列，否则节点宕机就丢队列。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;消费模型与-offset-管理&#34;&gt;消费模型与 Offset 管理&lt;/h4&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;Offset 管理&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;长轮询 Pull 封装成 Push&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Consumer 本地维护消费进度，定时上报 Broker。支持集群消费和广播消费。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;消费者主动 Pull&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Offset 存在 Kafka 内部 Topic（&lt;code&gt;__consumer_offsets&lt;/code&gt;），Consumer 提交进度。&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Broker Push（AMQP）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;无 Offset 概念，消息 ACK 后从 Queue 移除。不支持消息回溯。&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;ul&gt;
&lt;li&gt;&lt;strong&gt;RocketMQ 的 Push 是「假 Push」&lt;/strong&gt;：Consumer 向 Broker 发起长轮询请求（默认 5 秒），Broker hold 住连接，有新消息时立即返回，没消息时超时返回再轮询。这样既实现了低延迟，又保留了 Pull 的背压能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kafka 的 Pull 是「真 Pull」&lt;/strong&gt;：Consumer 自己决定什么时候拉、拉多少，框架不负责推送，灵活性最高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RabbitMQ 的 Push 是「真 Push」&lt;/strong&gt;：Broker 把消息塞给 Channel，Consumer 处理完 ACK。如果 Consumer 处理慢，Broker 会堆积在内存或磁盘，容易把 Broker 压垮。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;顺序消息的实现&#34;&gt;顺序消息的实现&lt;/h4&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;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;同一 Topic 下，相同 Sharding Key 的消息发送到同一个 Message Queue；Consumer 单线程消费该 Queue&lt;/td&gt;
					&lt;td&gt;全局顺序需要所有消息进一个 Queue，吞吐极低&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;同一 Partition 内有序；Producer 按 Key 路由到固定 Partition&lt;/td&gt;
					&lt;td&gt;同 RocketMQ，Partition 内有序，全局有序不可行&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;单 Queue 天然有序；多个 Consumer 竞争消费时会乱序&lt;/td&gt;
					&lt;td&gt;要顺序消费只能单 Consumer 单线程&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;三种 MQ 的所谓「顺序消息」本质上都是&lt;strong&gt;把需要顺序的消息收敛到同一个最小调度单元&lt;/strong&gt;（Queue / Partition），然后单线程消费。不存在真正的全局顺序高吞吐方案。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&#34;延迟消息的实现&#34;&gt;延迟消息的实现&lt;/h4&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;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;内置 18 个延迟级别，消息先进入 SCHEDULE_TOPIC_XXXX，到期后转回真实 Topic&lt;/td&gt;
					&lt;td&gt;时间粒度固定，不支持任意时间；实现成熟可靠&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;无原生支持&lt;/td&gt;
					&lt;td&gt;常用方案：时间轮、RocketMQ 中转、Pulsar 等替代&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;通过 TTL + 死信队列（DLX）实现&lt;/td&gt;
					&lt;td&gt;每个延迟级别需要一个 Queue，灵活但配置繁琐&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&#34;事务消息的实现&#34;&gt;事务消息的实现&lt;/h4&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;RocketMQ&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;半消息 + 事务回查&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Producer 先发「半消息」（对消费者不可见），本地事务成功后 Commit，失败则 Rollback；Broker 会回查超时事务&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Kafka&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;无原生事务消息&lt;/td&gt;
					&lt;td&gt;Kafka 有幂等 Producer 和事务 API，但那是「跨分区事务」，不是「本地事务 + 消息发送」的事务消息&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;RabbitMQ&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;p&gt;RocketMQ 事务消息的核心流程：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;1. Producer 发送 Half Message 到 Broker&amp;#34;] --&amp;gt; B[&amp;#34;2. Producer 执行本地事务（如扣减库存）&amp;#34;]
    B --&amp;gt; C{&amp;#34;本地事务结果&amp;#34;}
    C --&amp;gt;|&amp;#34;成功&amp;#34;| D[&amp;#34;Commit&amp;#34;]
    C --&amp;gt;|&amp;#34;失败&amp;#34;| E[&amp;#34;Rollback&amp;#34;]
    D --&amp;gt; F[&amp;#34;3. Broker 投递消息给消费者&amp;#34;]
    E --&amp;gt; G[&amp;#34;3. Broker 删除半消息&amp;#34;]
    C -. &amp;#34;Producer 无响应&amp;#34; .-&amp;gt; H[&amp;#34;Broker 主动回查事务状态&amp;#34;]
    H --&amp;gt; C&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h4 id=&#34;为什么-rocketmq-更适合在线业务&#34;&gt;为什么 RocketMQ 更适合在线业务？&lt;/h4&gt;
&lt;p&gt;把底层差异串起来看，答案就清晰了：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CommitLog + ConsumeQueue 让它在消息量不大时延迟很低&lt;/strong&gt;（两次索引查询 + 顺序读），适合在线交易这种「消息体小、QPS 高、延迟敏感」的场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NameServer 轻量无状态&lt;/strong&gt;，部署和扩容简单，不像 Kafka 强依赖 ZooKeeper/KRaft。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;原生事务消息和延迟消息&lt;/strong&gt;，是电商、金融场景的刚需，其他两个 MQ 都需要业务侧自己造轮子。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长轮询 Push 模型&lt;/strong&gt;对业务开发者友好，自动重平衡、失败重试、死信队列开箱即用。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;而 Kafka 更适合&lt;strong&gt;海量日志、流计算&lt;/strong&gt;这种「吞吐优先、延迟可接受、需要分区有序」的场景；RabbitMQ 更适合&lt;strong&gt;轻量解耦、路由复杂、快速上线&lt;/strong&gt;的场景。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三推拉通知三种消费模式怎么选&#34;&gt;三、推、拉、通知三种消费模式：怎么选？&lt;/h2&gt;
&lt;p&gt;RocketMQ 的消费端主要提供 &lt;strong&gt;Push&lt;/strong&gt; 和 &lt;strong&gt;Pull&lt;/strong&gt; 两种消费方式，实际工程里还经常配合 &lt;strong&gt;Notify/回调&lt;/strong&gt; 一起用。三者不是非此即彼，而是适用场景不同。&lt;/p&gt;
&lt;h3 id=&#34;31-pull-模式消费者主动拉取&#34;&gt;3.1 Pull 模式：消费者主动拉取&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant C as Consumer
    participant B as Broker
    C-&amp;gt;&amp;gt;B: 有消息吗？
    B-&amp;gt;&amp;gt;C: 有，拿去吧&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;/li&gt;
&lt;li&gt;可以灵活控制拉取频率、批量大小、并发量。&lt;/li&gt;
&lt;li&gt;代码稍复杂，需要自己维护 offset、拉取循环、负载均衡。&lt;/li&gt;
&lt;/ul&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;/li&gt;
&lt;li&gt;对实时性要求不高，或者消费逻辑很重。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;权衡&lt;/strong&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;/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;需要自管 offset 和重平衡&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;32-push-模式broker-主动推送给消费者&#34;&gt;3.2 Push 模式：Broker 主动推送给消费者&lt;/h3&gt;
&lt;p&gt;RocketMQ 的 PushConsumer 本质上也是 &lt;strong&gt;Pull 的长轮询封装&lt;/strong&gt;，但对业务代码来说更像「Broker 推过来」。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant C as Consumer
    participant B as Broker
    C-&amp;gt;&amp;gt;B: 注册长轮询
    B-&amp;gt;&amp;gt;C: 来消息了，处理一下&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;/li&gt;
&lt;li&gt;RocketMQ 自动处理 offset、重平衡、失败重试。&lt;/li&gt;
&lt;li&gt;实时性好，能立刻感知新消息。&lt;/li&gt;
&lt;/ul&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;/li&gt;
&lt;li&gt;消费逻辑相对轻量、可并发扩展。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;权衡&lt;/strong&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;/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;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;33-notify--回调模式处理完主动通知上游&#34;&gt;3.3 Notify / 回调模式：处理完主动通知上游&lt;/h3&gt;
&lt;p&gt;消息队列本身不负责回调，但业务常把 MQ 和回调配合起来用：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;上游&amp;#34;] --&amp;gt; MQ[&amp;#34;MQ&amp;#34;] --&amp;gt; C[&amp;#34;消费者处理&amp;#34;] --&amp;gt; N[&amp;#34;回调 / 事件总线 / SSE&amp;#34;] --&amp;gt; U[&amp;#34;上游或其他系统&amp;#34;]&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;不是 RocketMQ 原生消费模式，而是业务架构模式。&lt;/li&gt;
&lt;li&gt;适合「异步任务完成后需要通知调用方」的场景，比如视频渲染、订单状态变更。&lt;/li&gt;
&lt;li&gt;通常需要配合幂等、状态机、重试策略一起设计。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;适合场景&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;任务完成后需要反向通知 UI、APP、第三方系统。&lt;/li&gt;
&lt;li&gt;长耗时异步任务 + 进度推送。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;34-选择原则总结&#34;&gt;3.4 选择原则总结&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;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;strong&gt;PushConsumer&lt;/strong&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;strong&gt;PullConsumer&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;控制力强，避免被消息压垮&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据同步、ETL、定时批量&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;Pull / 批量拉取&lt;/strong&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;strong&gt;Push + 业务回调/Notify&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;MQ 解耦 + 回调闭环&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;不确定怎么选&lt;/td&gt;
					&lt;td&gt;&lt;strong&gt;先 Push，遇到瓶颈再切 Pull&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Push 是默认最佳实践&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一个常见误区是认为 Push 一定比 Pull 先进。实际上 RocketMQ 的 Push 底层也是拉，只是框架帮你做了。真正决定选型的不是「推或拉」，而是 &lt;strong&gt;「谁来控制消费节奏」和「实时性与吞吐的优先级」&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四异常处理消息丢了重复了卡住了怎么办&#34;&gt;四、异常处理：消息丢了、重复了、卡住了怎么办？&lt;/h2&gt;
&lt;p&gt;消息队列的异常处理是面试和工程中最核心的部分。RocketMQ 提供了机制，但业务层必须配合。&lt;/p&gt;
&lt;h3 id=&#34;41-消息丢失从哪里可能丢&#34;&gt;4.1 消息丢失：从哪里可能丢？&lt;/h3&gt;
&lt;p&gt;消息丢失通常发生在三个环节：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    P[&amp;#34;Producer&amp;#34;] -- &amp;#34;发送失败&amp;#34; --&amp;gt; B[&amp;#34;Broker&amp;#34;] -- &amp;#34;刷盘/同步失败&amp;#34; --&amp;gt; C[&amp;#34;Consumer&amp;#34;] -- &amp;#34;消费失败未重试&amp;#34; --&amp;gt; Biz[&amp;#34;业务&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;防范措施&lt;/strong&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;Producer 发送&lt;/td&gt;
					&lt;td&gt;同步发送 + 失败重试&lt;/td&gt;
					&lt;td&gt;重要消息用 &lt;code&gt;send_sync&lt;/code&gt;，不要用 oneway&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Broker 存储&lt;/td&gt;
					&lt;td&gt;同步刷盘 + 主从同步&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;SYNC_MASTER&lt;/code&gt; + &lt;code&gt;SYNC_FLUSH&lt;/code&gt; 保证落盘&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Consumer 消费&lt;/td&gt;
					&lt;td&gt;先处理业务再返回成功&lt;/td&gt;
					&lt;td&gt;不要先 ACK 再执行业务&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;RocketMQ 的发送确认策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SYNC&lt;/strong&gt;：同步等待 Broker ACK，最可靠但最慢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ASYNC&lt;/strong&gt;：异步发送，失败时回调，吞吐高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ONEWAY&lt;/strong&gt;：只发不管，最快但不可靠，适合日志等可丢场景。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;42-消息重复不可避免靠幂等解决&#34;&gt;4.2 消息重复：不可避免，靠幂等解决&lt;/h3&gt;
&lt;p&gt;RocketMQ 保证「至少一次投递」，不保证「恰好一次」。网络抖动、Broker 重试、Consumer 重启都会导致重复消费。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;幂等设计要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;唯一业务 Key&lt;/strong&gt;：发送时给消息设置 &lt;code&gt;keys&lt;/code&gt;，消费时用业务 Key 去重。&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;code&gt;pending → paid → shipped&lt;/code&gt;，重复消息不会触发非法流转。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redis 去重&lt;/strong&gt;：短期用 &lt;code&gt;SETNX msg_key 1&lt;/code&gt; 配合过期时间，适合无数据库或高速场景。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 伪代码：基于数据库唯一索引做幂等&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;consume&lt;/span&gt;(msg):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    biz_key &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; msg&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;keys
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        idempotent_record&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;insert(biz_key&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;biz_key, status&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;processing&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; UniqueViolation:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CONSUME_SUCCESS  &lt;span style=&#34;color:#75715e&#34;&gt;# 已处理过，直接成功&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        do_business(msg)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        idempotent_record&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update(biz_key, status&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;done&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CONSUME_SUCCESS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Exception&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        idempotent_record&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update(biz_key, status&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;failed&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;RECONSUME_LATER&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;43-消费失败重试与死信队列&#34;&gt;4.3 消费失败：重试与死信队列&lt;/h3&gt;
&lt;p&gt;RocketMQ 对消费失败的消息会自动重试，默认最多 16 次，间隔逐渐拉长。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重试间隔（默认值，Broker 端可配置）&lt;/strong&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;1&lt;/td&gt;
					&lt;td&gt;10 秒&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;2&lt;/td&gt;
					&lt;td&gt;30 秒&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;3&lt;/td&gt;
					&lt;td&gt;1 分钟&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&amp;hellip;&lt;/td&gt;
					&lt;td&gt;递增&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;16&lt;/td&gt;
					&lt;td&gt;约 2 小时&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;重试仍然失败后，消息进入 &lt;strong&gt;死信队列（DLQ）&lt;/strong&gt;，Topic 名为 &lt;code&gt;%DLQ%consumer_group&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;处理建议&lt;/strong&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;：DLQ 不是垃圾桶，需要监控和补偿机制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;区分可重试异常和不可重试异常&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;网络超时 → 可重试&lt;/li&gt;
&lt;li&gt;参数非法 / 依赖返回 4xx / 业务规则冲突 → 不可重试，直接失败或进死信&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;consume&lt;/span&gt;(msg):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        process(msg)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CONSUME_SUCCESS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; RetryableException &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; e:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# 可重试异常：网络抖动、依赖短暂不可用&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        log&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;warn(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;retryable: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;e&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;RECONSUME_LATER
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; NonRetryableException &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; e:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# 不可重试异常：直接进死信，避免浪费重试次数&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        log&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;error(&lt;span style=&#34;color:#e6db74&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;non-retryable: &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;{&lt;/span&gt;e&lt;span style=&#34;color:#e6db74&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#75715e&#34;&gt;# 某些客户端可通过返回特定状态或手动移入死信&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CONSUME_SUCCESS  &lt;span style=&#34;color:#75715e&#34;&gt;# 业务侧兜底，记录失败原因&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;44-消息卡住消费超时与心跳检测&#34;&gt;4.4 消息卡住：消费超时与心跳检测&lt;/h3&gt;
&lt;p&gt;Consumer 处理到一半进程挂了，消息状态可能卡在「处理中」。《RocketMQ 技术内幕》里强调：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;消息队列只保证投递，不保证业务逻辑一定完成。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&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;：Consumer 处理任务时定时更新心跳，超时时由兜底任务重置状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;进度监控&lt;/strong&gt;：关注消费进度（ConsumeProgress），发现停滞及时告警。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# 伪代码：心跳续命 + 超时释放&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;process_task&lt;/span&gt;(task_id):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    claim_task(task_id, worker_id&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;WORKER_ID, ttl&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;300&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; heartbeat_renewal(task_id, interval&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            do_heavy_work(task_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        mark_done(task_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Exception&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        release_task(task_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;raise&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;五如何支撑高并发&#34;&gt;五、如何支撑高并发？&lt;/h2&gt;
&lt;p&gt;RocketMQ 本身的吞吐很高（单机万级 TPS 没问题），但业务侧能不能撑住高并发，取决于怎么用它。&lt;/p&gt;
&lt;h3 id=&#34;51-生产者侧优化&#34;&gt;5.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;strong&gt;批量发送&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;把小消息攒成一批发送，减少 RTT&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;code&gt;send_async&lt;/code&gt;，失败回调补偿&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;多 Producer 实例&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;生产端水平扩展，分摊发送压力&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;合理选择 Topic/Queue 数量&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Queue 数影响并行度，太多会增加元数据开销&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;关闭不必要的消息轨迹&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;非核心场景关闭 msg_trace 减少开销&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;52-broker-侧优化&#34;&gt;5.2 Broker 侧优化&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;strong&gt;集群部署&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;多 Master 多 Slave，避免单点&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;读写分离（Slave 读）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;消费压力大时，从 Slave 拉取减轻 Master 压力&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;code&gt;ASYNC_FLUSH&lt;/code&gt; 性能更高，但可能丢少量消息&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;分片存储（CommitLog）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;利用顺序写磁盘，提升吞吐&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;JVM/网卡/磁盘调优&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;消息队列是 IO 密集型，磁盘 SSD、网卡万兆是基础&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;53-消费者侧优化&#34;&gt;5.3 消费者侧优化&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;strong&gt;增加 Consumer 实例&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;消费组内实例数 ≤ Queue 数时，扩容能线性提升吞吐&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;提高单实例并发&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;PushConsumer 可配置 consumeThreadMin/Max&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;批量消费&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;一次拉取多条，合并数据库/Redis 操作&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;消费逻辑异步化&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;IO 密集型任务不要阻塞消费线程&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;本地缓存热点数据&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;减少重复 RPC/DB 查询&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;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;总消费吞吐 = Consumer 实例数 × 每个实例的并发线程数 × 单线程处理 TPS&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;当 Consumer 实例数达到 Topic 的 Queue 数后，再增加实例也无法提升并行度，这时需要增加 Queue 数或优化单线程处理速度。&lt;/p&gt;
&lt;h3 id=&#34;54-削峰填谷的核心思想&#34;&gt;5.4 削峰填谷的核心思想&lt;/h3&gt;
&lt;p&gt;高并发不是无限扩容，而是&lt;strong&gt;用队列把流量削平&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    A[&amp;#34;突发流量&amp;#34;] --&amp;gt; B[&amp;#34;Broker 队列堆积&amp;lt;br/&amp;gt;（容忍峰值）&amp;#34;] --&amp;gt; C[&amp;#34;Consumer 按稳定速率消费&amp;lt;br/&amp;gt;（保护下游）&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;所以设计高并发系统时，Queue 的堆积能力本身就是「容量缓冲」。只要 Consumer 能最终追上来，系统就是健康的。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六避坑指南&#34;&gt;六、避坑指南&lt;/h2&gt;
&lt;h3 id=&#34;61-消费组订阅不一致&#34;&gt;6.1 消费组订阅不一致&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;坑&lt;/strong&gt;：同一个 ConsumerGroup 下的不同实例订阅了不同的 Topic 或 Tag，导致重平衡混乱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：一个 ConsumerGroup 只订阅一个 Topic + 一套 Tag 表达式，不要随意变更。&lt;/p&gt;
&lt;h3 id=&#34;62-tag-用得太细&#34;&gt;6.2 Tag 用得太细&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;坑&lt;/strong&gt;：每个业务场景都建一个 Tag，结果消费者端过滤复杂，Topic 管理混乱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：Topic 按业务域划分，Tag 做粗粒度过滤。不要用一个 Topic 承载完全无关的业务。&lt;/p&gt;
&lt;h3 id=&#34;63-消息体过大&#34;&gt;6.3 消息体过大&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;坑&lt;/strong&gt;：把完整对象、图片二进制、大 JSON 塞进消息体，超过 4MB 默认限制，发送失败。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：消息体只传 ID 和必要字段，完整数据由 Consumer 从存储（DB/Redis/OSS）拉取。&lt;/p&gt;
&lt;h3 id=&#34;64-顺序消息误用&#34;&gt;6.4 顺序消息误用&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;坑&lt;/strong&gt;：为了「简单」把所有消息都当顺序消息发，结果全局顺序严重拖垮吞吐。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：RocketMQ 的顺序消息是「分片内有序」，不是全局有序。只在真正需要局部顺序的场景用（比如同一订单的操作按顺序处理），并且用业务 Key 做 Sharding。&lt;/p&gt;
&lt;h3 id=&#34;65-忽视-nameserver-路由更新延迟&#34;&gt;6.5 忽视 NameServer 路由更新延迟&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;坑&lt;/strong&gt;：Broker 扩缩容后，Producer/Consumer 还没刷新路由，导致发送到已下线的 Broker。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：理解 NameServer 是最终一致的路由发现，扩缩容时留足等待时间或主动触发重连。&lt;/p&gt;
&lt;h3 id=&#34;66-producerconsumer-启动顺序问题&#34;&gt;6.6 Producer/Consumer 启动顺序问题&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;坑&lt;/strong&gt;：Consumer 先启动但 Topic 还没创建，订阅失败；或 Producer 先启动但 NameServer 还没 ready。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：生产环境用自动化方式提前创建 Topic 和 ConsumerGroup，不要依赖自动创建。&lt;/p&gt;
&lt;h3 id=&#34;67-忽视监控与告警&#34;&gt;6.7 忽视监控与告警&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;核心监控指标&lt;/strong&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;消息堆积量（Consumer Lag）&lt;/td&gt;
					&lt;td&gt;是否需要扩容 Consumer&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;消费 TPS&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;Broker CPU/内存/磁盘 IO&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;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;68-版本与客户端兼容性&#34;&gt;6.8 版本与客户端兼容性&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;坑&lt;/strong&gt;：服务端和客户端版本差距大，出现协议不兼容或特性缺失。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：尽量保持服务端与客户端版本一致，升级时先升级服务端再升级客户端，灰度验证。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七总结&#34;&gt;七、总结&lt;/h2&gt;
&lt;p&gt;RocketMQ 不是银弹，但在大多数分布式异步场景里是非常稳妥的选择。用好它，关键是理解几个核心原则：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;推/拉/通知不是对立关系&lt;/strong&gt;：Push 适合大多数业务，Pull 适合重任务和批量，Notify 是业务层闭环。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异常处理要分三层&lt;/strong&gt;：发送层用同步/异步确认，Broker 层用刷盘和主从，消费层用幂等 + 重试 + 死信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;高并发 = 水平扩展 + 削峰填谷&lt;/strong&gt;：Queue 是缓冲，Consumer 是处理单元，单线程效率决定上限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;避坑的核心是规范&lt;/strong&gt;：订阅一致、消息体小、顺序消息慎用、监控到位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选型不是单选题&lt;/strong&gt;：RocketMQ 适合在线业务，Kafka 适合日志流处理，RabbitMQ 适合轻量快速落地，大厂往往是组合使用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;底层差异决定选型&lt;/strong&gt;：CommitLog + ConsumeQueue、NameServer、长轮询 Push、原生事务/延迟消息，共同决定了 RocketMQ 在在线业务场景的优势。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;《RocketMQ 实战与核心原理》&lt;/li&gt;
&lt;li&gt;《RocketMQ 技术内幕》&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://cloud.tencent.com/developer/article/2615437&#34;&gt;RocketMQ 消息消费核心原理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://zhuanlan.zhihu.com/p/1978935567824736405&#34;&gt;RocketMQ 高并发与异常处理实践&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你也在用 RocketMQ 做异步化改造，欢迎在评论区交流推拉选型、重试策略或高并发调优经验。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
