<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/rss.xsl" type="text/xsl"?>
<rss version="2.0">
  <channel>
    <title>IT瘾设计推荐</title>
    <link>https://itindex.net/categories/设计</link>
    <description>IT社区推荐资讯 - ITIndex.net</description>
    <language>zh</language>
    <copyright>https://itindex.net/</copyright>
    <generator>https://itindex.net/</generator>
    <docs>http://backend.userland.com/rss</docs>
    <image>
      <url>https://itindex.net/images/logo.gif</url>
      <title>IT社区推荐资讯 - ITIndex.net</title>
      <link>https://itindex.net/categories/设计</link>
    </image>
    <item>
      <title>斯坦福大学人生设计课</title>
      <link>https://itindex.net/detail/62974-%E6%96%AF%E5%9D%A6%E7%A6%8F-%E5%A4%A7%E5%AD%A6-%E4%BA%BA%E7%94%9F</link>
      <description>&lt;p&gt;  &lt;img alt="image" src="https://ipfs.crossbell.io/ipfs/Qme48Vt86gvXUiNA6wP9dDmsKrWu597tK2m6nwo5hGXmWw"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;div&gt;Copy  &lt;pre&gt;   &lt;code&gt;思维误区1： 你的学位决定你的职业生涯。
重新定义： 3/4的大学毕业生最后从事的工作都与他们所学的专业 无关。

思维误区2： 获得成功就会感到幸福。
重新定义： 真正的幸福源于设计有意义的人生。

思维误区3： 太晚了。
重新定义： 设计一种自己热爱的生活，何时都不晚。

思维误区4： 我应该已经知道自己的目的地了。
重新定义： 只有你了解了自己现在的位置，你才能知道如何到达目的地。

思维误区5： 我应该知道前进的目标。
重新定义： 我不可能一直知道自己前进的目标，但是我会清楚自 己的方向是否正确。

思维误区6： 我陷入了困境。
重新定义： 我从来不会被困住，因为我总是能想出很多点子。

思维误区7： 我必须找到一个正确的想法。
重新定义： 我需要很多想法，这样就可以为我的未来探索出尽可能多的机会。

思维误区8： 我需要找到最佳的生活方式，然后制订计划，实现它。
重新定义： 有无数美好的生活（和计划）等着我，我可以自主做出选择，打造我的人生路。

思维误区9： 如果我全面研究了有关计划的所有数据，那么我就一定会成功。
重新定义： 我应该进行原型设计，从中发现可选计划中存在的问题。

思维误区10： 找工作时，首先关注你自己的需求。
重新定义： 你应该关注的是人事经理的需求——他想找到一个合适的人。

思维误区11： 我理想中的工作就在那里等着我。
重新定义： 你需要通过积极寻找和再创造，设计你的理想工作。

思维误区12： 关系网就是一群虚伪的人。
重新定义： 关系网只是为了咨询方向。

思维误区13： 我正在找工作。
重新定义： 我正在寻找大量的工作机会。

思维误区14： 要想幸福，我就不得不做出正确的选择。
重新定义： 没有正确的选择——只有好的选择。

思维误区15： 幸福就是拥有一切。
重新定义： 幸福是断舍离。

思维误区16： 我们以结果评判我们的人生。
重新定义： 人生是一个过程，而不是一个结果。

思维误区17： 人生是一个有限游戏，有赢家，也有失败者。
重新定义： 人生是一个无限游戏，无所谓输赢。

思维误区18： 这是我的生活，我可以独立完成设计。
重新定义： 要想活出自己的人生，对人生进行设计，就需要与他人进行合作。

思维误区19： 我完成了人生设计，艰难的部分已经完成，从此将一 切顺利。
重新定义： 人生设计是没有尽头的——生活是一个有趣且不间断 的设计项目，我永远在前行，永不停歇。
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;div&gt;Copy  &lt;pre&gt;   &lt;code&gt;设计思维的5种基本心态：
好奇心
努力实践
重新定义
专注
深度合作



&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;h3&gt;一  分析测评你目前的生活&lt;/h3&gt;
 &lt;p&gt;  &lt;strong&gt;&amp;quot;健康 / 工作 / 爱 / 娱乐&amp;quot; 仪表盘&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#23631;&amp;#24149;&amp;#25130;&amp;#22270;_11-2-2025_141218_" src="https://ipfs.crossbell.io/ipfs/QmXwcxPuG7ub7ddigJTW5BWw2C6ZMVUcNL3BJD3tygtsn5"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;二 创建人生的指南针&lt;/h3&gt;
 &lt;p&gt;  &lt;code&gt;创建人生的指南针，需要两样东西：工作观、人生观&lt;/code&gt;&lt;/p&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;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;我为什么在这里？&lt;/li&gt;
  &lt;li&gt;生活的意义或者目的是什么？&lt;/li&gt;
  &lt;li&gt;个人和他人之间有什么关系？&lt;/li&gt;
  &lt;li&gt;家庭、国家和周围世界的融合点在哪里？&lt;/li&gt;
  &lt;li&gt;什么是善？什么是恶？&lt;/li&gt;
  &lt;li&gt;是否存在更高级的力量？比如上帝或者其他超自然的事物？如果存在，这将对你的生活产生什么影响？&lt;/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;h3&gt;三 寻路（乐趣会指引你找到适合你的工作）&lt;/h3&gt;
 &lt;p&gt;“美好时光日志” 中要包含两个元素：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;活动记录（记录能让你全身心投入并感到能量充沛的活动）&lt;/li&gt;
  &lt;li&gt;反思（哪些活动让你有收获？收获是什么？）&lt;/li&gt;
  &lt;li&gt;一周最好不要少于两次&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="&amp;#23631;&amp;#24149;&amp;#25130;&amp;#22270; 2025-02-11 142634" src="https://ipfs.crossbell.io/ipfs/QmYLDCrWzvRpke7MHUgVg4LyK9LEEDUU7vDXEkFRQien27"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;AEIOU 法的妙用&lt;/strong&gt;  &lt;br /&gt;
AEIOU 法共包括 5 套问题。在你对活动日志进行反思时，可以利用这 5 套问题。&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;li&gt;用户：活动中还有其他人吗？他们扮演了什么角色？他们为活动带 来了正面影响还是负面影响？&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;回顾你的高峰体验&lt;/strong&gt;  &lt;br /&gt;
你可以花些时间回忆一下过去和工作相关的高峰 体验，并记录在 “美好时光日志” 的活动日志中，对这些活动认真思考一 下，看看会发现什么。&lt;/p&gt;
 &lt;h3&gt;四 摆脱困境&lt;/h3&gt;
 &lt;p&gt;  &lt;strong&gt;打开思路 拒绝自我设限&lt;/strong&gt;  &lt;br /&gt;
作为一名人生设计师，你需要接受下面两种观点：  &lt;br /&gt;
1. 只有好点子足够多，你才能从中选择出更好的。  &lt;br /&gt;
2. 对于任何问题，都不要选择第一个解决方案。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;第一个构思技巧：制作思维导图&lt;/strong&gt;  &lt;br /&gt;
制作思维导图的过程包括三个步骤：  &lt;br /&gt;
1. 选取一个主题  &lt;br /&gt;
2. 制作思维导图  &lt;br /&gt;
3. 制作次级连接，并创造概念（将概念连接起来，建立概念混搭模式）&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image" src="https://ipfs.crossbell.io/ipfs/QmRfkWRVRCmJpKxAWTdJDz25triuAXB4BwhGfM7nrdNF5F"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;辨识无法自行消失的 “锚问题”&lt;/strong&gt;  &lt;br /&gt;
“锚问题” 是一个现实问题，它只是难以解决，它具有可操作性 —— 但是因为我们被困在上面的时间太长，所以感觉它变得难以逾越了。（这就是必须对锚问题重新定义的原因。这类问题需要我们开拓思路、寻找新的解决方法，如原型测试。）  &lt;br /&gt;
但是，“重力问题” 并不是一个真实的问题，它属于你无法改变的情况。“重力问题” 没有解决方法。你只能重新定向。&lt;/p&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;h3&gt;五 制订你的 “奥德赛计划”&lt;/h3&gt;
 &lt;p&gt;  &lt;strong&gt;畅想人生的多种可能性&lt;/strong&gt;  &lt;br /&gt;
  &lt;strong&gt;请你想象一下，然后写出未来 5 年的三个版本的人生计划。我们将这种 方法称为 “奥德赛计划”。&lt;/strong&gt;  &lt;br /&gt;
每一个 “奥德赛计划” 都应是计划 A，因为它是你真正的渴望，有可能实现。“奥德赛计划” 就是潜在的可 能性草图，它可以激发你的想象力，帮助你选择前进的方向，让你开始 原型设计，推动你的人生。&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;p&gt;  &lt;img alt="image" src="https://ipfs.crossbell.io/ipfs/QmevZomVaJGwUynLoQERuTRxChcXRDtsgfNa1untKwRNZ5"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;1. 一个直观的 / 图解形式的时间明细表，包括私人的、与工作无关 的事情。例如，你是否想结婚？你想参加培训而后在健身大赛中取得胜利吗？你希望学习通过意念掰弯汤勺吗？&lt;/li&gt;
  &lt;li&gt;2. 为每个计划拟定一个六字标题，描述计划的核心内容。&lt;/li&gt;
  &lt;li&gt;3. 针对每个计划提出两到三个问题。优秀的设计师会通过提问进 行测试，发现新想法。在每一个时间明细表中，你可以尝试各种不同 的可能性，多方了解自己和周围的世界。在这三个人生计划中，你想 测试并探索哪些事情？&lt;/li&gt;
  &lt;li&gt;4. 填写 “仪表盘”，评估如下内容：   &lt;br /&gt;
・物力（你拥有客观资源吗？例如时间、金钱、人脉，这些都是你 实现计划所必需的）   &lt;br /&gt;
・喜欢程度（你对这三个计划的态度如何？迫切、缺少热情还是满 怀热情？）   &lt;br /&gt;
・自信心（你相信自己一定会实现计划，还是不确定？）   &lt;br /&gt;
・一致性（这些计划本身有意义吗？它们与你及你的工作观和人生观是否一致？）   &lt;br /&gt;
・潜在考量：   &lt;br /&gt;
・地理环境 —— 你住在哪里？   &lt;br /&gt;
・你会获得哪些经验？   &lt;br /&gt;
・如果选择了其中一种计划，这将对你产生什么影响或者带来什么   &lt;br /&gt;
・你的生活会变成什么样？你希望自己成为什么样的人？你希望自己在什么行业或企业工作？   &lt;br /&gt;
・其他内容：   &lt;br /&gt;
・除了事业和金钱，其他事情你也要放在心上。事业和金钱，对你随后几年的发展方向能够起决定性作用，但仍然有一些其他的关键因素需要引起你的注意。   &lt;br /&gt;
・上述的任何一个因素都可能成为你随后几年人生计划的起点。如果你发现自己陷入困境，就试着根据上述列举的设计因素绘制一张思维 导图吧。关于这个练习，你不要思虑过度，也不要跳过不做。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;分享你的人生计划&lt;/strong&gt;  &lt;br /&gt;
最有趣、最有效的完成人生设计过程的方法就是组建团队， 有 3~6 个成员，包括你自己&lt;/p&gt;
 &lt;h3&gt;六 原型设计&lt;/h3&gt;
 &lt;p&gt;  &lt;strong&gt;学会提问&lt;/strong&gt;  &lt;br /&gt;
  &lt;code&gt;建造即思考&lt;/code&gt;  &lt;br /&gt;
进行原型设计主要是为了提出有建设意义的问题，暴露出我们内 心深处的偏好和设想，迅速重复反馈，逼近目标，为我们想要尝试的人生路提供前进的动力。  &lt;br /&gt;
  &lt;code&gt;我们的理念是，对你感兴趣的事，一定要进行原型设计。&lt;/code&gt;  &lt;br /&gt;
  &lt;code&gt;原型设计并不是指在大脑中进行思维实验，原型设计必须是现实生活中的一次实际体验。有益的数据只存在于真实世界中，原型设计最好的方法是亲身参与到你感兴趣的领域，这样才能获得所需的信息。&lt;/code&gt;  &lt;br /&gt;
  &lt;strong&gt;   &lt;code&gt;因此，原型设计包括提出有难度的问题、创造体验、提出假设、快速失败，并且在失败中前进，悄悄走近你的未来。&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;进行原型对话 —— 人生设计采访&lt;/strong&gt;  &lt;br /&gt;
人生设计采访非常简单，就是了解他人的经历。  &lt;br /&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;br /&gt;
  &lt;strong&gt;展开头脑风暴&lt;/strong&gt;  &lt;br /&gt;
想想看，你的问题是什么？通过原型体验，你还想了解些什么？  &lt;br /&gt;
  &lt;code&gt;头脑风暴的最终目的，是找到可以 进行原型设计并且可以在现实生活中进行尝试的想法。&lt;/code&gt;  &lt;br /&gt;
  &lt;code&gt;头脑风暴规则： &lt;/code&gt;  &lt;br /&gt;
  &lt;code&gt;1.重量不重质。 2.不要对他人提出的想法立即做出评判，也不要进行修改。 3.在其他人的想法上进行创新。 4.鼓励大家说出大胆疯狂的想法。&lt;/code&gt;  &lt;br /&gt;
在人生设计中，头脑风暴法分为四步：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;步骤 1：提出一个合适的问题（开放式、不要太过宽泛、不要包含解决方案）&lt;/li&gt;
  &lt;li&gt;步骤 2：热身&lt;/li&gt;
  &lt;li&gt;步骤 3：集思广益&lt;/li&gt;
  &lt;li&gt;步骤 4：为结果命名并进行规划（小组成员提出的想法可以根据 主题或者范畴进行分类，并分别给它们起一个名字，然后根据最初的焦点问题对结果进行设计）&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;li&gt;如果你陷入了困境，并且你已经召集了一个优秀的头脑风暴小 组，那么召开头脑风暴会议可以让你得到各种可能的解决方法。（找不 到团队？那就试试思维导图。）&lt;/li&gt;
  &lt;li&gt;积极寻找一些人进行人生设计采访，并亲身参与体验，打造你的 原型体验。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;七 成功求职的秘密&lt;/h3&gt;
 &lt;p&gt;  &lt;strong&gt;读懂职位描述之外的含义&lt;/strong&gt;  &lt;br /&gt;
网络招聘的内幕信息：  &lt;br /&gt;
1. 网络上的职位描述通常并不是由人事经理或者真正了解该工作的 人写的。  &lt;br /&gt;
2. 职位描述几乎从来不会写出成功获取这份工作所需要的条件。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;找工作的首要原则是 “适合”&lt;/strong&gt;  &lt;br /&gt;
下面是我们总结的一些小建议，可以帮助你在网络上更有效地找到满意的工作：  &lt;br /&gt;
建议 1：重写简历，使用和职位描述中一样的词汇。  &lt;br /&gt;
建议 2：如果你具备职位描述中提到的技能，那么就写进你的简历 里，同样要使用职位描述中所使用的词汇。  &lt;br /&gt;
建议 3：在你的简历中，着重突出职位描述中提到的关键技能。  &lt;br /&gt;
建议 4：如果有面试机会，你一定要带一份最新的、干净整洁的简历。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;尽早放弃 “超级工作”&lt;/strong&gt;  &lt;br /&gt;
  &lt;code&gt;在一个运行良好的劳动力市场中，一条工作招聘信息公布的 时间不会超过4周（最多6周）。&lt;/code&gt;  &lt;br /&gt;
  &lt;code&gt;根据我们的经验，如果已经有超过8个人饱受这场面试的折磨，而且公司还没有确定人选，那么这个招聘过程可能已经失败了。这也预示 着此公司不是一个很好的工作地点，你应该尽快离开。&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;蒙人的 “幽灵” 招聘&lt;/strong&gt;  &lt;br /&gt;
已有人选，形式招聘&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;知名公司里的 “假阳性” 和 “伪阴性”&lt;/strong&gt;  &lt;br /&gt;
如果一家公司误认为某个应聘者很杰出，而 实际上这个人能力非常一般，这就是 “假阳性”。  &lt;br /&gt;
“伪阴性”—— 错误地认为一个真正优秀的应聘者能力不强。  &lt;br /&gt;
  &lt;code&gt;如果你想在一家知名企业工作，那么你最好与公司内部的人取得联系，然后利用原型对话的方法——私人联系会带给你巨大帮助。&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;让工作更完美&lt;/strong&gt;&lt;/p&gt;
 &lt;h3&gt;八 好工作是设计出来的&lt;/h3&gt;
 &lt;p&gt;  &lt;strong&gt;发挥人际网的作用&lt;/strong&gt;  &lt;br /&gt;
“关系网” 与其说是一个名词，还不如说是一个动词，我们让你问路，并不是让你 “利用” 关系网，而是希望你加入关系网。  &lt;br /&gt;
  &lt;code&gt;关系网的存在就是为了支持该领域内的人完成工作，关系网也是进入隐形工作市场的唯一通路。&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;你寻找的是工作机会，而不是工作&lt;/strong&gt;  &lt;br /&gt;
当你转变心态，寻找工作机会，而不是工作时，就会变得更真诚、更有活力、更坚毅，也更开心  &lt;br /&gt;
原型设计谈话和原型设计体验最主要的内容就是：以开放的心态 和十足的好奇心面对一切可能性。&lt;/p&gt;
 &lt;h3&gt;九 主动选择幸福&lt;/h3&gt;
 &lt;p&gt;在人生设计中，要想让自己幸福，意味着你要选择幸福。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;选择四步骤&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image" src="https://ipfs.crossbell.io/ipfs/QmVMSTr2BSXWWBXRYkAgPJvZERCqbfWwK5cMsifBAbNWL3"&gt;&lt;/img&gt;  &lt;br /&gt;
步骤 3 的关键是通过运用多种知觉方式，做出精明的决定，不要单纯地依赖认知判断。  &lt;br /&gt;
  &lt;code&gt;要想让多种知觉方式共同发挥作用，需要培养并且完善你的情感/ 直觉/精神知觉方法和意识，最常见的方法是进行个人修行方面的练 习，如记日志、祈祷或者冥想，同时结合身体练习，如瑜伽、太极等&lt;/code&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;避免过度思虑，学会放手&lt;/strong&gt;&lt;/p&gt;
 &lt;h3&gt;十 你可以对失败免疫&lt;/h3&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;/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;br /&gt;
失败重构是一个过程，通过对原材料（失败）的转 化，达到质的飞跃（成功）。这个练习很简单，分为三步：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;1. 记录失败的经历。&lt;/li&gt;
  &lt;li&gt;2. 对失败进行分类。   &lt;br /&gt;
   &lt;code&gt;第一类失败是由低级错误导致的失败，即由你一般不会犯的错误所 导致的失败。&lt;/code&gt;   &lt;br /&gt;
   &lt;code&gt;第二类失败是由于自身弱点导致的失败。&lt;/code&gt;   &lt;br /&gt;
   &lt;code&gt;第三类失败是蕴含成长机会的失败，它是一种原本可以避免的失 败，或者是以后可以避免的失败。这类失败的原因是可辨认的，而且有 补救方法。我们重点关注的应该是这类失败，而不是其他两类失败，因 为对于前两类即使我们花费了很多时间，也不会有多少改进。&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;3. 鉴别出蕴含成长机会的失败。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="image" src="https://ipfs.crossbell.io/ipfs/QmdzWCy27bYKhwn6imfMkjGymBra7oH418xgFfpvRkX659"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;练习重新定义失败&lt;/strong&gt;  &lt;br /&gt;
1. 利用上面的表格，回顾过去一周（一个月或一年），然后记录你遭遇的失败。  &lt;br /&gt;
成长。  &lt;br /&gt;
2. 按照低级错误、弱点、成长机会的标准，对失败事件进行分类。  &lt;br /&gt;
3. 鉴别出蕴含成长机会的失败。  &lt;br /&gt;
4. 一个月做 1~2 次这个练习，养成习惯，从失败中吸取经验，获得成长。&lt;/p&gt;
 &lt;h3&gt;十一 创建团队&lt;/h3&gt;
 &lt;p&gt;  &lt;strong&gt;确定你的团队成员&lt;/strong&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;支持者。支持者是那些可靠的、值得你信赖的人，凡是关心你的人都可以 被称为支持者。&lt;/li&gt;
  &lt;li&gt;参与者。他们会积极参与你的人生设计，尤其和工作、娱乐、原型 设计等相关的事情。&lt;/li&gt;
  &lt;li&gt;亲友。亲友包括你的家庭成员和亲属，以及你最亲密的朋友。这些人最有可能直接受到你的人生设计的影响，不管他们是否参与你的人生设计，他们都是对你来说影响力最大的人。&lt;/li&gt;
  &lt;li&gt;团队。团队成员是一些能够和你分享人生设计细节的人，并且能够定期与你会面，持续关注你的人生设计的人。&lt;/li&gt;
  &lt;li&gt;一个健康的团队一定要在 2~6 个人之间，其中也包括你自己。这个 团队最佳的人数是 3~5 人。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;团队角色和规则&lt;/strong&gt;  &lt;br /&gt;
1. 尊重  &lt;br /&gt;
2. 保密  &lt;br /&gt;
3. 积极参与（不退缩）  &lt;br /&gt;
4. 生成性（建设性的，不质疑，不评判）&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;寻找人生导师&lt;/strong&gt;  &lt;br /&gt;
如果能够找到某个人，既可以给你有益的引 导，又能让你头脑清醒、心态稳定，那么你就拥有了一笔巨额财富 — 这就是导师的作用。&lt;/p&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;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>post</category>
      <guid isPermaLink="true">https://itindex.net/detail/62974-%E6%96%AF%E5%9D%A6%E7%A6%8F-%E5%A4%A7%E5%AD%A6-%E4%BA%BA%E7%94%9F</guid>
      <pubDate>Tue, 11 Feb 2025 23:00:23 CST</pubDate>
    </item>
    <item>
      <title>高性能高并发高可用三高系统架构设计看这篇绝对够了</title>
      <link>https://itindex.net/detail/62966-%E6%80%A7%E8%83%BD-%E5%B9%B6%E5%8F%91-%E4%B8%89%E9%AB%98</link>
      <description>&lt;p&gt;保证系统的可用性是系统建设中的重中之重，如果没有可用性，高性能和高并发也无从谈起，高可用的建设通常是通过    &lt;strong&gt;保护&lt;/strong&gt;系统和    &lt;strong&gt;冗余&lt;/strong&gt;的方法来进行容错保证系统的可用性。本篇主要从三个维度：应用层，存储层，部署层谈下可用性的建设。应用层的内容来自我的另一篇文章：万字长文浅谈系统稳定性建设。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;1、方法论&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/e74140d49e5a40f681501d3b68d02f90~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=xYlDqgQmsECSvNgNMJqv7KdloMk%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;1）应用层&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&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;限流一般是从服务提供者provider的视角提供的针对自我保护的能力，&lt;/strong&gt;对于流量负载超过我们系统的处理能力，限流策略可以防止我们的系统被激增的流量打垮。京东内部无论是同步交互的JSF, 还是异步交互的JMQ都提供了限流的能力，大家可以根据自己系统的情况进行设置；我们知道常见的限流算法包括：计数器算法，滑动时间窗口算法，漏斗算法，令牌桶算法，具体算法可以网上google下，下面是这些算法的优缺点对比。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;table width="auto"&gt;    &lt;tr&gt;      &lt;td colspan="1" rowspan="1"&gt; &lt;/td&gt;      &lt;td colspan="1" rowspan="1"&gt;优点&lt;/td&gt;      &lt;td colspan="1" rowspan="1"&gt;缺点&lt;/td&gt;&lt;/tr&gt;    &lt;tr&gt;      &lt;td colspan="1" rowspan="1"&gt;流量计数器算法&lt;/td&gt;      &lt;td colspan="1" rowspan="1"&gt;简单好理解&lt;/td&gt;      &lt;td colspan="1" rowspan="1"&gt;单位时间很难把控，不平滑&lt;/td&gt;&lt;/tr&gt;    &lt;tr&gt;      &lt;td colspan="1" rowspan="1"&gt;滑动时间窗口算法&lt;/td&gt;      &lt;td colspan="1" rowspan="1"&gt;时间好把控&lt;/td&gt;      &lt;td colspan="1" rowspan="1"&gt;1 超过窗口时间的流量就丢弃或降级 2 没有办法削峰填谷&lt;/td&gt;&lt;/tr&gt;    &lt;tr&gt;      &lt;td colspan="1" rowspan="1"&gt;漏桶算法&lt;/td&gt;      &lt;td colspan="1" rowspan="1"&gt;削峰填谷&lt;/td&gt;      &lt;td colspan="1" rowspan="1"&gt;1 漏桶大小的控制，太大给服务端造成压力，太小大量请求被丢弃 2 漏桶给下游发送请求的速率固定&lt;/td&gt;&lt;/tr&gt;    &lt;tr&gt;      &lt;td colspan="1" rowspan="1"&gt;令牌桶算法&lt;/td&gt;      &lt;td colspan="1" rowspan="1"&gt;1 削峰填谷 2 动态控制令牌桶的大小，从而控制向下游发送请求的速率&lt;/td&gt;      &lt;td colspan="1" rowspan="1"&gt;1 实现相对复杂 2 只能预先设计不适配突发&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;  &lt;p&gt;    &lt;strong&gt;②熔断降级&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;熔断和降级是两件事情，但是他们一般是结合在一起使用的。熔断是防止我们的系统被下游系统拖垮，比如下游系统接口性能严重变差，甚至下游系统挂了；这个时候会导致大量的线程堆积，不能释放占用的CPU，内存等资源，这种情况下不仅影响该接口的性能，还会影响其他接口的性能，严重的情况会将我们的系统拖垮，造成雪崩效应，通过打开熔断器，流量不再请求到有问题的系统，可以保护我们的系统不被拖垮。降级是一种有损操作，我们作为服务提供者，需要将这种损失尽可能降到最低，无论是返回友好的提示，还是返回可接受的降级数据。降级细分的话又分为人工降级，自动降级。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;人工降级：&lt;/strong&gt;人工降级一般采用降级开关来控制，公司内部一般采用配置中心Ducc来做开关降级，开关的修改也是线上操作，这块也需要做好监控；&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;自动降级：&lt;/strong&gt;自动降级是采用自动化的中间件例如Hystrix，公司的小盾龙等；如果采用自动降级的话；我们必须要对降级的条件非常的明确，比如失败的调用次数等。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&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;京东物流现有的微服务架构下，服务之间都是通过JSF网络交互进行同步通信，    &lt;strong&gt;我们探测下游依赖服务是否可用的最快捷的方式是设置超时时间。&lt;/strong&gt;超时的设置可以让系统快速失败，进行自我保护，避免无限等待下游依赖系统，将系统的线程耗尽，系统拖垮；&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;超时时间如何设置也是一门学问，如何设置一个合理的超时时间也是一个逐步迭代的过程，比如下游新开发的接口，一般会基于压测提供一个TP99的耗时，我们会基于此配置超时时间；老接口的话，会基于线上的TP99耗时来配置超时时间。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;超时时间在设置的时候需要遵循漏斗原则，&lt;/strong&gt;从上游系统到下游系统设置的超时时间要逐渐减少，如下图所示。为什么要满足漏斗原则，假设不满足漏斗原则，比如服务A调取服务B的超时时间设置成500ms，而服务B调取服务C的超时时间设置成800ms，这个时候回导致服务A调取服务B大量的超时从而导致可用率降低，而此时服务B从自身角度看是可用的。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/7f82b9c460a64d549f2effa1b0bc662c~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=bvf7e9rTMA0kVCkOfAkrwGLKSlY%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;④重试&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;分布式系统中性能的影响主要是通信，无论是在分布式系统中还是垮团队沟通，communication是最昂贵的；比如我们研发都知道需求的交付有一半以上甚至更多的时间花在跨团队的沟通上，真正写代码的时间是很少的；分布式系统中我们查看调用链路，其实我们系统本身计算的耗时是很少的，主要来自于外部系统的网络交互，无论是下游的业务系统，还是中间件：Mysql, redis, es等等；所以在和外部系统的一次请求交互中，我们系统是希望尽最大努力得到想要的结果，但往往事与愿违，由于不可靠网络的原因，我们在和下游系统交互时，都会配置超时重试次数，希望在可接受的SLA范围内一次请求拿到结果，但重试不是无限的重试，我们一般都是配置重试次数的限制，偶尔抖动的重试可以提高我们系统的可用率，如果下游服务故障挂掉，重试反而会增加下游系统的负载，从而增加故障的严重程度。在一次请求调用中，我们要知道对外提供的API，后面是有多少个service在提供服务，如果调用链路比较长，服务之间rpc交互都设置了重试次数，这个时候我们需要警惕重试风暴。如下图service D 出现问题，重试风暴会加重service D的故障严重程度。对于API的重试，我们还要区分该接口是读接口还是写接口，如果是读接口重试一般没什么影响，写接口重试一定要做好接口的幂等性。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/16cc615990ca4ffab1bdfc7dcbe652e3~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=gdz6O6psCklZifFlqVfs%2BWkf4QM%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;⑤隔离&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;隔离是将故障爆炸半径最小化的有效手段，我们通过不同层面的隔离来控制影响范围，保证系统的高可用：&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;系统建设层面隔离&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;我们知道系统的分类可以分为：在线的系统，离线系统（批处理系统），近实时系统（流处理系统），如下是这些系统的定义：&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;在线系统：服务端等待请求的到达，接收到请求后，服务尽可能快的处理，然后返回给客户端一个响应，响应时间通常是在线服务性能的主要衡量指标。我们生活中在手机使用的APP大部分都是在线系统；&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;离线系统：或称批处理系统，接收大量的输入数据，运行一个作业来处理数据，并产出输出数据，作业往往需要定时，定期运行一段时间，比如从几分钟到几天，所以用户通常不会等待作业完成，吞吐量是离线系统的主要衡量指标。例如我们看到的报表数据：日订单量，月订单量，日活跃用户数，月活跃用户数都是批处理系统运算一段时间得到的；&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;近实时系统：或者称流处理系统，其介于在线系统和离线系统之间，流处理系统一般会有触发源：用户的行为操作，数据库的写操作，传感器等，触发源作为消息会通过消息代理中间件：JMQ, KAFKA等进行传递，消费者消费到消息后再做其他的操作，例如构建缓存，索引，通知用户等；&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;以上三种系统是需要进行隔离建设的，因为他们的衡量指标及对资源的使用情况完全不一样的，比如我们小组会将在线系统作为一个服务单独部署：jdl-uep-main, 离线系统和近实时系统作为一个服务单独部署：jdl-uep-worker；&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;环境的隔离&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;从研发到上线阶段我们会使用不同的环境，比如业界常见的环境分为：开发，测试，预发和线上环境；研发人员在开发环境进行开发和联调，测试人员在测试环境进行测试，运营和产品在预发环境进行UAT，最终交付的产品部署到线上环境提供给用户使用。在研发流程中，我们部署时要遵循从应用层到中间件层再到存储层，都要在一个环境，严禁垮环境的调用，比如测试环境调用线上，预发环境调用线上等。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/2a480b2757c34146b797548bef346f92~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=7wO7kJSDI9DJ5ezgyVhgJvUMBu4%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;数据隔离&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;随着业务的发展，我们对外提供的服务往往会支撑多业务，多租户，所以这个时候我们会按照业务进行数据隔离；比如我们组产生的物流订单数据业务方就包含京东零售，其他电商平台，ISV等，为了避免彼此的影响我们需要在存储层对数据进行隔离，数据的隔离可以按照不同粒度，第一种是通过租户id字段进行区分，所有的数据存储在一张表中，另外一个是库粒度的区分，不同的租户单独分配对应的数据库。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/69fbd41d508d4f03830bb525d01ba2e2~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=sDDVSdZN%2B7tONzpAF1KaUBDjVmU%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/5856c1bb660f4d6590a8745a4a18b467~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=INIIVVyTaVEQJirbJykdm8xxkwM%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;数据的隔离除了按照业务进行隔离外，还有    &lt;strong&gt;按照环境进行隔离&lt;/strong&gt;的，比如我们的数据库分为测试库，预发库，线上库，全链路压测时，我们为了模拟线上的环境，同时避免污染线上的数据，往往会创建影子库，影子表等。    &lt;strong&gt;根据数据的访问频次进行隔离&lt;/strong&gt;，我们将经常访问的数据称为热数据，不经常访问的数据称为冷数据；将经常访问的数据缓存到缓存，提高系统的性能。不经常访问的数据持久化到数据库或者将不使用的数据结转归档到OSS，避免大库大表。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;核心/非核心流程隔离&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;我们知道应用是分级的，京东内部针对应用的重要程度会将应用分为0，1，2，3级应用。业务的流程也分为黄金流程和非黄金流程。在业务流程中，针对不同级别的应用交互，需要将核心和非核心的流程进行隔离。例如在交易业务过程中，会涉及到订单系统，支付系统，通知系统，那这个过程中核心系统是订单系统和支付系统，而通知相对来说重要性不是那么高，所以我们会投入更多的资源到订单系统和支付系统，优先保证这两个系统的稳定性，通知系统可以采用异步的方式与其他两个系统解耦隔离，避免对其他另外两个系统的影响。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/da297427065540e2976247c539b1b7a3~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=t3yIw3NeMNtvP%2FUTEhahXeP809A%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;读写隔离&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;应用层面，领域驱动设计（DDD）中最著名的CQRS（Command Query Responsibility Segregation）将写服务和读服务进行隔离。写服务主要处理来自客户端的command写命令，而读服务处理来自客户端的query读请求，这样从应用层面进行读写隔离，不仅可以提高系统的可扩展性，同时也会提高系统的可维护性，应用层面我们都采用微服务架构，应用层都是无状态服务，可以扩容加机器随意扩展，存储层需要持久化，扩展就比较费劲。除了应用层面的CQRS，在存储层面，我们也会进行读写隔离，例如数据库都会采用一主多从的架构，读请求可以路由到从库从而分担主库的压力，提高系统的性能和吞吐量。所以应用层面通过读写隔离主要解决可扩展问题，存储层面主要解决性能和吞吐量的问题。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/64ce1fa605244b13b62ea2dbde31abad~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=vaFFOr2LcPLR7nsiceAm3RCETGM%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;线程池隔离&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;线程是昂贵的资源，为了提高线程的使用效率，复用线程，避免创建和销毁的消耗，我们采用了池化技术，线程池，但是在使用线程的过程中，我们也做好线程池的隔离，避免多个API接口复用同一个线程。&lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/a3b8ed7b9fbe4333ac432756e53096c8~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=AgWpctDlLbMmneAoSh6pfHQZ5tc%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;⑥兼容&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;我们在对老系统，老功能进行重构迭代的时候，一定要做好兼容，否则上线后会出现重大的线上问题，公司内外有大量因为没有做好兼容性，而导致资损的情况。兼容分为：向前兼容性和向后兼容性，需要好好的区分他们，如下是他们的定义:&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;向前兼容性：向前兼容性指的是旧版本的软件或硬件能够与将来推出的新版本兼容的特性，简而言之旧版本软件或系统兼容新的数据和流量。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;向后兼容性：向后兼容性则是指新版本的软件或硬件能够与之前版本的系统或组件兼容的特性，简而言之新版本软件或系统兼容老的数据和流量。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;根据新老系统和新老数据我们可以将系统划分为四个象限：    &lt;strong&gt;第一象限：&lt;/strong&gt;新系统和新数据是我们系统改造上线后的状态，    &lt;strong&gt;第三象限：&lt;/strong&gt;老系统和老数据是我们系统改造上线前的状态，第一象限和第三象限的问题我们在研发和测试阶段一般都能发现排除掉，线上故障的高发期往往出现在第二和第四象限，    &lt;strong&gt;第二象限&lt;/strong&gt;是因为没有做好向前兼容性，例如上线过程中，发现问题进行了代码回滚，但是在上线过程中产生了新数据，回滚后的老系统不能处理上线过程中新产生的数据，导致线上故障。    &lt;strong&gt;第四象限&lt;/strong&gt;是因为没有做好向后兼容性，上线后新系统影响了老流程。针对第二象限的问题，我们可以构造新的数据去验证老的系统，针对第四象限的问题，我们可以通过流量的录制回放解决，录制线上的老流量，对新功能进行验证。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/7878e4df783642cfb65b9653f91450b8~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=jwy%2FQig5%2FzU2B2efY8z6g0Twt8s%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;2）存储层&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;存储层主要通过    &lt;strong&gt;复制和分片&lt;/strong&gt;来保证存储层的高可用，    &lt;strong&gt;复制主要是通过副本（主从节点，主从副本）来保证高可用，分片是将数据分散到不同的节点上来保证高可用&lt;/strong&gt;（鸡蛋不要放在同一个篮子中）。复制和分片在保证高可用的情况下，其实也提高了系统的高性能和高并发，复制和分片的思想在Mysql，Redis，ElasticSearch, kafka中都进行了采用。&lt;/p&gt;  &lt;p&gt; &lt;/p&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;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;主从复制：&lt;/strong&gt;客户端将所有写入操作发送到单个节点（主库），该节点将数据更改事件流发送到其他副本（从库）。读取可以在任何副本上执行，但从库的读取结果可能是陈旧的。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;多主复制：&lt;/strong&gt;客户端将每个写入发送到几个主库节点之一，其中任何一个主库都可以接受写入。主库将数据更改事件流发送给彼此以及任何从库节点。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;无主复制：&lt;/strong&gt;客户端将每个写入发送到几个节点，并从多个节点并行读取，以检测和纠正具有陈旧数据的节点。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&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;/p&gt;  &lt;p&gt; &lt;/p&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;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;③Redis 的复制和分片&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;redis cluster集群中，我们会划分16384个槽，key 通过散列哈希算法会映射到相应的槽中，这些槽分配到不同的分片上，每个分片有主节点和从节点，主节点对外提供读写服务，从节点对外提供读服务。当某个分片的主节点挂掉，其他分片的主节点会从挂掉分片的从节点选择一个作为主节点继续对外提供服务。整体的架构如下图所示。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/82538735e2ba4340b8f5a8185249ed67~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=e0w2v6jIy%2FG9ZkUU0F1h9d8lsSo%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;④ES索引的复制和分片&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;我们在创建ES索引时，会指定分片的数量和副本的数量，分片的数量确定后是不允许修改的，副本的数量允许修改，分片的数量一般和数据节点的数量保持一致，这样能将索引的数据分配到每个数据节点上，每个数据节点都存储索引的部分数据，Primary分片可以对外提供读写服务，Replica分片对外提供读服务的同时作为备份节点保证可用性，ES索引的不同分片在不同数据节点的分布如下图所示。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/b1e201f9f38c42f68e2159571bc95c77~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=%2FHgd3jZA2IEvM8JM%2BBo2cauID%2FQ%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;⑤Kafka topic的复制和分区&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;kafka的topic为了提高可用性及高吞吐，引入了topic的分区，每个分区为了提高可用性，分区分为Leader partition 和 Follower partition，Leader partition对外提供读写服务，Follower partition作为灾备提高可用性，整体的架构如下图。&lt;/p&gt;  &lt;p&gt;图片&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;3）部署层&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&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;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/518a7095ae754ec0a5c0511cea0b4452~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=e%2B%2F9cDob0V8dUuPKiTQwsOS5xPo%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;②我们部署架构现状&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;目前我们的应用都是采用多机房多分组Docker容器化部署，会根据业务方的重要程度及流量大小设置不同的别名，隔离到不同的分组中对外提供服务。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;应用容器机房为：中云信，有孚，廊坊，宿迁等；&lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;数据库Mysql双机房部署：中云信，有孚；&lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;缓存Redis双机房部署：中云信，有孚；&lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;ES单机房部署：有孚。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/c51897177fb44afcbb5da521c696f125~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1737512013&amp;x-signature=a9f0w%2F2HzOb6E6rJxliQVOm%2F6Gk%3D"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62966-%E6%80%A7%E8%83%BD-%E5%B9%B6%E5%8F%91-%E4%B8%89%E9%AB%98</guid>
      <pubDate>Thu, 16 Jan 2025 14:17:13 CST</pubDate>
    </item>
    <item>
      <title>在自驾旅行中如何设计分镜</title>
      <link>https://itindex.net/detail/62965-%E6%97%85%E8%A1%8C-%E8%AE%BE%E8%AE%A1-%E5%88%86%E9%95%9C</link>
      <description>&lt;p&gt;在自驾旅行中设计分镜，主要是为了更好地记录和表现旅途中的独特景观、人物和情感。分镜设计可以帮助你规划拍摄的内容，确保每个重要瞬间都能够被有效捕捉。以下是一些设计分镜的建议：&lt;/p&gt; &lt;h3&gt;1.   &lt;strong&gt;确定主题和故事情节&lt;/strong&gt;&lt;/h3&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;/ul&gt; &lt;h3&gt;2.   &lt;strong&gt;选择拍摄视角和镜头类型&lt;/strong&gt;&lt;/h3&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;大景别（Wide shot）&lt;/strong&gt;：记录辽阔的自然风光、城市全景等。&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;中景别（Medium shot）&lt;/strong&gt;：适合拍摄车内或车外人物的互动，展示旅行中的细节。&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;特写镜头（Close-up）&lt;/strong&gt;：捕捉人物的面部表情或一些重要细节，如车窗外的景象、旅行小物件等。&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;运动镜头（Tracking shot）&lt;/strong&gt;：通过车窗或车内拍摄沿途风景，或用车载固定设备记录车速、转弯等动态场景。&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;鸟瞰镜头（Aerial shot）&lt;/strong&gt;：如果条件允许，可以利用无人机或高楼拍摄开阔的景观和行车路线。&lt;/li&gt;&lt;/ul&gt; &lt;h3&gt;3.   &lt;strong&gt;规划拍摄顺序&lt;/strong&gt;&lt;/h3&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;/ul&gt; &lt;h3&gt;4.   &lt;strong&gt;加入人物和互动&lt;/strong&gt;&lt;/h3&gt; &lt;ul&gt;  &lt;li&gt;如果有同行的伙伴，可以设计一些他们的互动镜头。比如车里大家的对话、笑声、互动等，能够增加情感色彩。&lt;/li&gt;  &lt;li&gt;同时也可以拍摄你与自然景观的互动，比如下车拍照、徒步旅行等。&lt;/li&gt;&lt;/ul&gt; &lt;h3&gt;5.   &lt;strong&gt;考虑拍摄的光线和时间&lt;/strong&gt;&lt;/h3&gt; &lt;ul&gt;  &lt;li&gt;   &lt;strong&gt;黄金时段（Golden Hour）&lt;/strong&gt;：利用清晨和傍晚的柔和光线拍摄，能让画面更有质感。&lt;/li&gt;  &lt;li&gt;   &lt;strong&gt;夜间拍摄&lt;/strong&gt;：如果计划夜晚开车，可以考虑拍摄车内灯光，或者车灯在夜晚的照射效果。&lt;/li&gt;&lt;/ul&gt; &lt;h3&gt;6.   &lt;strong&gt;留意旅行中的细节&lt;/strong&gt;&lt;/h3&gt; &lt;ul&gt;  &lt;li&gt;旅行中的许多小细节也很重要，如加油站、停车、地图、沿途的标志性建筑等，能够丰富故事的层次感。&lt;/li&gt;&lt;/ul&gt; &lt;h3&gt;7.   &lt;strong&gt;使用设备&lt;/strong&gt;&lt;/h3&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;/ul&gt; &lt;h3&gt;示例分镜脚本：&lt;/h3&gt; &lt;ol&gt;  &lt;li&gt;   &lt;p&gt;    &lt;strong&gt;开场镜头&lt;/strong&gt;&lt;/p&gt;   &lt;ul&gt;    &lt;li&gt;     &lt;strong&gt;镜头1&lt;/strong&gt;：清晨，车主在家中收拾行李，放进车后备箱，手拿地图或导航。&lt;/li&gt;    &lt;li&gt;     &lt;strong&gt;镜头2&lt;/strong&gt;：车启动的镜头，外景是车在城市街道上行驶。&lt;/li&gt;    &lt;li&gt;     &lt;strong&gt;镜头3&lt;/strong&gt;：车内镜头，几个人聊着旅行的计划，背景音乐轻松。&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;strong&gt;旅途中的镜头&lt;/strong&gt;&lt;/p&gt;   &lt;ul&gt;    &lt;li&gt;     &lt;strong&gt;镜头4&lt;/strong&gt;：车窗外，风景变换，快速掠过草地、山脉、湖泊。&lt;/li&gt;    &lt;li&gt;     &lt;strong&gt;镜头5&lt;/strong&gt;：车内，一人透过窗外观看风景，另一人指着前方讨论一个地标。&lt;/li&gt;    &lt;li&gt;     &lt;strong&gt;镜头6&lt;/strong&gt;：停在一个有趣的地方，大家下车拍照，特写拍摄手持相机的动作。&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;strong&gt;到达目的地&lt;/strong&gt;&lt;/p&gt;   &lt;ul&gt;    &lt;li&gt;     &lt;strong&gt;镜头7&lt;/strong&gt;：车驶入目的地，一座小镇的全景镜头。&lt;/li&gt;    &lt;li&gt;     &lt;strong&gt;镜头8&lt;/strong&gt;：人们在镇上漫步，享受当地美食或与当地人互动。&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;strong&gt;结束镜头&lt;/strong&gt;&lt;/p&gt;   &lt;ul&gt;    &lt;li&gt;     &lt;strong&gt;镜头9&lt;/strong&gt;：夜晚，车行驶在回程的路上，灯光照亮远方。&lt;/li&gt;    &lt;li&gt;     &lt;strong&gt;镜头10&lt;/strong&gt;：车内，大家谈笑风生，窗外是星空。&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ol&gt; &lt;p&gt;通过这些镜头的设计，你的自驾旅行视频不仅能展现沿途的美景，还能传达旅行中的情感和故事。&lt;/p&gt;
     
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62965-%E6%97%85%E8%A1%8C-%E8%AE%BE%E8%AE%A1-%E5%88%86%E9%95%9C</guid>
      <pubDate>Wed, 15 Jan 2025 16:42:12 CST</pubDate>
    </item>
    <item>
      <title>分镜从理论到实践，教你如何做好分镜设计</title>
      <link>https://itindex.net/detail/62964-%E5%88%86%E9%95%9C-%E7%90%86%E8%AE%BA-%E5%AE%9E%E8%B7%B5</link>
      <description>&lt;p&gt;分镜设计，是备考国美影视与动画类相关专业必考的科目。很多同学对分镜设计仅仅只是有一个模糊的概念，也不清楚如何下手备考。&lt;/p&gt; &lt;p&gt;本期文章为大家带来分镜设计的干货，教你从理论到实践。&lt;/p&gt; &lt;p&gt;▌一、要点目录&lt;/p&gt; &lt;p&gt;①. 分镜的概念和意义&lt;/p&gt; &lt;p&gt;②. 分镜设计涵盖的内容&lt;/p&gt; &lt;p&gt;③. 优秀电影/动画分镜案例详解&lt;/p&gt; &lt;p&gt;④. 备考分镜指南&lt;/p&gt; &lt;p&gt;▌二、分镜的概念和意义&lt;/p&gt; &lt;p&gt;1. 分镜的概念&lt;/p&gt; &lt;p&gt;分镜，英文名称为Storyboard，因此又被称之为故事板。&lt;/p&gt; &lt;p&gt;分镜头脚本是指电影、动画、电视剧、广告等各种影像媒体，在实际拍摄或绘制之前，以图表的方式来说明影像的构成，将连续画面以一次运镜为单位作分解，并且标注运镜方式、时间长度、对白、特效等。根据媒体不同划分成不同分镜。常见的分镜类型有两种：影片分镜和漫画分镜。&lt;/p&gt; &lt;p&gt;2. 分镜的意义&lt;/p&gt; &lt;p&gt;在影视创作的流程中，首先需要有故事大纲，再根据大纲创作完整的剧本。剧本中的每一个场景先需要创作对应的分镜头脚本，然后在依据分镜头脚本推进到拍摄和后期剪辑。&lt;/p&gt; &lt;p&gt;所以分镜头脚本是导演对影片全面设计和构思的蓝图，是摄制组在实际开展拍摄工作时的重要参考，能够保证设置工作按照计划进行。分镜头脚本也是使文字剧本转变为电影画面的关键一环，涉及到电影的前期、中期、后期各个环节。&lt;/p&gt; &lt;p&gt;▌三、分镜设计涵盖的内容&lt;/p&gt; &lt;p&gt;分镜头脚本中的内容包括镜头号、景别、角度、摄法（运动）、画面、台词、音乐、音响效果、镜头长度等项目。&lt;/p&gt; &lt;p&gt;➊. 镜头号：通过序号来体现镜头的组接顺序。&lt;/p&gt; &lt;p&gt;➋. 景别：一般分为全景、远景、中景、近镜和特写，决定了被摄主体在画面中呈现的范围。&lt;/p&gt; &lt;p&gt;➌. 角度：拍摄主体的角度，可分为垂直（平仰俯）和水平（正侧背）两种。变换视角是一种突出拍摄对象、刻画人物情感和思想的拍摄手段。&lt;/p&gt; &lt;p&gt;➍. 运动：主要指镜头在拍摄中的运动。包括推、拉、摇、移、跟、升、降、旋转和晃动等不同形式的运动。运动摄影是电影区别于其他造型艺术的独特表现手段，是电影语言的独特表达方式，也是电影作为艺术的重要标志之一。&lt;/p&gt; &lt;p&gt;➎. 画面：分镜头中需要包含的画面，包括主要场地画面、人物等。&lt;/p&gt; &lt;p&gt;➏. 台词：分镜头内人物的台词。&lt;/p&gt; &lt;p&gt;➐. 声音：分镜头内的重要声音，包括人声、音乐、音响等。&lt;/p&gt; &lt;p&gt;➑. 镜头长度：镜头的时间长度。&lt;/p&gt; &lt;p&gt;【分镜头表格参考】&lt;/p&gt; &lt;p&gt;  &lt;img height="270" src="https://p2.itc.cn/q_70/images03/20221121/600672158bb64ab0b77282e8b7d4b58b.png" width="640"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;img height="156" src="https://p7.itc.cn/q_70/images03/20221121/caee69d30df541eeb1e3a97ea00a2518.png" width="640"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;▌四、优秀分镜案例详解&lt;/p&gt; &lt;p&gt;曹保平《狗十三》“宴会片段”&lt;/p&gt; &lt;p&gt;  &lt;img height="557" src="https://p3.itc.cn/q_70/images03/20221121/01a1cfcbc7844c3f911c5846593ddd23.png" width="640"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;▌五、备考分镜指南&lt;/p&gt; &lt;p&gt;➪ 1. 第一阶段：学习电影理论知识&lt;/p&gt; &lt;p&gt;在备考前期，我们要先充分了解电影理论，明白电影的构架。&lt;/p&gt; &lt;p&gt;推荐给大家《电影概论》这本书。它是一本全面系统的参考书，为大家提供电影纲要式的认知轮廓。本书建构出涵盖创作、技术、产业、历史、理论和批评的电影整体构架。&lt;/p&gt; &lt;p&gt;《电影概论》&lt;/p&gt; &lt;p&gt;  &lt;img height="875" src="https://p5.itc.cn/q_70/images03/20221121/01c8f60da5ce4c3d9a4b0dcd636942c5.png" width="640"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;本书的各章内容正是学习电影要了解的各部分概要：&lt;/p&gt; &lt;p&gt;媒介特质（构成电影的媒介是什么）&lt;/p&gt; &lt;p&gt;创作流程（电影创作要经历的流程步骤）&lt;/p&gt; &lt;p&gt;生产机制（融资、制片、发行、放映）&lt;/p&gt; &lt;p&gt;历史过程（以史为鉴，学好电影史并促进电影发展）&lt;/p&gt; &lt;p&gt;国族特色（各国电影介绍）&lt;/p&gt; &lt;p&gt;批评框架（电影理论批评）&lt;/p&gt; &lt;p&gt;➪ 2. 第二阶段：学习电影视听语言&lt;/p&gt; &lt;p&gt;推荐给大家《视听语言》这本书，它从影像、声音、剪辑三个角度研究和分析视听语言，从“单个镜头”到“镜头与镜头的组合”，清晰地呈现视听语言从元素到篇章的过程，并以经典影片为例，详细讲解视听语言的分析和解读方法。&lt;/p&gt; &lt;p&gt;《视听语言》&lt;/p&gt; &lt;p&gt;  &lt;img height="903" src="https://p3.itc.cn/q_70/images03/20221121/26f614ea66d24cfe9c13877eb5d881e5.png" width="640"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;  &lt;img height="628" src="https://p5.itc.cn/q_70/images03/20221121/3995d0699f5f4cfa89a7a3677e04b8bd.png" width="640"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;（《视听语言》主要内容）&lt;/p&gt; &lt;p&gt;➪ 3．第三阶段：赏析经典、拉片临摹&lt;/p&gt; &lt;p&gt;基础知识打好后，就开始进行上手实操了。在前期我们还是要多涉猎优秀的电影，增加阅片量，然后对其中的优秀片段进行镜头的拉片和临摹。&lt;/p&gt; &lt;p&gt;▼ 拉片临摹主要分为几个步骤：&lt;/p&gt; &lt;p&gt;①、理解整体片段内容，拆分镜头，细致理解每个镜头的内容。&lt;/p&gt; &lt;p&gt;②、逐个分析镜头，把每个镜头的内容按照我们上文的分镜头脚本表格来整理出来，即每个镜头的景别、角度、运动、画面、声音、时长等。这样可通过分镜拉片转化为文本的形式去逐个理解镜头设置的含义，并熟悉分镜写作的形式。&lt;/p&gt; &lt;p&gt;③、深入思考，学习每个镜头中导演所用的拍摄手法和含义。比如在什么情况下用近镜、什么情况下用特写、这个镜头中的角度选择有什么用处、这个镜头中的镜头运动对于突出人物有什么作用等等。&lt;/p&gt; &lt;p&gt;④、临摹借鉴。开始针对经典片段进行临摹，一边临摹一边可以积累一些优秀的场景布置、人物对话等等，逐步扩充自己的创作素材库。&lt;/p&gt; &lt;p&gt;⑤、反复深化修改。完成后的分镜设计可以拿给老师进行批改点评，寻求改进方案。在征艺的分镜设计课程中，老师会一对一的针对每个学员的问题做出辅导，深化打磨方案。&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62964-%E5%88%86%E9%95%9C-%E7%90%86%E8%AE%BA-%E5%AE%9E%E8%B7%B5</guid>
      <pubDate>Tue, 14 Jan 2025 10:59:19 CST</pubDate>
    </item>
    <item>
      <title>异地多活架构设计看这篇就够了</title>
      <link>https://itindex.net/detail/62963-%E6%9E%B6%E6%9E%84-%E8%AE%BE%E8%AE%A1</link>
      <description>&lt;p&gt;异地多活是分布式系统架构设计的一座高峰，当业务系统走到需要考虑异地多活这一步，其体量和复杂度都会达到很高的水准。接入层、逻辑层、数据层的三层架构，基本上是每个业务都会拥有的基础架构形态，而三层架构的关键在于数据层，本文将从数据层切入探讨异地多活对于基础架构设计的影响。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;一、关于基础架构&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;信息技术的发展，渗透到人们各类活动的方方面面，应对的问题五花八门，纷繁错杂，催生了面向各种业务而非常复杂的软件系统。架构的核心目的就在于解决软件系统的复杂性问题，在互联网分布式系统下的大体量的业务，其复杂性尤其高，主要来自下面几个方面：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;高可用，分布式系统中节点众多，引发故障不可避免，如何减少故障的影响，尽快从故障中恢复，就是高可用设计的关键；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;高性能，大体量业务的海量请求，需要软件系统能够应对大并发量能力，具备强大的吞吐量，而且要有更短的响应时延；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;高扩展，功能迭代、请求模型、外部环境都是变化不定的，软件系统需要针对这种种变化，作出良好设计，以便灵活应对；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;低成本，软件系统往往是一种商业行为，需要关注商业价值，其构建要衡量投入产出比，最小化成本实现最大化商业价值；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;安全性，软件系统安全性要求，需防范数据泄露、保护用户隐私、防止非法访问及操作，确保系统稳定可靠，维护用户权益；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;多功能，业务是多变的，架构设计的前瞻性归根到底是有局限性的，未知的未知对架构的破坏性巨大，是复杂性的根本所在。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;架构是一个庞大的话题，设计原则、抽象方法、业务解耦、领域模型、模块划分等等，每一个方面都是大有文章。不过，一般来说，不管多么复杂的软件系统，都可以抽象为“基于数据的一系列处理逻辑组合，供目标用户接入使用的系统”，接入、逻辑、数据，这就是本文讨论的“基础架构”，如下图所示。基础架构着重关注高可用、高性能、高扩展的要求，本文将从后台的视角展开，看看这几个要素对于基础架构的设计影响。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-axegupay5k/3aaa85cab2484ed383431f0b3fbf5ebe~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=r0dRID8Yo7bztFS4tKI7OWt8x94%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;二、关于异地多活&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;需要考虑异地多活的业务，大概率是要把高可用当作核心目标，高可用重点关注的是软件系统面对故障的应对方案。互联网分布式系统的任何组成部分，都不是百分百绝对可靠的，总是会有发生故障的可能，要保障系统的可用性，就需要针对故障做容灾设计。容灾的本质，在于提供冗余以避免单点故障问题，当系统的某个组成部分发生故障时，可以由冗余部分接管服务使服务整体不受（或少受）影响。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;在业务发展的不同阶段，业务体量和规模决定其对于灾难的接受程度是不同的，容灾要应对的单点故障类型也不一样：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;单机器，业务起步阶段，体量非常小，单机部署就能够支撑，这时候面临的单机故障可能会是磁盘损坏、操作系统异常、数据误删等，为了应对这种故障，避免数据丢失，需要做一些数据备份，搭建主从；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;单机房，业务规模增长，体量比较大，需要用到相当多的机器，这时候有了更高的追求，在部署的时候，会将机器部署到不同的机房，以规避单个机房故障带来的影响；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;单城市，业务持续增长，体量非常大，同城市内的多个机房已经不能满足业务的容灾需要，如果发生城市级别的灾害，例如台风、地震、洪水等灾害，会使城市成为整个服务的单点。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;解决城市级别的单点故障，也就是本文题目中的“异地多活”了。城市单点问题，和单机器、单机房的单点问题，有着巨大的不同。城市在台风、地震、洪水等极端剧烈灾害时成为单点，而这些灾害往往影响大片区域，该区域内的多个城市会一起受到牵连。所以，要解决城市单点问题，就需要将冗余做到距离较远的另一个区域，例如同属于珠三角城市圈的广州深圳，同属于京津唐城市圈的北京天津，同属于长三角城市圈的上海杭州，聚集在一个城市圈内的城市，往往共享了很多基础设施，这样的距离不能满足容灾的需要。要达到异地多活的容灾目标，基本都需要将服务分别部署在千里之外，例如深圳上海分布，北京上海分布等。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;三、写时延是关键&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;1、核心在于数据层的写操作&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;在基础架构中，一般来说，逻辑层负责计算，是无状态的，可以做到无缝切换接管。逻辑层服务的根本是对数据的读取、处理、写入，数据层的故障，涉及到数据的同步、搬迁、恢复，要保证其完整性和一致性才可以切换投入使用，所以，基础架构的容灾关键在于数据层。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;数据层的操作涉及读和写，因为读操作不涉及到数据状态的变更，可以通过副本的方式方便扩展，而写操作为了保证写入数据在多份冗余之间的完整性和一致性，需要做数据复制，所以，数据层的关键，在于写请求的处理上。&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;2、写时延在跨城时发生质变&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;跨城的写时延发生质变是因为，在做跨城级别之前的容灾时，基本上所有业务对于时延都是能接受的，数据的复制直接采用同步的方式即可。在做跨城的时候，业务是否能接受更高的时延，就需要慎重斟酌了，而这也将影响具体的应对方案。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;更长的距离意味着更长的时延，通过 ping 工具，测得的时延情况大致是：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;同机房内的往返耗时大约 0.5ms 以内；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;同城跨机房往返耗时大约在 3ms 以内；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;千里之外的深圳上海跨城市耗时大约在 30ms 以内，北京上海，深圳天津的耗时会更长一些。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/178d9e0a29804184ade29c949e5aadf9~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=3gPPLiCx1QaQeSYTpzwsXBnv%2B5Y%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;当时延达到 30ms 的级别，业务可用性面临另一个层面的考验，业务是否能接受数据写入的跨城耗时，这里不单单是 30ms 的问题：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;跨城容灾的场景，涉及到数据写入和副本复制，一次写请求将产生两倍延时，即 60ms；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;业务写请求是否要串行发起 n次写请求，即 60ms 的 n倍；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;需要做一些前瞻性设计，当前是能接受 60ms 的 n倍，后续的 n 会不会扩大，会不会扩大到不能接受的地步，在后续的设计中是否能贯彻这一依赖，都需要重点关注；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;跨城情况下，网络状况可能会差一些，例如容易产生抖动，这个倒不是大问题，一般来说有能力搭建跨城网络的公司，也有能力保障网络的稳定性，在测试的过程中也发现跨城的耗时挺稳定的，在长达近4个半小时的测试中，最大抖动不超过8毫秒，而且最大耗时在30毫秒以内。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;如果业务能够接受跨城写时延，那么问题就退化到同城容灾，直接采用跨城同步复制即可。如果不能够接受写入延时，就不能走长距离跨城的同步复制，必须找退而求其次的方案，下面聊聊两种考虑方向。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/dd6b29719371402c9eddca0f1867efb2~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=l%2BJaOIq7DOf%2BQ2Ik83Kv4%2Bdc7IM%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;3、同步复制缩短距离降目标&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;缩短距离，不做千里之外，而是选择做距离较近的跨城，例如做广州-深圳、上海-杭州、北京-天津的跨城，距离在100-200公里，时延在5-7ms，这样依然可以用同步复制的方式，但是，如前面提到的，这种方式是达不到跨城异地多活的真实目标的。&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;4、异步复制就近分片做有损&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;不做同步复制，首先要做的就是将数据根据地理位置做分片，异地多活不能接受延时的情况下，不同业务的分片规则可能会有差异，例如某多、某东、某宝的电商业务，和饿某某和某团的外卖业务的分片规则肯定是不同的，但基本上都是基于用户地理位置来做的：让离哪个城市近的用户数据尽量放到对应城市去。如下图所示，针对用户做了就近分片后，数据写入不需要做跨城同步复制，写入主写点后，直接对外返回成功，而不需要等数据同步到异地。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/0c54bb3c5042405b87cac766bc4be191~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=%2FxZZpr9PSO8%2Fr2%2FMUVfFlYnY7MI%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;采用异步复制，这样必然会出现灾难发生时数据没有及时复制到异地的情况，如下图所示：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/79918215e11d41eebb86080dadf51387~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=eIuvrdpZO01pVuRDJ%2BxlzGA%2B9z8%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;对于数据一致性要求不高的业务，例如微博、视频等，可以接受数据重复的情况，自由切换即可，结合一些业务层的去重逻辑，例如结合灾难情况，将灾难发生期间的重复数据做一些去重，基本也就够用了。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;对于数据一致性要求高的业务，例如金融、支付等，就必须要保证在做灾难切换的时候，为了将影响的数据尽量减少，需要根据业务的特点，圈出来可能影响的数据，并针对这些相关数据的所属用户的相关操作拒绝服务。下文的数据复制架构中会提到。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;四、写量大拆分片&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;写请求量大，单个写入点的容量扛不住，这种情况下，就不能让所有数据的写入都归到同一个写入点来处理，需要做分片，将完整的数据拆分成几部分，各个部分分别有独立的写入点。单纯考虑写量大，并不要求做就近分片，但是就近分片还是能收获一些益处，例如减少 30ms 的耗时等，所以，一般也还是会做就近的分片。下图所示的写量大拆分片的情况，数据写入的时候，等把数据同步复制到异地之后，才认为请求处理成功，给上层返回。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/c4dec4fbb891451684fd6efc0cea2f6a~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=I0RwSUk2dTM8%2FIraMZEGPVgwJwM%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;另外，写量大的业务产生的数据如果是膨胀型的（例如，电商业务的订单数据），会随着时间累积，数据量不断增加。这类数据往往多呈现为流水型特征，写入一段时间后即不会再次访问或更新；对访问频率很低甚至为 0 的数据，其占用的在线业务库存储空间，造成了大量硬件资源浪费，堆高企业的 IT 成本。这种情况根据膨胀的情况，做分库分表以及老数据存档即可，不会产生数据分片而需要实例隔离的这种影响。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;五、做隔离拆分片&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;做隔离，是为了减少故障/异常情况下对整个业务系统的影响，核心思想就是“不要把鸡蛋放在一个篮子里”。这个对于数据层的影响和上文“写量大拆分片”的效果是一样的，即把全部数据分片拆分成多份，每一份出问题的时候不影响其他数据。做隔离，其实与异地多活的跨城容灾关系不大，在做同城容灾的时候，也是一种常用手段。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;业务系统，除了自身的数据层之外，往往还涉及其他的依赖，例如相关的基础组件，更底层的一些服务，运营操作平台等。所以，做隔离的时候，往往不局限于数据层的隔离，而是会把各种依赖，甚至上层的逻辑层也统一囊括进来整体考虑。这种串联上下依赖的隔离方案，名字比较多，例如“单元化”、“SET 化”、“条带化”等。下图是示意图：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;接入层，根据请求中的相关信息做单元分片路由选择；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;逻辑层，只处理归属于本单元分片内的请求；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;数据层，单从隔离角度出发，可以做跨城数据的同步复制；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;单元化隔离与本文讨论的异地多活跨城容灾的关系不是特别紧密，不过多展开，对于路由的影响在下文中会提到。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/09adf22fe251497fba94e212cba0c782~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=dMBZWC%2FY8LiWglEw89Qm5CFQTpM%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;六、其他影响因素&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;在讨论数据模型的时候，常常会聊到“读多写少”、“读写频繁”、“读写分离”等情况，可见，读也是决定数据模型的重要因素。不过，和上面写延时、写量级、隔离性等因素会导致数据分片不同，读的影响主要在副本管理、缓存机制和连接管理上。读是一个后置的二级考虑因素，即首先确定是否要做分片之后，再基于分片的基础来考虑。&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;1、读时延可就近&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;读操作，根据业务场景的需要，可以分为两种情况：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;写后立即读，这种情况，要求读到写入之后的最新值，是一种强一致性的诉求，须通过读写点的方式来解决，实际上就归入到写操作的范畴里面去了；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;适当延迟读，这种情况，可以接受读取到历史旧值，满足最终一致性要求即可，可以通过读写入之后同步数据的副本来应对，是本部分讨论的内容。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;一般来说，业务对于读操作的时延要求相较写操作有更严苛的要求，例如，写一条微博，发布一段视频，下定一件商品，发起一笔转账，用户对于适当等待是有所预期，而看微博、刷视频、浏览商品、查看账户余额等操作，用户如果感到卡顿，基本上就要流失了。大部分的读场景，都可以接受适当延迟，看不到最新的内容，用户基本上“刷新一下”就可以了。理清楚场景需要，读时延的解决方案就很明显了：提供离用户更近的副本供读取。如下图所示，从上海到访的用户，访问上海的备份副本即可，不需要到深圳去读取数据。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/bcac0164455a40d1be6d467c8b695ab6~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=bxFw%2F7QdDSfs0g23SKBlhyBZH0Q%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;2、读量大扩副本&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;和上文“读时延可就近”类似，这里依然讨论的是能够接受适当延迟读的场景。很多业务都是读多写少，大量的读请求，可以通过扩充副本来满足。不过，需要注意，当副本扩展到一定规模后，由于需要做读副本的数据复制，会增加对写点的负载，可以通过级联同步的方式来解决。另外，还会通过添加缓存的方式来进一步提升读请求的吞吐量，这里不做展开了。总体来说，读量一般都不会像写延时和写量一样产生数据分片而需要实例隔离的这种影响。下图是级联复制的示意。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/3ff01ae107034ef9b22c54aed1ec38fc~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=NTJ%2BhIuFc7zJTH8lz5f1Z41ku0g%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;3、连接多加代理&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;数据层是业务的根本，虽然通过分片、副本、缓存等操作，将落到 DB 的请求量减少到可接受的地步，但是逻辑层作为数据层的调用方，还是不可避免的需要建立和数据层 DB 的连接，如果逻辑层的调用方过多，则会需要和 DB 构建更多的连接数。增加连接数能够增加 DB 的并发度，支持更多的调用方，提升吞吐量。但是，数据库的性能并不是可以无限扩展的，当达到一个阈值以后，由于高并发导致的资源抢占、线程上下文切换，反而会导致数据库的整体性能下降。比较普遍的做法，是为数据层 DB 添加一层代理，避免逻辑层调用方直连 DB，由代理来收拢和 DB 之间的连接。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p26-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/8f23470328944d5b874f7be58d641683~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=SOn9GTEJPhFunLFcLG58WU9JslM%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;七、数据复制架构&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;上文的讨论中，对于数据的复制，都是采用了一主一备的简化表述。事实上，要达到容灾的效果，一主一备是不够的，下面来看一下几种典型的数据复制架构。&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;1、三地五中心&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;要想达到容灾的效果，基本上都是要用到多数派协议的方式来做，比较经典的模式是三地五中心架构：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;搭建1主4从5实例的架构，分布在3个城市5个 IDC 机房中；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;写请求要保证对应的数据写入到1主4从中的3个实例中，即写入主后，要同步到另外2个备，达成多数派要求；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;在发生城市故障的情况下，不管哪个城市发生故障，在该城市以外，都有完整的数据可以满足容灾要求。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/c6fceb65e8dc4abda3dcadabc007978b~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=fG1ba2UWVZ762%2Fz2GCL8MhUiSUc%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;2、三地三中心&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;三地三中心，可以形成最小的多数派，也能满足容灾需要，不过考虑到下面几点，一般都没有采纳：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;一般来说，跨城切换更复杂，成本更高；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;机器以及机房的故障概率要远高于城市故障；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;在某个 DB 实例发生故障的时候，尽量让其发生做同城内进行切换，如果是三地三中心，只要有故障就会发生跨城切换；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;三地五中心相对于三地三中心会有机器资源浪费的情况，对于跑不满资源的情况，可以采用混布的方式，通过一些资源隔离的（例如 CGroup）机制，来提高资源利用率的同时，又可避免混布业务之间互相影响。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/14eafb1e4ca649118797c72adcc7f3c7~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=bO%2FYfTg2lLePIWOnteT735pek3s%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;3、同城三中心&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;在不能接受跨城时延的场景中，会用到同城三中心的复制架构。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/041b1fbc03054332999f3fd1a71d3071~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=NXbmOTRPj2ScSzyw0ZknPcN5oxg%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;在多个城市配备多套互为对等的同城三中心，可以做到有损的跨城容灾，如下图所示：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;某城市故障时，将写请求放到异地对等的同城三中心处理，所以，下图中每个同城三中心的实例中都有一部分对等同城三中心实例的数据；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;这种容灾模式，在发生城市故障的时候，可能会发生产生重复数据的问题，例如用户在蓝色的 set1 种新增一条数据，发生了城市故障，数据来不及同步到异地的异步备，故障切换，用户的请求已经切换到对等的绿色 set2，此时，用户读不到刚刚新增的数据，就会重新操作；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;数据读取时，可以整合本城市的写点数据和对等同城三中心的异步备数据。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/05e19eb2ab0049dbb219fcbdbe712c5b~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=wHQ3D9gnD5HyCVgB8oqkLgFQVZA%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;4、双主互复制&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;双主互复制的架构，是每一个实例中都有完整的数据，不过，每一个主里面在一个时刻只处理其中一部分数据的写入，规避写冲突。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/5d42a4fea11a481b8e620dd37796a4b7~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=RCH1xxwiUNUP%2F6evtkzzxRzauD0%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;这种模式，在做跨城容灾的时候，通过记录同步时间位点的方式来决定跨城容灾时候的数据写入逻辑，也是有损的：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;维护一个统一的时间位点发生器，每次写操作，都记录时间位点，新增记录记为 Ti（insert）；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;数据做异步复制的时候，记录复制到的时间位点，记为 Ts（sync）；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;发生故障时，将故障所在的实例禁写，禁写时间记为 Tb（ban）；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;有损的情况：&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;Ts &amp;lt; Ti &amp;lt; Tb，即在主写新增的记录在没有复制到备的情况下，用户由于读不到之前在主写写入的数据，而重新尝试写操作，就会产生重复数据；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;对于所有新增时间 Ti &amp;lt; Tb &amp;amp;&amp;amp; Ts &amp;lt; Tb 的数据，即在故障禁写之前就存在的数据，不能在切换到新写点之后进行更新，因为在 Tb 之前总有数据写操作没有复制到位，如果直接更新，就可能产生写冲突。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/1862e363e0e4482d8bddb95651870ee5~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=rtVOzYe6QAG2D%2Btc0HtyqE2SI08%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;5、未同步名单&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;前面提到的对等同城三中心和双主互复制，都有可能产生重复数据，根源在于不知道哪些数据没有同步到。如果可以明确知道哪些数据没有复制到位，那么就可以针对性的拒绝这些没有复制到位的数据的操作。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;业务接受不了 N 次跨城带来的 N 倍 60ms 的影响，但是一般，能接受一次跨城 30ms 的延时，这种模式的工作机制是：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;在具体进行写操作之前，通过做一次跨城调用记录下该写操作对应的数据属主到未同步名单中；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;待该写操作同步到跨城实例后，再将写操作的数据属主从未同步名单中清理掉；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;在做数据写入之前，先检查写操作的数据属主是否存在未同步名单中，如果存在，则拒绝该请求；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;这种模式，依然是有损的，只是牺牲了极少部分未复制到位的用户，而且数据一致性得到了保障，配合双主互复制使用，可以达到很好的效果。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/c257b74af5124bb7b8c4d57f94e038c9~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=nRjG6NpzGYcc4kmwxzqpOZAuipw%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;八、数据影响路由&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;考虑从上面分析的几种因素，不同业务的数据形态可能不同，可以分为三类：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;跨城全局数据，数据不分片，主从之间做跨城同步复制；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;就近分片数据，数据需分片，由于业务不能接受跨城串行写入的耗时，只能做同城的同步复制，跨城则采用异步复制；&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;跨城分片数据，数据需分片，每个分片的主从之间做跨城同步复制。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;下面来看看这三种模式对路由的影响。&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;1、跨城全局数据就近路由&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;数据不分片，写入点只有一个，多副本的方式提供就近读取。这种情况，路由适合全链路就近的方式，按照机房-城市-全局的优先级就近选取路由。如果考虑逻辑层的隔离，也可以在接入层进行路由分流，不过，意义并不大，因为，对于全局数据来说，就近访问，已经具备了较好的隔离性。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://mmbiz.qpic.cn/mmbiz_png/VY8SELNGe94XvSa7AsevzppdQUc6a8JmNicjiaGABicbanbomouU7aVIEia0XZHdadCLJts9ALdoFN7UYKBXSqAaicg/640?wx_fmt=png&amp;from=appmsg&amp;tp=wxpic&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;2、就近分片数据接入分流&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;就近分片数据，重点在于解决写请求穿行N次写操作的跨城时延问题，所以，需要在业务执行写请求之前，把请求提前路由到数据所在的城市（机房），这样穿行的 N 次写操作就是同城（同机房）操作，免去了跨城的耗时。这就要求在执行具体写请求的逻辑层之上做路由分流，考虑到逻辑层的隔离，一般都会把路由分流放在接入层做。读请求也可以和写请求采用同样的路由策略，这样针对同一个分片的读写请求就都在一处了。每一个分片里面的同城三中心的复制架构，在下图中省略了。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/899bae1d1af34ce89ec691de2eea5048~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=aDGBtFzP8js7Sm0CaKV0Rsrd%2FfQ%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt;  &lt;p&gt;3、跨城分片数据接入分流&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;跨城分片数据能够接受跨城延时，针对写操作是支持跨城同步的，在已经做了分片的基础上，出于就近和影响隔离的考虑，基本上都会做就近，在拆分片的时候，将用户做聚集，并把各个分片的主写点分布到不同城市和机房。跨城分片数据对于路由的影响和就近分片基本上差不多。差别在于跨城分片需要引入第三个城市做数据的完整容灾，如下图的天津，第三城市一般只是为了形成数据容灾的多数派，不会做流量接入，也不会考虑做分片就近部署。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/c7b5059dfb3c461fb7619122d9e78141~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=D03ALX%2F0%2FO2%2Fufcch4ZYfWpX3Vg%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;九、架构选型模式&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;在具体进行架构设计的时候，可以参考如下步骤考量评估。下图中只是聊到了本文描述到的一些比较普遍的关键信息，刨除了很多也许在某些业务场景看来是决定性的因素，例如成本，在做多个跨城三地五中心分片的情况下，比只做就近同城三中心，吞吐量可能下降，而机器资源却上升，也有可能会成为决定采用哪种模型的决定性因素。总之，架构设计是一个非常复杂的过程，要考虑的因素繁杂多样，还是要根据业务具体情况具体分析。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img src="https://p3-sign.toutiaoimg.com/tos-cn-i-6w9my0ksvp/61db5e62fa694a82b6f18eba170452c8~tplv-tt-origin-web:gif.jpeg?_iz=58558&amp;from=article.pc_detail&amp;lk3s=953192f4&amp;x-expires=1736391563&amp;x-signature=jArfisJs14fjNe886Wfj9HCxqns%3D"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt;作者丨  熊章俊
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62963-%E6%9E%B6%E6%9E%84-%E8%AE%BE%E8%AE%A1</guid>
      <pubDate>Mon, 13 Jan 2025 16:25:26 CST</pubDate>
    </item>
    <item>
      <title>高并发系统设计思路</title>
      <link>https://itindex.net/detail/62820-%E5%B9%B6%E5%8F%91-%E7%B3%BB%E7%BB%9F-%E8%AE%BE%E8%AE%A1</link>
      <description>&lt;hr&gt;&lt;/hr&gt;
 &lt;h2&gt;theme: geek-black&lt;/h2&gt;
 &lt;p&gt;不管是哪一门语言，并发都是程序员们最为头疼的部分。同样，对于一个软件而言也是这样，你可以很快增删改查做出一个秒杀系统，但是要让它支持高并发访问就没那么容易了。比如说：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;如何让系统面对百万级的请求流量不出故障？&lt;/li&gt;
  &lt;li&gt;如何保证高并发情况下数据的一致性写？&lt;/li&gt;
  &lt;li&gt;完全靠堆服务器来解决吗？&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;基本原则&lt;/h2&gt;
 &lt;p&gt;作为一个架构师，首先要勾勒出一个轮廓，如何构建一个超大流量并发读写、高性能，以及高可用的系统，这其中有哪些要素需要考虑。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;数据要尽量少：首先是指用户请求的数据能少就少（请求的数据包括上传给系统的数据和系统返回给用户的数据），其次还要求系统依赖的数据能少就少（包括系统完成某些业务逻辑需要读取和保存的数据，这些数据一般是和后台服务以及数据库打交道）。&lt;/li&gt;
  &lt;li&gt;请求数要尽量少：用户请求的页面返回后，浏览器渲染这个页面还要包含其他的额外请求，减少请求数最常用的一个实践就是合并 CSS 和 JavaScript 文件，把多个 JavaScript 文件合并成一个文件。&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;h2&gt;做好动静分离&lt;/h2&gt;
 &lt;p&gt;所谓“动静分离”，其实就是把用户请求的数据划分为“动态数据”和“静态数据”，动态数据还是静态数据区分主要是：确认数据中是否含有和访问者相关的个性化数据。&lt;/p&gt;
 &lt;p&gt;怎样对静态数据做缓存呢？&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;把静态数据缓存到离用户最近的地方。比如调用端，客户端等等。&lt;/li&gt;
  &lt;li&gt;缓存静态数据的方式也很重要。不同语言写的 Cache 软件处理缓存数据的效率也各不相同。以 Java 为例，因为 Java 系统本身也有其弱点（比如不擅长处理大量连接请求，每个连接消耗的内存较多，Servlet 容器解析 HTTP 协议较慢），所以你可以不在 Java 层做缓存，而是直接在 Web 服务器层上做，这样你就可以屏蔽 Java 语言层面的一些弱点；而相比起来，Web 服务器（如 Nginx、Apache、Varnish）也更擅长处理大并发的静态文件请求。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;动态内容的处理通常有两种方案：ESI（Edge Side Includes）方案和 CSI（Client Side Include）方案。&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;ESI 方案（或者 SSI）：即在 Web 代理服务器上做动态内容请求，并将请求插入到静态页面中，当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响，但是用户体验较好。&lt;/li&gt;
  &lt;li&gt;CSI 方案。即单独发起一个异步 JavaScript 请求，向服务端获取动态内容。这种方式服务端性能更佳，但是用户端页面可能会延时，体验稍差。&lt;/li&gt;
&lt;/ol&gt;
 &lt;h2&gt;针对性的处理系统“热点数据”&lt;/h2&gt;
 &lt;p&gt;热点数据就是用户的热点请求对应的数据。而热点数据又分为“静态热点数据”和“动态热点数据”。“静态热点数据”，就是能够提前预测的热点数据；“动态热点数据”，就是不能被提前预测到的，系统在运行过程中临时产生的热点。&lt;/p&gt;
 &lt;p&gt;由于热点数据会引起大量的热点操作，对系统产生很大的性能压力，需要提前识别和处理。处理热点数据通常有几种思路：一是优化，二是限制，三是隔离。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;优化：优化热点数据最有效的办法就是缓存热点数据，如果热点数据做了动静分离，那么可以长期缓存静态数据。&lt;/li&gt;
  &lt;li&gt;限制：限制更多的是一种保护机制，限制的办法也有很多，例如对被访问热点数据的 ID 做一致性 Hash，然后根据 Hash 做分桶，每个分桶设置一个处理队列，这样可以把热点限制在一个请求队列里，防止因某些热点占用太多的服务器资源，而使其他请求始终得不到服务器的处理资源。&lt;/li&gt;
  &lt;li&gt;隔离：将这种热点数据隔离出来，不要让 1% 的请求影响到另外的 99%，隔离出来后也更方便对这 1% 的请求做针对性的优化。
   &lt;ol&gt;
    &lt;li&gt;业务隔离。把热点做成一种营销活动，请求方需要报名参加，从技术上来说，报名后对我们来说就有了已知热点，因此可以提前做好预热。&lt;/li&gt;
    &lt;li&gt;系统隔离。系统隔离更多的是运行时的隔离，可以通过分组部署的方式和另外 99% 分开。秒杀可以申请单独的域名，目的也是让请求落到不同的集群中。&lt;/li&gt;
    &lt;li&gt;数据隔离。秒杀所调用的数据大部分都是热点数据，比如会启用单独的 Cache 集群或者 MySQL 数据库来放热点数据，目的也是不想 0.01% 的数据有机会影响 99.99% 数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;流量削峰&lt;/h2&gt;
 &lt;p&gt;我们知道服务器的处理资源是恒定的，你用或者不用它的处理能力都是一样的，所以出现峰值的话，很容易导致忙到处理不过来，闲的时候却又没有什么要处理。但是由于要保证服务质量，我们的很多处理资源只能按照忙的时候来预估，而这会导致资源的一个浪费。&lt;/p&gt;
 &lt;p&gt;这就好比因为存在早高峰和晚高峰的问题，所以有了错峰限行的解决方案。削峰的存在，一是可以让服务端处理变得更加平稳，二是可以节省服务器的资源成本。针对热点这一场景，削峰从本质上来说就是更多地延缓用户请求的发出，以便减少和过滤掉一些无效请求，它遵从“请求数要尽量少”的原则。&lt;/p&gt;
 &lt;p&gt;流量削峰的一些操作思路：排队、分层过滤。这几种方式都是无损（即不会损失用户的发出请求）的实现方案，当然还有些有损的实现方案，包括我们后面要介绍的关于稳定性的一些办法，比如限流和机器负载保护等一些强制措施也能达到削峰保护的目的，当然这都是不得已的一些措施。&lt;/p&gt;
 &lt;h3&gt;排队&lt;/h3&gt;
 &lt;p&gt;要对流量进行削峰，最容易想到的解决方案就是用消息队列来缓冲瞬时流量，把同步的直接调用转换成异步的间接推送，中间通过一个队列在一端承接瞬时的流量洪峰，在另一端平滑地将消息推送出去。在这里，消息队列就像“水库”一样，拦蓄上游的洪水，削减进入下游河道的洪峰流量，从而达到减免洪水灾害的目的。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b69c5098dd854fb184510a1377b3dae5~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&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;把请求序列化到文件中，然后再顺序地读文件（例如基于 MySQL binlog 的同步机制）来恢复请求等方式。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;这些方式都有一个共同特征，就是把“一步的操作”变成“两步的操作”，其中增加的一步操作用来起到缓冲的作用。&lt;/p&gt;
 &lt;h3&gt;分层过滤&lt;/h3&gt;
 &lt;p&gt;对请求进行分层过滤，从而过滤掉一些无效的请求。分层过滤其实就是采用“漏斗”式设计来处理请求的，如下图所示。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b303b4eb3ee04669a989083ce01a5210~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;分层过滤的核心思想是：在不同的层次尽可能地过滤掉无效请求，让“漏斗”最末端的才是有效请求。而要达到这种效果，我们就必须对数据做分层的校验。&lt;/p&gt;
 &lt;p&gt;分层校验的基本原则是：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;将动态请求的读数据缓存（Cache）在 Web 端，过滤掉无效的数据读；&lt;/li&gt;
  &lt;li&gt;对读数据不做强一致性校验，减少因为一致性校验产生瓶颈的问题；&lt;/li&gt;
  &lt;li&gt;对写数据进行基于时间的合理分片，过滤掉过期的失效请求；&lt;/li&gt;
  &lt;li&gt;对写请求做限流保护，将超出系统承载能力的请求过滤掉；&lt;/li&gt;
  &lt;li&gt;对写数据进行强一致性校验，只保留最后有效的数据。&lt;/li&gt;
&lt;/ol&gt;
 &lt;h2&gt;如何提高系统的性能&lt;/h2&gt;
 &lt;p&gt;对 Java 系统来说，可以优化的地方很多，这里我重点说一下比较有效的几种手段，供你参考，它们是：减少编码、减少序列化、Java 极致优化、并发读优化。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;减少编码：Java 的编码运行比较慢，这是 Java 的一大硬伤。
   &lt;ul&gt;
    &lt;li&gt;在很多场景下，只要涉及字符串的操作（如输入输出操作、I/O 操作）都比较消耗 CPU 资源，不管它是磁盘 I/O 还是网络 I/O，因为都需要将字符转换成字节，而这个转换必须编码。那么如何才能减少编码呢？例如，网页输出是可以直接进行流输出的，即用 resp.getOutputStream() 函数写数据，把一些静态的数据提前转化成字节，等到真正往外写的时候再直接用 OutputStream() 函数写，就可以减少静态数据的编码转换。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
  &lt;li&gt;减少序列化：序列化也是 Java 性能的一大天敌，减少 Java 中的序列化操作也能大大提升性能。又因为序列化往往是和编码同时发生的，所以减少序列化也就减少了编码。
   &lt;ul&gt;
    &lt;li&gt;序列化大部分是在 RPC 中发生的，因此避免或者减少 RPC 就可以减少序列化，当然当前的序列化协议也已经做了很多优化来提升性能。有一种新的方案，就是可以将多个关联性比较强的应用进行“合并部署”，而减少不同应用之间的 RPC 也可以减少序列化的消耗。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
  &lt;li&gt;并发读优化：集中式缓存为了保证命中率一般都会采用一致性 Hash，所以同一个 key 会落到同一台机器上。虽然单台缓存机器也能支撑 30w/s 的请求，但还是远不足以应对像“大秒”这种级别的热点。采用应用层的 LocalCache，即在热点系统的单机上缓存热点相关的数据。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;兜底方案&lt;/h2&gt;
 &lt;p&gt;系统的高可用建设，它其实是一个系统工程，需要考虑到系统建设的各个阶段，也就是说它其实贯穿了系统建设的整个生命周期。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/943ebd0cfd2c4513bcc20dc9b90173a3~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在遇到大流量时，应该从哪些方面来保障系统的稳定运行，所以更多的是看如何针对运行阶段进行处理，这就引出了接下来的内容：降级、限流和拒绝服务。&lt;/p&gt;
 &lt;h3&gt;降级&lt;/h3&gt;
 &lt;p&gt;所谓“降级”，就是当系统的容量达到一定程度时，限制或者关闭系统的某些非核心功能，从而把有限的资源保留给更核心的业务。&lt;/p&gt;
 &lt;p&gt;它是一个有目的、有计划的执行过程，所以对降级我们一般需要有一套预案来配合执行。如果我们把它系统化，就可以通过预案系统和开关系统来实现降级。它分为两部分，一部分是开关控制台，它保存了开关的具体配置信息，以及具体执行开关所对应的机器列表；另一部分是执行下发开关数据的 Agent，主要任务就是保证开关被正确执行，即使系统重启后也会生效。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e2e3b1bb9c7040b986e40f3884cb365f~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;执行降级无疑是在系统性能和用户体验之间选择了前者，降级后肯定会影响一部分用户的体验。&lt;/p&gt;
 &lt;h3&gt;限流&lt;/h3&gt;
 &lt;p&gt;如果说降级是牺牲了一部分次要的功能和用户的体验效果，那么限流就是更极端的一种保护措施了。限流就是当系统容量达到瓶颈时，我们需要通过限制一部分流量来保护系统，并做到既可以人工执行开关，也支持自动化保护的措施。&lt;/p&gt;
 &lt;p&gt;总体来说，限流既可以是在客户端限流，也可以是在服务端限流。此外，限流的实现方式既要支持 URL 以及方法级别的限流，也要支持基于 QPS 和线程的限流。  &lt;img alt="image.png" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/52ca4a0be0e44487934aed4ecfee9db2~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&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;/ul&gt;
 &lt;h3&gt;拒绝服务&lt;/h3&gt;
 &lt;p&gt;如果限流还不能解决问题，最后一招就是直接拒绝服务了。&lt;/p&gt;
 &lt;p&gt;当系统负载达到一定阈值时，例如 CPU 使用率达到 90% 或者系统 load 值达到 2*CPU 核数时，系统直接拒绝所有请求，这种方式是最暴力但也最有效的系统保护方式。&lt;/p&gt;
 &lt;p&gt;拒绝服务可以说是一种不得已的兜底方案，用以防止最坏情况发生，防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务，但是系统仍然可以运作，当负载下降时又很容易恢复，所以每个系统和每个环节都应该设置这个兜底方案，对系统做最坏情况下的保护。&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62820-%E5%B9%B6%E5%8F%91-%E7%B3%BB%E7%BB%9F-%E8%AE%BE%E8%AE%A1</guid>
      <pubDate>Sat, 29 Jul 2023 11:52:17 CST</pubDate>
    </item>
    <item>
      <title>从 Oracle 迁移到 TiDB 的方案设计与用户实践</title>
      <link>https://itindex.net/detail/62766-oracle-tidb-%E8%AE%BE%E8%AE%A1</link>
      <description>&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;p&gt;  &lt;strong&gt;王耀强&lt;/strong&gt; ， PingCAP 资深解决方案架构师&lt;/p&gt;
 &lt;h2&gt;  &lt;strong&gt;导读&lt;/strong&gt;&lt;/h2&gt;
 &lt;p&gt;当前，全球数字化浪潮推动数字经济与实体经济融合，更多的企业意识到数据平台对业务增长和创新的重要性。通过国产化迁移和替换数据库，中国数据库市场蓬勃发展，为企业自主创新奠定了基础。本文以中国人寿财险公司为例，详述其从 Oracle 到 TiDB 分布式数据库的四个阶段的迁移，展示了金融行业对数据库的高要求和国产数据库的价值应用。&lt;/p&gt;
 &lt;h1&gt;背景和前言&lt;/h1&gt;
 &lt;p&gt;当前，数字化浪潮席卷全球，随着大数据、云计算、移动互联网等数字技术的广泛应用，数字经济与实体经济深度融合的趋势越加明显。在数字化转型加速期，全球企业与组织深刻意识到数据平台是业务发展的重要驱动力，如何有效利用数据，充分释放数据价值，成为业务增长和创新的关键基础。&lt;/p&gt;
 &lt;p&gt;近年来，科技创新、科技自立自强已作为重要发展战略，中国数据库市场正迎来发展的黄金窗口期。在 IT 基础设施之中，数据库作为三大基础软件之一，承载了各类业务系统的不同种类数据，如客户信息类、交易流水类、用户行为类等，通过对底层数据库的国产化替换和迁移，为企业 IT 系统的自主创新奠定坚实的基础。&lt;/p&gt;
 &lt;h1&gt;业务系统替换升级的发展路径&lt;/h1&gt;
 &lt;p&gt;企业在不同时期、由不同业务部门主导来推动信息系统的建设，割裂的烟囱式架构使得系统之间的数据无法打通并利用。数字化转型和国产数据库的迁移工作，并不是简单的对传统商业数据库产品（如 Oracle 等）替换，更是要站在企业发展的战略高度，重新对业务、应用、技术架构等方面进行综合考量，满足企业可持续规模化发展的需求。金融机构在国产数据库替换道路上，呈现出两类发展路径：&lt;/p&gt;
 &lt;h2&gt;  &lt;em&gt;   &lt;strong&gt;1&lt;/strong&gt;&lt;/em&gt;   &lt;strong&gt;对成熟商业数据库的平行替换&lt;/strong&gt;&lt;/h2&gt;
 &lt;p&gt;金融机构有部分业务系统由于开发时间较早、熟悉代码的人员较少、又特别依赖于成熟商业数据库的特性等原因，往往会优先选择不改或者少改应用、使用兼容商业数据库特性的国产数据库进行替换。虽然短期内能够实现快速替换，但长期看这种方案使得技术债进一步堆积，用户对原有数据库的粘性没有减少，业务开发人员的习惯并没有改变，因此“平行替换”适用于相对较传统的应用。&lt;/p&gt;
 &lt;h2&gt;  &lt;em&gt;   &lt;strong&gt;2&lt;/strong&gt;&lt;/em&gt;   &lt;strong&gt;从集中式到分布式的架构跃迁&lt;/strong&gt;&lt;/h2&gt;
 &lt;p&gt;分布式、云原生为代表的新技术的出现，为数据库技术突破实现弯道超车做好了理论铺垫。伴随着数据使用场景的多元化，对于海量数据增长迅速、高并发读写、高峰业务弹性需求大的业务系统，集中式商业数据库已经无法支撑，从敏捷开发迭代、技术自主掌控、业务连续性等角度进行评估，金融机构更倾向选择国产分布式数据库实现架构的跃迁，这样才能在基础架构层面开辟自主创新的道路。&lt;/p&gt;
 &lt;h1&gt;从 Oracle 迁移到 TiDB 实践&lt;/h1&gt;
 &lt;p&gt;金融行业对于数据库的要求极其严苛，“稳定、高可用、高并发、高扩展”是选择合适国产数据库的多维度考量标准。TiDB 作为原生分布式数据库，已广泛应用金融、互联网、电信、能源等各行各业中的不同业务场景，在积极协助企业稳步推进国产化相关工作，并总结了数据库国产化迁移的实践经验。&lt;/p&gt;
 &lt;p&gt;为响应国家层面“加快建设科技强国，实现高水平科技自立自强”的号召，中国人寿财险公司积极推进数字化转型，启动 IT 架构的整体规划工作，探索业界先锋技术及进行分布式架构转型。在数据库的国产化迁移过程中，按照先边缘再核心的策略稳步推进，目前已完成多个核心业务系统从 Oracle 到 TiDB 的迁移改造工作，同时也为后续多部署形态的架构打下了坚实的基础。本文以中国人寿财险公司核心系统的改造实践为蓝本，阐述通过四个阶段的分步骤实施，实现从 Oralce 迁移到 TiDB 分布式数据库。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="img" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/148262f6cc0841e4adf85fabb45bc3c7~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;中国人寿财险核心系统分布式数据库改造历程&lt;/p&gt;
 &lt;h2&gt;  &lt;em&gt;   &lt;strong&gt;1&lt;/strong&gt;&lt;/em&gt;   &lt;strong&gt;迁移准备阶段&lt;/strong&gt;&lt;/h2&gt;
 &lt;p&gt;首先是对分布式数据库的选型，从数据库产品特点是否与业务场景匹配，满足业务平滑迁移及持续发展；是否完全自主研发，保证产品的持续演进能力；是否有完善的生态建设，包括上下游工具、文档体系、培训体系；是否有广泛的行业用户案例；是否支持云原生，满足未来的架构演进等角度进行综合评估后进行决策。&lt;/p&gt;
 &lt;p&gt;其次是迁移改造规划，主要涉及迁移改造量分析、迁移方案制定和回退方案制定几个方面。异构数据库之间的迁移适配，通常涉及应用系统的改造。以保险为例，保险业务逻辑处理复杂，各业务系统之间的调用关系、完成整个交易的复杂度较高。中国人寿财险制定的策略是不再依赖封闭的商业数据库特性，而是由应用来主导业务流程实现，实现系统分布式微服务架构的转型。应用改造分析主要包括各系统间调用模式、微服务的设计等。异构数据库的改造分析，涉及数据库对象改造（表结构、其他对象等）、SQL 语法改造、运维工具改造和流程变更等；通过提供的 O2T schema 比对工具，可以比对 schema 迁移后，检测出是否有表、视图、或索引遗失的情况。&lt;/p&gt;
 &lt;p&gt;迁移方案的制定需要结合系统的存量数据及每日增量数据情况，制定切换前全量截面数据加截面后增量追数的迁移方式，形成了迁移手册及迁移中使用的脚本工具，为后续项目的开展提供经验支持。借助 Kettle、SQLULDR2 与 Lightning、国产数据库同步工具、OGG 等多种模式，丰富数据迁移方案、实现差异化的需求，同时可以通过 o2t-sync-diff 工具比对 Oracle 和 TiDB 的快照数据正确性。&lt;/p&gt;
 &lt;p&gt;数据库是保险企业信息系统当中的关键基础设施，稳定运行是重中之重，因此也需要制定完善的回退方案。从 Oracle 到 TiDB 迁移，可以使用 OGG 进行数据实时同步，反向同步通过 TiDB Drainer 工具把 Oracle 作为目标库，实现高效的反向回退。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="img" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7b6d56dbdc604435802f1bba72d8dbae~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;从 Oracle 迁移到 TiDB 的并行和回退机制&lt;/p&gt;
 &lt;h2&gt;  &lt;em&gt;   &lt;strong&gt;2&lt;/strong&gt;&lt;/em&gt;   &lt;strong&gt;开发测试阶段&lt;/strong&gt;&lt;/h2&gt;
 &lt;p&gt;开发阶段需要对于原有业务系统使用的数据库对象类型、数据库函数功能等进行细致分析，通过 Oracle 到 TiDB 的数据类型映射、函数映射等指导手册、并结合 TiDB 数据库开发规范，完成代码开发及单元测试工作。测试阶段需要涵盖功能、性能、高可用、流量双发和回退测试。在系统开发完毕之后，需要进行各业务功能测试，并按照 3-5 年未来业务量的数据存储基线做性能测试，完成业务单交易负载测试、混合交易负载、水平扩展性测试等各项压力测试，验证高并发条件下数据库所能承载的业务流量。&lt;/p&gt;
 &lt;p&gt;在高可用测试方面，需要完成分布式数据库生产部署架构搭建、进行数据库基础组件故障演练，进行数据库同城和异地切换演练，以及进行数据库物理备份和逻辑备份恢复等测试。流量双发测试可以对生产业务流量进行异步复制，搭建生产流量双发并行环境，实现 100% 全量业务生产级别仿真执行，对执行结果利用 o2t-sync-diff 比对工具进行生产和并行环境数据的比对验证，同时在并行环境搭建 TiDB Drainer 数据库增量回退链路，实现数据库日志级别的一致性数据同步，进行应用和数据库回退演练。&lt;/p&gt;
 &lt;h2&gt;  &lt;em&gt;   &lt;strong&gt;3&lt;/strong&gt;&lt;/em&gt;   &lt;strong&gt;投产切换与运行保障&lt;/strong&gt;&lt;/h2&gt;
 &lt;p&gt;投产切换的所有步骤严格按照投产前演练的顺序和操作方式进行，防止出现流程上以及操作上的风险。以中国人寿财险报价中心投产切换为例，为了防止投产风险，制定了分批切换策略：第一批按照 10% 的业务流量将部分典型渠道进行切换验证，数据迁移按照全量和增量迁移，无需回退；第二批按照 30% 的业务流量将标准和个性化的业务进行切换，重复第一批迁移流程，部分业务表进行回退链路搭建；最后一批切换 60% 的核心业务流量，新增部分核心业务表的回退链路。在投产后进行 48 小时内进行数据库各性能指标实时监控保障支持，保障投产顺利实施完成。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="img" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6ed6635e879c47e7827a37a3ccec182f~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;中国人寿财险分批次切投产切换&lt;/p&gt;
 &lt;h2&gt;  &lt;em&gt;   &lt;strong&gt;4&lt;/strong&gt;&lt;/em&gt;   &lt;strong&gt;成效总结与未来展望&lt;/strong&gt;&lt;/h2&gt;
 &lt;p&gt;TiDB 上线后完全满足各项业务性能指标，运行稳定，承载核心系统报价中心的全渠道业务量。TiDB 分布式数据库在中国人寿财险核心业务的成功应用，首先节省了成本，通过 X86 通用服务器代替 IBM 小型机，硬件投入成本降低了 75%；其次，用先进的分布式数据库替换集中式数据库，改造完成之后，OLTP 和 OLAP 性能较原来的 Oracle 数据库均有了明显上升，其中全量状态统计报表的处理时效提升 80 多倍，并具备了更强的数据汇聚和查询能力；最后，通过开放架构实现了安全可控，初步打造了中国人寿财险的自主创新体系，并不断演进，为后续异地双活改造奠定了坚实的技术基础。&lt;/p&gt;
 &lt;p&gt;目前，国内金融机构的重要业务系统仍在使用国外成熟的商业数据库，而国产数据库从成熟度和稳定性上还需要进行持续的打磨。数据库技术的发展离不开新兴技术和业务场景的融合，因此不必局限于传统数据库的技术锁定。在产业变革加速的今天，中国数据库厂商应加大核心技术攻关力度，提升产品创新性和影响力，加强知识产权保护，探索变道超车的发展道路，逐步形成技术引领、生态完善、应用成功的创新发展局面。&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62766-oracle-tidb-%E8%AE%BE%E8%AE%A1</guid>
      <pubDate>Thu, 25 May 2023 13:00:55 CST</pubDate>
    </item>
    <item>
      <title>工作十年，在腾讯沉淀的高可用系统架构设计经验</title>
      <link>https://itindex.net/detail/62678-%E5%B7%A5%E4%BD%9C-%E5%8D%81%E5%B9%B4-%E8%85%BE%E8%AE%AF</link>
      <description>&lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e4a8a8d2df8744c79821cbe8adabc9fa~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/12ab009747cf43f9accfe03f174848ad~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&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;/p&gt;
 &lt;p&gt;1 高可用系统的架构设计思想&lt;/p&gt;
 &lt;p&gt;1.1 可用性和高可用概念&lt;/p&gt;
 &lt;p&gt;1.2 高可用系统设计思想&lt;/p&gt;
 &lt;p&gt;2 研发规范层面&lt;/p&gt;
 &lt;p&gt;2.1 方案设计和编码规范&lt;/p&gt;
 &lt;p&gt;2.2 容量规划和评估&lt;/p&gt;
 &lt;p&gt;2.3 QPS 预估（漏斗型）&lt;/p&gt;
 &lt;p&gt;3 应用服务层面&lt;/p&gt;
 &lt;p&gt;3.1 无状态和负载均衡设计&lt;/p&gt;
 &lt;p&gt;3.2 弹性扩缩容设计&lt;/p&gt;
 &lt;p&gt;3.3 异步解耦和削峰设计（消息队列）&lt;/p&gt;
 &lt;p&gt;3.4 故障和容错设计&lt;/p&gt;
 &lt;p&gt;3.5 过载保护设计（限流、熔断、降级）&lt;/p&gt;
 &lt;p&gt;4 存储层面&lt;/p&gt;
 &lt;p&gt;4.1 集群存储（集中式存储）&lt;/p&gt;
 &lt;p&gt;4.2 分布式存储&lt;/p&gt;
 &lt;p&gt;5 产品层面&lt;/p&gt;
 &lt;p&gt;6 运维部署层面&lt;/p&gt;
 &lt;p&gt;6.1 开发阶段-灰度发布、接口测试设计&lt;/p&gt;
 &lt;p&gt;6.2 开发阶段-监控告警设计&lt;/p&gt;
 &lt;p&gt;6.3 开发阶段-安全性、防攻击设计&lt;/p&gt;
 &lt;p&gt;6.4 部署阶段-多机房部署（容灾设计）&lt;/p&gt;
 &lt;p&gt;6.5 线上运行阶段-故障演练（混沌实验）&lt;/p&gt;
 &lt;p&gt;6.6 线上运行阶段-接口拨测系列设计&lt;/p&gt;
 &lt;p&gt;7 异常应急层面&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e67bb16849b5445ea5fbccfee6e23c9b~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;01、高可用系统的架构设计思想&lt;/h2&gt;
 &lt;h4&gt;  &lt;strong&gt;1.1&lt;/strong&gt;   &lt;strong&gt;可用性和高可用概念&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;可用性是一个可以量化的指标，是指在某个考察时间，系统能够正常运行的概率或时间占有率期望值。行业内一般用几个 9 表示可用性指标，对应用的可用性程度一般衡量标准有三个 9 到五个 9。一般我们的系统至少要到 4 个 9（99.99%）的可用性才能谈得上高可用。&lt;/p&gt;
 &lt;p&gt;高可用 High Availability 的定义（From 维基百科）：&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td align="left" colspan="1" rowspan="1" valign="middle" width="557"&gt;高可用是 IT 术语，指系统无中断地执行其功能的能力，代表系统的可用性程度，是进行系统设计时的准则之一。服务不可能 100% 可用，因此要提高我们的高可用，就要尽最大可能的去增加我们服务的可用性，提高可用性指标。&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;p&gt;一句话来表述就是：高可用就是让我们的服务在任何情况下，都尽最大可能地能够对外提供服务。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;2.2&lt;/strong&gt;   &lt;strong&gt;高可用系统设计思想&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;高可用系统的架构设计，需要有一套比较科学的工程管理套路。要从产品、开发、运维、基建等全方位去考量和设计。  &lt;strong&gt;高可用系统的架构设计思想包括但不限于&lt;/strong&gt;：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;做好研发规范。系统都是研发人员设计和编码写出来的，因此首先要对研发层面有一个规范和标准。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;做好容量规划和评估。主要是让开发人员对系统要抗住的量级有一个基本认知，方便进行合理的架构设计和演进。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;做好服务层面的高可用。主要是负载均衡、弹性扩缩容、异步解耦、故障容错、过载保护等。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;做好存储层面的高可用。主要是冗余备份（热备，冷备）、失效转移（确认，转移，恢复）等。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;做好运维层面的高可用。主要是发布测试、监控告警、容灾、故障演练等。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;做好产品层面的高可用。主要是兜底策略等。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;做好应急预案。主要是要思考在出现问题后怎样快速恢复，不至于让我们的异常事态扩大。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;02、  &lt;strong&gt;研发规范层面&lt;/strong&gt;&lt;/h2&gt;
 &lt;h4&gt;  &lt;strong&gt;2.1&lt;/strong&gt;   &lt;strong&gt;方案设计和编码规范&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;研发规范层面是大家容易忽视的一个点。但是我们所有的设计，都是研发人员来完成的，包括从设计文档到编码再到发布上线。因此，研发层面也要有一个规范流程和套路，以让我们更好地去研发和维护一个高可用的系统：&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td align="center" valign="top" width="123"&gt;    &lt;strong&gt;阶段&lt;/strong&gt;    &lt;br /&gt;&lt;/td&gt;   &lt;td align="center" valign="top" width="403"&gt;事项&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="2" valign="middle" width="123"&gt;    &lt;strong&gt;设计&lt;/strong&gt;    &lt;strong&gt;阶段&lt;/strong&gt;&lt;/td&gt;   &lt;td align="left" valign="middle" width="403.3333333333333"&gt;规范好相关方案设计文档的模板和提纲，让团队内部保持统一。&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="left" valign="middle" width="403"&gt;方案设计后一定要进行评审。在团队中，新项目一定要评审，重构项目一定要评审，大的系统优化或者升级一定要评审，其他的一般研发工作量超过一周的建议也要评审。&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="3" valign="middle" width="123"&gt;    &lt;strong&gt;编码阶段&lt;/strong&gt;&lt;/td&gt;   &lt;td align="left" valign="middle" width="403"&gt;    &lt;strong&gt;执行代码规范：&lt;/strong&gt;    &lt;ul&gt;     &lt;li&gt;工程的 layout 目录需结构规范，团队内部保持统一，尽量简洁；&lt;/li&gt;     &lt;li&gt;遵循团队内部的代码规范。一般公司都有对应语言的规范，如果没有则参考官方的规范，代码规范可以大大减少 bug 并且提高可用性。&lt;/li&gt;&lt;/ul&gt;&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="left" height="163" valign="middle" width="403"&gt;    &lt;strong&gt;单测覆盖率：&lt;/strong&gt;    &lt;ul&gt;     &lt;li&gt;代码编写完需要有一定的单测能力来保证代码的健壮性，同时也能保障我们后续调整逻辑或者优化的时候可以保证代码的稳定。&lt;/li&gt;     &lt;li&gt;包括：增量覆盖率、全量覆盖率。具体的覆盖率要达到多少，可以根据团队内部的实际情况来定，一般定的规则是 50% 的覆盖率。&lt;/li&gt;&lt;/ul&gt;&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="left" height="60" valign="middle" width="403"&gt;    &lt;strong&gt;日志规范&lt;/strong&gt;：    &lt;p&gt;不要随便打日志、要接入远程日志、要能够分布式链路追踪。&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" valign="middle" width="123"&gt;    &lt;strong&gt;发布上线阶段&lt;/strong&gt;&lt;/td&gt;   &lt;td align="left" valign="middle" width="403"&gt;参考下面运维部署层面章节的灰度发布和接口测试相关说明（即6.1）。&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h4&gt;  &lt;strong&gt;2.2&lt;/strong&gt;   &lt;strong&gt;容量规划和评估&lt;/strong&gt;&lt;/h4&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;strong&gt;容量评估：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;是指需要评估好在做的这个系统是为了应对一个什么体量的业务、这个业务请求量的平均值、高峰的峰值大概都在一个什么级别。&lt;/p&gt;
 &lt;p&gt;如果是新系统，那么就需要先搜集产品和运营同事对业务的大体预估，开发者根据他们给的数据再进行详细评估。如果是老系统，那么就可以根据历史数据来评估。评估的时候，要从一个整体角度来看全局的量级，然后再细化到每个子业务模块要承载的量级。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;strong&gt;容量规划：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;是指系统在设计的时候，就要能够初步规划好系统大致能够维持的量级，比如是十万级还是百万级别的请求量，或者更多。不同量级对应的系统架构设计完全不一样。尤其到了千万、亿级别的量级的时候，架构设计会有更多的考量。&lt;/p&gt;
 &lt;p&gt;这里值得注意的是，不需要在一开始就设计出远超当前业务真实流量的系统，要根据业务实际情况来设计。同时，容量规划还涉及到：系统上下游的各个模块、依赖的存储、依赖的三方服务分别需要多少资源，需要有一个相对可量化的数据。容量规划阶段更多是要依靠自身和团队的经验，比如要了解系统的 log 的性能、redis 的性能、rpc 接口的性能、服务化框架的性能等等，然后根据各种组件的性能来综合评估已经设计的系统的整体性能情况。&lt;/p&gt;
 &lt;p&gt;容量评估和容量规划之后，还需要做就是性能压测。最好是能够做到全链路压测。&lt;/p&gt;
 &lt;p&gt;性能压测的目的是确保系统的容量规划是否准确。假设设计的这个系统，规划的是能够抗千万级别的请求。那么实际上，真的能够抗住吗 ？在上线之前首先要根据经验来判断，其次是一定要经过性能压测得出准确结论。  &lt;strong&gt;性能压测要关注的指标很多，但是重点要关注的是两个指标：一个是 QPS，一个是响应耗时要确保压测的结果符合预期&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;压测的步骤：可以先分模块单独压测。最后如果情况允许，那么最好执行全链路压测。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;2.3&lt;/strong&gt;   &lt;strong&gt;QPS 预估（漏斗型）&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;  &lt;strong&gt;QPS 预估（漏斗型）指的是：一个真实的请求过来后，从接入层开始分别经过了整个系统的哪些层级、哪些模块，每一个层级的 QPS 的量级分别有多少。&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;从请求链路上来看，层级越往下，下游层级的量级就会越少。因为每经过一个层级，都有可能会被各种条件过滤掉一部分请求。例如进入活动页后查看商品详情然后下单这个例子，首先进入活动页，所有的请求都会进入访问。然后只会有部分用户查询商品详情。最后查看商品详情的这些用户又只会有部分用户会下单。这里就会有一个漏斗，所以从上层模块到下层模块的量级一定是逐步减少的。&lt;/p&gt;
 &lt;p&gt;QPS 预估（漏斗型）需要开发者按照请求的层面和模块，来构建预估漏斗模型，然后预估好每一个层级的量级。包括但不限于从服务、接口、分布式缓存等各个层面来预估，最后构成完整的 QPS 漏斗模型。&lt;/p&gt;
 &lt;h2&gt;03、应用服务层面&lt;/h2&gt;
 &lt;h4&gt;  &lt;strong&gt;3.1&lt;/strong&gt;   &lt;strong&gt;无状态和负载均衡设计&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;要做到系统的高可用，一般应用服务的常规设计都是无状态的。这也就意味着，开发者可以部署多个实例来提高系统的可用性。而这多个实例之间的流量分配，就需要依赖系统的负载均衡能力。  &lt;strong&gt;「无状态 + 负载均衡」既可以让系统提高并发能力，也可以提高系统的可用性。&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;如果开发者的业务服务使用的是各种微服务框架，那么大概率在这个微服务框架里面就包含了服务发现和负载均衡的能力。这一整套流程包括：服务注册和发现、负载均衡、健康状态检查和自动剔除。当系统的任何一个服务实例出现故障后，它会被自动剔除掉。当系统有新增一个服务实例后，它会被会自动添加进来提供服务。&lt;/p&gt;
 &lt;p&gt;如果大家不是使用的微服务框架来开发的，那么就需要依赖负载均衡的代理服务，例如 LVS、Nginx 来帮系统实现负载均衡。当然，腾讯云的 CLB 负载均衡也支持亿级连接和千万级并发，各位感兴趣的话可自行搜索了解。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;3.2&lt;/strong&gt;   &lt;strong&gt;弹性扩缩容设计&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;弹性扩缩容设计是应对突峰流量的非常有效的手段之一，同时也是保障系统服务可用性的必要手段。弹性扩缩容针对的是系统无状态的应用服务而言的。服务是无状态的，因此可以随时根据请求量的大小来进行扩缩容，流量大就扩容来应对大量请求，流量小的时候就缩容减少资源占用。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;怎么实现弹性扩缩容呢？&lt;/strong&gt; 现阶段都是云原生时代，大部分的公司都是采用容器化（K8s）部署，那么基于这个情况的话，弹性扩缩容就非常容易了，只需要配置好 K8s 的弹性条件就能自动根据 CPU 的使用率来实现。&lt;/p&gt;
 &lt;p&gt;如果不是容器化部署，是物理机部署的方式，那么要做到弹性扩缩容，必须要有一个公司内部的基础建设能力、能够在运营平台上针对服务的 CPU 或者 QPS 进行监控。如果超过一定的比例就自动扩缩容，就和 K8s 的弹性原理是一样的，只是需要自行实现。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;3.3&lt;/strong&gt;   &lt;strong&gt;异步解耦和削峰设计（消息队列）&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;  &lt;strong&gt;要想系统能够高可用？&lt;/strong&gt; 从架构层面来说，要做到分层、分模块来设计。而分层分模块之后各个模块之间，还可以进行异步处理、解耦处理。目的是为了不相互影响，通过异步和解耦可以使系统的架构大大的提升可用性。&lt;/p&gt;
 &lt;p&gt;架构层面的异步解耦方式就是采用消息队列（比如常见的 Kafka），并且同时消息队列还有削峰的作用，这两者都可以提高系统的架构可用性：&lt;/p&gt;
 &lt;p&gt;采用消息队列之后，可以把同步的流程转换为异步的流程，消息生成者和消费者都只需要和消息队列进行交互。这样不仅做了异步处理，还将消息生成者和消费者进行了隔离。&lt;/p&gt;
 &lt;table align="center"&gt;  &lt;tr&gt;   &lt;td align="center" valign="middle" width="55.33333333333333"&gt;    &lt;p&gt;     &lt;strong&gt;方式&lt;/strong&gt;     &lt;br /&gt;&lt;/p&gt;&lt;/td&gt;   &lt;td align="center" valign="middle" width="471"&gt;    &lt;p&gt;解析&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="1" valign="middle" width="55.33333333333333"&gt;    &lt;p&gt;     &lt;strong&gt;异步&lt;/strong&gt;     &lt;br /&gt;&lt;/p&gt;&lt;/td&gt;   &lt;td align="left" colspan="1" rowspan="1" valign="middle" width="68"&gt;异步处理的优势在于，不管消息的后续处理的业务服务是否完成，只要消息队列还没满，那么就可以执行对外提供服务。而消费方则可以根据自身处理能力来消费消息，再进行处理。&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="1" valign="middle" width="55.33333333333333"&gt;    &lt;p&gt;解耦     &lt;br /&gt;&lt;/p&gt;&lt;/td&gt;   &lt;td align="left" colspan="1" rowspan="1" valign="middle" width="68"&gt;    &lt;p&gt;解耦的优势在于如果消费方异常，并不影响生产方，依然可以对外提供服务。消息消费者恢复后可以继续从消息队列里面，获取消费数据后执行业务逻辑。&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="3" valign="middle" width="55.33333333333333"&gt;    &lt;p&gt;     &lt;strong&gt;削峰&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;   &lt;td align="left" colspan="1" rowspan="3" valign="middle" width="471"&gt;采用消息队列之后，还可以做到削峰的作用，当并发较高的时候甚至是流量突发的时候，只要消息生产者能够将消息写入到消息队列中，那么这个消息就不会丢。后续处理逻辑会慢慢的去消息队列里面消费这些突发的流量数据。这样就不会因为有突发流量而把整个系统打垮。&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h4&gt;  &lt;strong&gt;3.4&lt;/strong&gt;   &lt;strong&gt;故障和容错设计&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;任何服务，一定会存在失败的情况，不可能有 100% 的可用性。服务在线上运行过程中，总会遇到各种各样意想不到的问题会让服务出现状况，因此业界来评价可用性 SLA 都是说多少个 9，例如 4 个 9(99.99%)的可用性。&lt;/p&gt;
 &lt;p&gt;为此，一般设计建议遵循「design for failure」的设计原则，设计出一套可容错的系统。需要做到尽早返回、自动修复，细节如下：&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td align="center" height="32" valign="top" width="123"&gt;    &lt;p&gt;     &lt;strong&gt;要点&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;   &lt;td align="center" height="32" valign="top" width="403"&gt;    &lt;p&gt;解析&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="2" valign="middle" width="123"&gt;    &lt;p&gt;     &lt;strong&gt;      &lt;strong&gt;       &lt;strong&gt;遵循 fail fast 原则&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;   &lt;td align="left" colspan="1" rowspan="2" valign="middle" width="403.3333333333333"&gt;    &lt;p&gt;Fail fast 原则是说，当系统的主流程的任何一步出现问题的时候，应该快速合理地结束整个流程，尽快返回错误，而不是等到出现负面影响才处理。&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="3" valign="middle" width="123"&gt;    &lt;p&gt;     &lt;strong&gt;      &lt;strong&gt;       &lt;strong&gt;具备自我保护的能力&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/p&gt;&lt;/td&gt;   &lt;td align="left" colspan="1" rowspan="3" valign="middle" width="403"&gt;    &lt;p&gt;当系统依赖的其他服务出现问题的时候，要尽快的进行降级、兜底等各种异常保护措施，避免出现连锁反应导致整个服务完全不可用。例如当系统依赖的数据存储出现问题，不能一直重试从而导致数据完全不可用。&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h4&gt;  &lt;strong&gt;3.5&lt;/strong&gt;   &lt;strong&gt;过载保护设计（限流、熔断、降级）&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;系统无法高可用的一个重要原因就在于：系统经常会有突发的流量到来，导致服务超载运行。这个时候首先要做的是快速扩容，并且开发者事先就要预留好一定的冗余。另外一个情况下，就算扩容了，但是还是会超载。例如超过了下游依赖的存储的最大容量、或者超过了下游依赖的三方服务的最大容量。&lt;/p&gt;
 &lt;p&gt;那么这个时候，系统就需要执行过载保护策略了，主要包括限流、熔断、降级，过载保护是为了保证服务部分可用，不至于导致整个服务完全不可用。&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td align="center" valign="top" width="123"&gt;    &lt;strong&gt;过载保护手段&lt;/strong&gt;&lt;/td&gt;   &lt;td align="center" valign="top" width="403"&gt;解析&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="2" valign="middle" width="123"&gt;    &lt;strong&gt;     &lt;strong&gt;      &lt;strong&gt;       &lt;strong&gt;限流&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/td&gt;   &lt;td align="left" colspan="1" rowspan="2" valign="middle" width="403.3333333333333"&gt;    &lt;p&gt;限流是指对进入系统的请求进行限流处理，如果请求量超过了系统最大处理能力或者超过了开发者指定的处理能力，那么直接拒绝请求，通过这种丢弃部分请求的方式可以保证整个系统有一定的可用性，不至于让整个系统完全不可用。那么怎样判别超过最大处理能力呢？一般就是利用 QPS 来判别，如果 QPS 超过阈值，那么就直接拒绝请求。     &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;     &lt;br /&gt;&lt;/p&gt;    &lt;p&gt;限流有很多细节的策略，例如针对接口限流、针对服务限流、针对用户限流。&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="3" valign="middle" width="123"&gt;    &lt;strong&gt;     &lt;strong&gt;      &lt;strong&gt;       &lt;strong&gt;熔&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;    &lt;strong&gt;     &lt;strong&gt;      &lt;strong&gt;       &lt;strong&gt;断&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/td&gt;   &lt;td align="left" colspan="1" rowspan="3" valign="middle" width="403"&gt;熔断，断路（开路）的价值在于限制故障影响范围。一般希望控制、减少或中断和故障系统之间的通信，从而降低故障系统的负载，这有利于系统的恢复。一般系统的服务都会有很多下游依赖，如果下游依赖的服务出现问题，例如开始就超时甚至响应非常慢的话，如果不做任何处理，那么会导致整个请求都被卡住造成超时，那么系统的业务服务对外就无法提供任何正常的功能。    &lt;br /&gt;为此，熔断策略就可以解决这个问题，熔断就是当系统依赖的下游服务出现问题时，可以快速对其进行熔断（不发起请求），这样系统的业务服务至少可以提供部分功能。    &lt;br /&gt;熔断的设计至少需要包括：熔断请求判断机制算法、熔断恢复、熔断告警三部分。&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="1" valign="middle"&gt;    &lt;strong&gt;     &lt;strong&gt;      &lt;strong&gt;       &lt;strong&gt;降级&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/td&gt;   &lt;td align="left" colspan="1" rowspan="1" valign="middle"&gt;降级是指开发者划分好系统的核心功能和非核心功能，然后当系统超过最大处理能力之后，直接关闭掉非核心的功能，从而保证核心功能的可用。关闭非核心的功能后可以使系统释放部分资源，从而可以有资源来处理核心功能。&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;p&gt;熔断和降级这两个策略虽有些相似，字面的意思都是要快速拒绝请求。但是却是两个维度的设计：降级的目的是应对系统自身的故障，而熔断的目的是应对系统依赖的外部服务故障。&lt;/p&gt;
 &lt;h2&gt;04、存储层面&lt;/h2&gt;
 &lt;p&gt;当前的互联网时代，应用服务基本都是无状态的。因此应用服务的高可用相对来说会比较简单。但是数据存储的高可用相对来说，会复杂很多。因为数据是有状态的，那具体开发者要怎样保障数据存储的高可用。下文一起来分析下。&lt;/p&gt;
 &lt;p&gt;存储层面的高可用方案本质是通过数据的冗余来实现，将数据复制到多个存储介质里面，可以有效的避免数据丢失，同时还可以提高并发能力。因为数据是有状态的，这里会比服务的高可用要复杂很多。&lt;/p&gt;
 &lt;p&gt;常见的解决存储高可用的方案有两种：  &lt;strong&gt;集群存储和分布式存储&lt;/strong&gt;。业界大多是围绕这些来构建，或者是做相关衍生和扩展。下面展开讲解。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;4.1&lt;/strong&gt;   &lt;strong&gt;集群存储（集中式存储）&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;集群存储是指由若干个「通用存储设备」组成的用于存储的集群。组成集群存储的每个存储系统的性能和容量均可通过「集群」的方式得以叠加和扩展。&lt;/p&gt;
 &lt;p&gt;集群存储适合业务存储量规模一般的场景，常规的业务数据存储一般都是集群存储方式就足够了。现在一般业务数据存储的使用默认都是集群方式。比如 Redis、MySQL 等存储类型。一般中大型互联网公司默认是集群存储的方式。&lt;/p&gt;
 &lt;p&gt;集群存储就是常说的「 1 主多备」或者「 1 主多从」的架构。写数据通过主机，读数据一般通过从机。集群存储主要需要考虑如下几个问题：&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td align="left" colspan="1" rowspan="2" valign="middle" width="537.3333333333334"&gt;    &lt;ul&gt;     &lt;li&gt;主机如何将数据复制给备机（从机）？数据的写入都是通过主机，因此数据同步到备机（从机），就是要通过主机进行数据复制到备机（从机）。还需要考虑主备同步的时间延迟问题。      &lt;p&gt;       &lt;br /&gt;&lt;/p&gt;&lt;/li&gt;     &lt;li&gt;备机（从机）如何检测主机状态？      &lt;p&gt;       &lt;br /&gt;&lt;/p&gt;&lt;/li&gt;     &lt;li&gt;主机故障后，备机（从机）怎么切换为主机？主从架构中，如果主机发生故障，可直接将备机（从机）切换为主机。      &lt;p&gt;       &lt;br /&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h4&gt;  &lt;strong&gt;4.2&lt;/strong&gt;   &lt;strong&gt;分布式存储&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;分布式是指将不同的业务分布在不同的节点。分布式中的每一个节点，都可以做集群。&lt;/p&gt;
 &lt;p&gt;「分布式存储系统」是将数据分散存储在多台独立的设备上。传统的网络存储系统采用集中的存储服务器存放所有数据，存储服务器成为系统性能的瓶颈，也是可靠性和安全性的焦点，不能满足大规模存储应用的需要。&lt;/p&gt;
 &lt;p&gt;分布式网络存储系统采用可扩展的系统结构，利用多台存储服务器分担存储负荷，利用位置服务器定位存储信息。它不但提高了系统的可靠性、可用性和存取效率，还易于扩展。&lt;/p&gt;
 &lt;p&gt;分布式存储适合非常大规模的数据存储，业务数据量巨大的场景可以采用这种方式。常见的分布式存储比如 COS、GooseFS、Hadoop(HDFS)、HBase、Elasticsearch 等。&lt;/p&gt;
 &lt;h1&gt;05&lt;/h1&gt;
 &lt;p&gt;产品层面&lt;/p&gt;
 &lt;p&gt;产品层面的高可用架构解决方案，基本上就是指兜底产品策略。降级/限流的策略，更多的是从后端的业务服务和架构上的设计来考虑相关解决方案。这里说的兜底策略也可叫做「柔性降级策略」，更多的则是通过产品层面上来考虑。下面举几个例子：&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td valign="top" width="542"&gt;    &lt;ul&gt;     &lt;li&gt;      &lt;p&gt;当系统的页面获取不到数据时，或者无法访问时，要如何友好的告知用户。如「稍后重试」之类的方式。&lt;/p&gt;      &lt;p&gt;       &lt;br /&gt;&lt;/p&gt;&lt;/li&gt;     &lt;li&gt;      &lt;p&gt;当系统的真实的页面无法访问的时候，就需要产品提供一个默认页面，如果后端无法获取真实数据，那么直接渲染默认页面。&lt;/p&gt;      &lt;p&gt;       &lt;br /&gt;&lt;/p&gt;&lt;/li&gt;     &lt;li&gt;      &lt;p&gt;服务器需要停机维护，那么产品层面就会显示一个停机页面，所有用户只会弹出这个停机页面，不会请求后端服务。&lt;/p&gt;      &lt;p&gt;       &lt;br /&gt;&lt;/p&gt;&lt;/li&gt;     &lt;li&gt;      &lt;p&gt;抽奖商品显示一个默认兜底商品等等。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h2&gt;06、  &lt;strong&gt;运维部署层面&lt;/strong&gt;&lt;/h2&gt;
 &lt;h4&gt;  &lt;strong&gt;6.1&lt;/strong&gt;   &lt;strong&gt;开发阶段-灰度发布、接口测试设计&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;灰度发布、接口测试、接口拨测系列设计包括但不限于：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;strong&gt;灰度发布：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;服务发布上线的时候，要有一个灰度的过程。先灰度 1-2 个服务实例，然后逐步放量观察。如果一切 ok，再逐步灰度，直到所有实例发布完毕。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;strong&gt;接口测试：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;每次服务发布上线的时候，服务提供的各种接口，都要有接口测试用例。接口测试用例测试通过后，服务才能发布上线。这样目的是为了查看系统对外提供的接口是否能够正常运行，避免服务发布后才发现有问题。&lt;/p&gt;
 &lt;p&gt;灰度发布和接口测试，一般在大公司里面会有相关的 DevOps 流程来保证。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;6.2&lt;/strong&gt;   &lt;strong&gt;开发阶段-监控告警设计&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;监控告警的设计，对部分大公司来说不是问题。因为一定会有一些比较专门的人去做这种基础能力的建设，会有对应的配套系统，业务开发者只需要配置或使用即可。&lt;/p&gt;
 &lt;p&gt;那如果公司内部没有相关基础建设，就需要开发者分别来接入对应的系统，或者直接接入一些指标、链路、日志、事件等多数据支持、更加一体化的监控平台，比如腾讯云可观测平台。&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td align="center" valign="top" width="48.33333333333333"&gt;    &lt;strong&gt;系统&lt;/strong&gt;&lt;/td&gt;   &lt;td align="center" valign="top" width="471.3333333333333"&gt;    &lt;strong&gt;建设要求&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="2" valign="middle" width="48.33333333333333"&gt;    &lt;strong&gt;监控系统&lt;/strong&gt;&lt;/td&gt;   &lt;td align="left" colspan="1" rowspan="2" valign="middle" width="471.3333333333333"&gt;    &lt;p&gt;     &lt;strong&gt;一般在监控系统这方面的开源解决方案包括但不限于这些：&lt;/strong&gt;&lt;/p&gt;    &lt;p&gt;     &lt;strong&gt;ELK (Elasticsearch、Logstash、Kibana) 日志收集和分析：&lt;/strong&gt;我们的日志记录不能都本地存储，因为微服务化后，日志散落在很多机器上，因此必须要有一个远程日志记录的系统，ELK 是不二人选。&lt;/p&gt;    &lt;p&gt;     &lt;strong&gt;Prometheus 监控收集：&lt;/strong&gt;可以监控各种系统层面的指标，包括自定义的一些业务指标&lt;/p&gt;    &lt;p&gt;     &lt;strong&gt;OpenTracing 分布式全链路追踪：&lt;/strong&gt;一个请求的上下游这么多服务，怎么能够把一个请求的上下游全部串起来，那么就要依靠 OpenTracing，可以把一个请求下的所有链路都串起来并且有详细的记录。&lt;/p&gt;    &lt;p&gt;     &lt;strong&gt;OpenTelemetry 可观测系统标准：&lt;/strong&gt;最新的标准，大一统，集合了跟踪数据（Traces），指标数据（Metrics），日志数据（Logs）来观测分布式系统状态的能力。&lt;/p&gt;    &lt;p&gt;     &lt;br /&gt;&lt;/p&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;p&gt;     &lt;strong&gt;操作系统层的监控&lt;/strong&gt;：这里需要包含物理机和容器。常见的核心指标监控包括 CPU 使用率、内存占用率、磁盘 IO 和网络带宽等。&lt;/p&gt;    &lt;p&gt;     &lt;strong&gt;应用服务层的监控&lt;/strong&gt;：这里的指标会比较多，核心的比如主调请求量、被调请求量、接口成功率、接口失败率、响应时间（平均值、P99、P95 等）等。&lt;/p&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;/td&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="3" valign="middle" width="48.33333333333333"&gt;    &lt;strong&gt;告警系统&lt;/strong&gt;&lt;/td&gt;   &lt;td align="left" colspan="1" rowspan="3" valign="middle" width="471.3333333333333"&gt;这些系统接入完，还只是做到监控和统计，当出现问题时，还需要进行实时告警，因此要有一个实时告警系统，如果没有实时报警，系统运行异常后就无法快速感知，这样就无法快速处理，就会给使用者的业务带来重大故障和灾难。    &lt;br /&gt;告警设计需要包括：    &lt;strong&gt;实时性&lt;/strong&gt;：实现秒级监控。    &lt;br /&gt;    &lt;strong&gt;全&lt;/strong&gt;    &lt;strong&gt;面性&lt;/strong&gt;：覆盖所有系统业务。    &lt;br /&gt;    &lt;strong&gt;实用性&lt;/strong&gt;：预警分为多个级别。监控人员可以方便、实用地根据预警严重程度做出精确的决策。    &lt;br /&gt;    &lt;strong&gt;多样性&lt;/strong&gt;：预警方式提供推拉模式。包括短信，邮件，可视化界面，方便监控人员及时发现问题。&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;h4&gt;  &lt;strong&gt;6.3&lt;/strong&gt;   &lt;strong&gt;开发阶段-安全性、防攻击设计&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;安全性、防攻击设计的目的是为了防刷、防黑产、防黑客，避免被外部恶意攻击。一般有两个策略：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;在公司级别的流量入口做好统一的防刷和鉴权的能力，例如在统一接入层做好封装。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;在业务服务内部，做好相关的业务鉴权，比如登录态信息、例如增加业务鉴权的逻辑。&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h4&gt;  &lt;strong&gt;6.4&lt;/strong&gt;   &lt;strong&gt;部署阶段-多机房部署（容灾设计）&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;一般的高可用策略，都是针对一个机房内的服务层面来设计的，但是如果整个机房都不可用了，例如地震、火灾、光纤挖断等情况怎么办？这就需要系统的服务和存储能够进行容灾了。容灾的常见方案就是多机房部署。&lt;/p&gt;
 &lt;table&gt;  &lt;tr&gt;   &lt;td align="center" height="34" valign="top" width="108.33333333333333"&gt;    &lt;strong&gt;类型&lt;/strong&gt;&lt;/td&gt;   &lt;td align="center" height="34" valign="top" width="411.3333333333333"&gt;    &lt;strong&gt;解析&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="2" valign="middle" width="108.33333333333333"&gt;    &lt;strong&gt;服务的多机房部署&lt;/strong&gt;&lt;/td&gt;   &lt;td align="left" colspan="1" rowspan="2" valign="middle" width="411.3333333333333"&gt;服务的多机房部署比较容易。因为我们的服务都是无状态的，因此只要名字服务能够发现不同机房的服务，就可以实现调用。这里需要注意的是名字服务（或者说负载均衡服务）要能够有就近访问的能力。&lt;/td&gt;&lt;/tr&gt;  &lt;tr&gt;&lt;/tr&gt;  &lt;tr&gt;   &lt;td align="center" colspan="1" rowspan="1" valign="middle" width="60"&gt;    &lt;strong&gt;存储的多机房部署&lt;/strong&gt;&lt;/td&gt;   &lt;td align="center" colspan="1" rowspan="1" valign="middle" width="411.3333333333333"&gt;存储的多机房部署，这个会比较难搞一点，因为存储是有状态的，部署在不同的机房就涉及到存储的同步和复制问题。&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
 &lt;p&gt;条件不允许的情况下，保证多机房部署业务服务就可以了。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;6.5&lt;/strong&gt;   &lt;strong&gt;线上运行阶段-故障演练（混沌实验）&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;故障演练在大公司是一个常见的手段。在业界，Netflix 早在 2010 年就构建了混沌实验工具 Chaos Monkey。混沌实验工程对于提升复杂分布式系统的健壮性和可靠性发挥了重要作用。&lt;/p&gt;
 &lt;p&gt;简单的故障演练就是模拟机房断电、断网、服务挂掉等场景，然后看整个系统运行是否正常。系统就要参考混沌实验工程来进行详细的规划和设计，这个是一个相对较大的工程、效果较好，但是需要有大量人力去开发这种基础建设。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;6.6&lt;/strong&gt;   &lt;strong&gt;线上运行阶段-接口拨测系列设计&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;接口拨测和巡检类似，就是服务上线后，每隔一个固定时间（比如 5s）调用后端的各种接口，如果接口异常则进行告警。&lt;/p&gt;
 &lt;p&gt;针对接口拨测，一般也会有相关配套设施来提供相关的能力去实现。如果没有提供，那么开发者可以写一个接口拨测（巡检）的服务，定期去调用重要的接口。&lt;/p&gt;
 &lt;h2&gt;07、异常应急层面&lt;/h2&gt;
 &lt;p&gt;即使前面做了很多保障，也不一定招架住线上的各种异常情况。如果真出问题让系统的服务异常、无法提供服务，开发者还有最后一根救命稻草——那就是应急预案，将服务异常的损失降低到最小。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;应急预案是需要开发者事先规划好，业务系统在各个层级出现问题后第一时间怎么恢复，并制定好相关规则和流程。当出现异常状况后可以按照既有的流程去执行。这样避免出现问题后手忙脚乱导致事态扩大。&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8d51fc9691ac46c29fc99c2da3836132~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;最后，我们整理出本文的思维导图如上，供各位参考。总体来说，我们从研发规范层面、应用服务层面、存储层面、产品层面、运维部署层面、异常应急层面这六大层面，剖析了一个高可用系统的架构设计需要有哪些关键的设计和考虑。&lt;/p&gt;
 &lt;p&gt;以上便是本次分享的全部内容，如果您觉得内容有用，欢迎点赞、收藏，把内容分享给更多开发者。&lt;/p&gt;
 &lt;p&gt;-End-&lt;/p&gt;
 &lt;p&gt;原创作者｜吴德宝&lt;/p&gt;
 &lt;p&gt;技术责编｜吴德宝&lt;/p&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;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2ea4f441907e4d718975bd116eaf9216~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;最近微信改版啦，有粉丝反馈收不到小云的文章。&lt;/p&gt;
 &lt;p&gt;请关注「腾讯云开发者」并  &lt;strong&gt;点亮星标&lt;/strong&gt;，&lt;/p&gt;
 &lt;p&gt;周一三晚8点 和小云一起  &lt;strong&gt;涨(领)技(福)术(利)&lt;/strong&gt;！&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5049d2836e284966ab1b227a3262e794~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;你可能感兴趣的腾讯工程师作品&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;|   &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247590528&amp;idx=1&amp;sn=2cf6bbdb2d5a0f41810b63ece41ee667&amp;chksm=eaa978d0dddef1c6fe009f3c890feec575c31782f11c313760f6f1ba7e78ef857b7625ed629e&amp;scene=21#wechat_redirect"&gt;编程语言70年：谁是世界上最好的编程语言？&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;|   &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247588095&amp;idx=1&amp;sn=4e68b4a7e5e719dc4c28396feca08f4c&amp;chksm=eaa982afddde0bb92d5bffa73bce37fec0e64e4c79c3e4a46dd013bc3efe36f9de32d00e2db8&amp;scene=21#wechat_redirect"&gt;腾讯工程师聊 ChatGPT 技术「文集」&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;|   &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247591274&amp;idx=1&amp;sn=2b9cac5339190ffcc8e97cd135266d2e&amp;chksm=eaa97f3adddef62c519b0589b69fadab26af560848cfdb289d93e805381be938bc1042040342&amp;scene=21#wechat_redirect"&gt;一文揭秘微信游戏推荐系统&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;|   &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247582311&amp;idx=1&amp;sn=33949a7d43a4b6c088f5c506222112fe&amp;chksm=eaa99837ddde11214ec7e7c4ccfcb73435317dfda22702931ad946d185e44cc891414e8a71e5&amp;scene=21#wechat_redirect"&gt;微信全文搜索耗时降94%？我们用了这种方案&lt;/a&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247583332&amp;idx=1&amp;sn=646f9423bed5990f75c0d99e618c0fa6&amp;chksm=eaa99c34ddde15228c45f00fa6e8d07de8097dfa4c0fb2ba448288748dec534165ac6538168e&amp;scene=21#wechat_redirect"&gt;&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;技术盲盒：  &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247568617&amp;idx=1&amp;sn=d3409583764c4877964765a6b774b1de&amp;chksm=eaa9d6b9ddde5faff511c416033948f76b056b209df76c6eb12adfea3f618422297b9b11895b&amp;scene=21#wechat_redirect"&gt;前端&lt;/a&gt;｜  &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247568512&amp;idx=1&amp;sn=5a2e887c0ac511e9a4fe5cd68a388e48&amp;chksm=eaa9d6d0ddde5fc6376f1ffcc6e7b050fefded23d5b24c5f7b801885f509df06cd53d99f0a45&amp;scene=21#wechat_redirect"&gt;后端&lt;/a&gt;｜  &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247568656&amp;idx=1&amp;sn=98f7033418fc1fd7d019eeb18008b616&amp;chksm=eaa9d740ddde5e56aa0b7df55dc2f70c65f329d37246453c2b3316356f3f84cc9f87eb6b8db4&amp;scene=21#wechat_redirect"&gt;AI与算法&lt;/a&gt;｜  &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247568672&amp;idx=1&amp;sn=85e4b3e1c46289058398b216edb40941&amp;chksm=eaa9d770ddde5e669cfaa25c37887ae058c433e4296ca04f8ff5373184bc76d4420f1d2049a7&amp;scene=21#wechat_redirect"&gt;运维｜&lt;/a&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247568677&amp;idx=1&amp;sn=e95255553777c53d38cb1e64c1c16432&amp;chksm=eaa9d775ddde5e633a75d20eb484181c0e03cb6f8237a4141c599e4f13ad3af6748c5e8d1a9a&amp;scene=21#wechat_redirect"&gt;工程师文化&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;a href="https://mp.weixin.qq.com/s/V3gwQmHKUUqPhqniTgSVUA"&gt;阅读原文&lt;/a&gt;&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62678-%E5%B7%A5%E4%BD%9C-%E5%8D%81%E5%B9%B4-%E8%85%BE%E8%AE%AF</guid>
      <pubDate>Tue, 14 Mar 2023 10:48:05 CST</pubDate>
    </item>
    <item>
      <title>小程序是如何设计百亿级用户画像分析系统的？</title>
      <link>https://itindex.net/detail/62645-%E7%A8%8B%E5%BA%8F-%E8%AE%BE%E8%AE%A1-%E7%99%BE%E4%BA%BF</link>
      <description>&lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0e0f3a6a15564719a18ded052dc33237~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;导语 |&lt;/strong&gt; We 分析是微信小程序官方推出的、面向小程序服务商的数据分析平台，其中画像洞察是一个非常重要的功能模块。微信开发工程师钟文波将描述 We 分析画像系统各模块是如何设计，在介绍基础标签模块之后，重点讲解用户分群模块设计。希望相关的技术实现思路，能够对你有所启发。&lt;/p&gt;
 &lt;p&gt;目录&lt;/p&gt;
 &lt;p&gt;1 背景介绍&lt;/p&gt;
 &lt;p&gt;1.1 画像系统简述&lt;/p&gt;
 &lt;p&gt;1.2 画像系统设计目标&lt;/p&gt;
 &lt;p&gt;2 画像系统整体概述&lt;/p&gt;
 &lt;p&gt;3 基础标签模块&lt;/p&gt;
 &lt;p&gt;3.1 功能描述&lt;/p&gt;
 &lt;p&gt;3.2 技术实现&lt;/p&gt;
 &lt;p&gt;4 用户分群模块&lt;/p&gt;
 &lt;p&gt;4.1 功能描述&lt;/p&gt;
 &lt;p&gt;4.2 人群包实时预估&lt;/p&gt;
 &lt;p&gt;4.3 人群创建&lt;/p&gt;
 &lt;p&gt;4.4 人群跟踪应用&lt;/p&gt;
 &lt;p&gt;5 总结&lt;/p&gt;
 &lt;h2&gt;01、背景介绍&lt;/h2&gt;
 &lt;h4&gt;  &lt;strong&gt;1.1 画像系统简述&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;We 分析是小程序官方推出的、面向小程序服务商的数据分析平台，其中画像洞察是一个重要的功能模块。该功能将为使用者提供基础的画像标签分析能力，提供自定义的用户分群功能，从而满足更多个性化的分析需求及支撑更多的画像应用场景。&lt;/p&gt;
 &lt;p&gt;在此之前，原有 MP 的画像分析仅有基础画像，相当于只能分析小程序大盘固定周期的基础属性，而无法针对特定人群或自定义人群进行分析及应用。平台头部的使用者均希望平台提供完善的画像分析能力。除最基础的画像属性之外，也为使用者提供更丰富的标签及更灵活的用户分群应用能力。因此， We 分析在相关能力上计划进行优化。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;1.2 画像系统设计目标&lt;/strong&gt;&lt;/h4&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;易用性&lt;/strong&gt;：易用性主要指使用者在体验画像洞察功能的时候，不需要学习成本就能直接上手使用。使用者可以结合自身业务场景解决问题，做到开箱即用 0 门槛。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;稳定性&lt;/strong&gt;：稳定性指系统稳定可靠体验好。例如画像标签数据、人群包按时稳定产出，在交互使用过程中查询速度快，做到如丝般顺滑的手感。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;完备性&lt;/strong&gt;：指数据丰富、规则灵活、功能完善；支持丰富的人群圈选数据，预置标签、人群标签、平台行为、自定义上报行为等。做到在不违反隐私的情况下平台基本提供使用者想要的数据。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;整体来看，平台支持灵活的标签及人群创建方式，使用者按照自己的想法任意圈选出想要的人群，按不同周期手动或自动选出人群包。此外也支持人群的跟踪分析，人群在多场景的应用等。&lt;/p&gt;
 &lt;h2&gt;02、画像系统整体概述&lt;/h2&gt;
 &lt;p&gt;系统从  &lt;strong&gt;产品形态&lt;/strong&gt;的角度出发，在下文分成2个模块进行阐述——分别是  &lt;strong&gt;基础标签模块及用户分群模块&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/52a4f1c9f71842d88f7635b40455cb8d~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;多源数据&lt;/strong&gt;：数据源包括用户属性、人群标签、平台行为数据、自定义上报数据。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;画像加工&lt;/strong&gt;：主要是对用户属性、人群标签、平台行为，进行相应的 ETL（Extract Transform  Load ，提取转换加载）及预计算。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;人群计算&lt;/strong&gt;：根据使用者定义的用户分群规则，从多源数据中计算出对应的人群。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;画像导入&lt;/strong&gt;：画像及人群数据在 TWD 加工好后，从 TWD 分布式 HDFS 集群导入到线上的 TDSQL 、 ClickHouse 存储；其中，预计算的数据导入到线上 TDSQL 存储，用户行为等明细数据导入到线上 ClickHouse 集群中。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;画像服务&lt;/strong&gt;：提供在线的画像服务接口。其中标签管理使用通用配置系统，数据服务采用 RPC 框架开发，在上一层是平台的数据中间件。此处也统一做了流量控制、异步调用、调用监控、及参数安全校验。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;画像应用&lt;/strong&gt;：提供基础标签分析及针对特定人群的标签分析，另外还提供人群圈选跟踪分析及线上应用等。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;03、基础标签模块&lt;/h2&gt;
 &lt;h4&gt;  &lt;strong&gt;3.1 功能描述&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;该模块主要满足使用者对画像的基础分析需求，预期能满足绝大部分中长尾使用者对画像的使用深度要求。主要提供的是  &lt;strong&gt;针对小程序大盘的基础标签分析，及针对特定人群&lt;/strong&gt;（如活跃：1天活跃、7天活跃、30天活跃、180天活跃）  &lt;strong&gt;的特定标签分析&lt;/strong&gt;。如下所示：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f1a7641308fb4f9abc84f5ceb1f6f350~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;3.2 技术实现&lt;/strong&gt;&lt;/h4&gt;
 &lt;h4&gt;  &lt;strong&gt;3.2.1 数据计算&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;从上述功能的描述，可以看出功能的特点是官方定义数据范围可控，支持的是针对特定人群的特定标签分析。&lt;/p&gt;
 &lt;p&gt;针对特定人群的特定标签分析数据是用离线 T + 1 的 hive 任务进行计算。流程如下。&lt;/p&gt;
 &lt;p&gt;分别计算官方特定标签的统计数据、特定人群的统计数据，以及计算特定人群交叉特定标签的数据。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/406833e7bc544e9fb07c646e73c8aca0~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;3.2.2 数据存储&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;  &lt;strong&gt;不同存储对比存在差异&lt;/strong&gt;。在上述分析之后，需要存储的是预计算好的结果数据。此外，业务的特点是按照小程序进行多个数据主题统计的存储，所以第一直觉是适合用分布式 OLTP 存储。团队也对比了不同的数据库，  &lt;strong&gt;在选型过程中，主要考虑对比的点包括数据的写入、读取性能。&lt;/strong&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;写入&lt;/strong&gt;：包括是否可以支持快速的建表等 DDL 操作。平台数据指标多，例如 We 分析平台数据指标近千个。&lt;/p&gt;
   &lt;p&gt;不同的场景主题指标一般会分别进行计算，写入到不同的在线存储数据表中，所以这需要具备快速 DDL 及高效出库数据的能力。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;读取&lt;/strong&gt;：包括查询性能、读取接口是否简单灵活、开发是否简单；以及相关运维配套设施是否完善，如监控告警、扩容、权限、辅助优化等。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b80c70a3ef434a4cb8410f3400cb58b2~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h4&gt;从上图和 Datacube / FeatureKV / HBase 的对比可以发现 TDSQL 更符合此业务诉求、更具备优势。&lt;/h4&gt;
 &lt;h4&gt;&lt;/h4&gt;
 &lt;h4&gt;因此 We 分析平台基本所有的预计算结果数据，最终选用 TDSQL 来存储离线预计算结果数据，关于 TDSQL 的几个关键点如下：&lt;/h4&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;存储容量&lt;/strong&gt;：We 画像分析系统采用的 TDSQL 服务中，当前支持最大 64 个分片，每个分片最大 3 T ，单个实例最大能支持存储 192 T 的数据。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;数据出库&lt;/strong&gt;：通过数平 US 上的出库组件可以完成数据从 TDW 直接出库到 TDSQL ，近 1 亿数据量可以在 40 min + 完成出库，出库组件的监控及日志完善。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e03d52224c134b61aae6240163fca061~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;查询性能&lt;/strong&gt;：2 个分片，8 核 32 G 进行测试，查询某小程序一段时间数据，查询  QPS 5 W。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;读取方式&lt;/strong&gt;：通过 jdbc 连接查询，拼接不同 sql 进行查询，查询方式简单灵活。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;运维方面&lt;/strong&gt;：实例申请 、 账号设置 、 监控告警 、 扩容和慢查询分析等能力，都可以开发自助在云控制台完成。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;开发效率&lt;/strong&gt;：DDL 操作简单，数据开发从建表到出库基本没有学习成本，问题定位简单高效。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;当前整个平台的预计算数据出库到 TDSQL 的数据达到十亿级别，数据表超百张，实际使用存储上百 T 。TDSQL 整体功能较为全面，开发者仅需要补充开发数据生命周期管理工具，删除方式的注意点跟 MySQL 一样。&lt;/p&gt;
 &lt;p&gt;如果采用 KV 类型的引擎进行存储，需要根据 KV 的特性合理设计存储 Key 。在查询端对 Key 进行拼接组装，发送 BatchGet 请求进行查询。整个过程开发逻辑会相对繁复些， 需要更加注重 Key 的设计。若要实现一个只有概要数据的趋势图，那么存储的 Key 需要设计成类似格式：{日期} # {小程序} # {指标类型} 。&lt;/p&gt;
 &lt;h2&gt;04、用户分群模块&lt;/h2&gt;
 &lt;h4&gt;  &lt;strong&gt;4.1 功能描述&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;  &lt;strong&gt;该模块主要提供自定义的用户分群能力&lt;/strong&gt;。用户分群依据用户的属性及行为特征将用户群体进行分类，以便使用者对其进行观察分析及应用。自定义的用户分群能够满足中头部客户的个性化分析运营需求，例如客户想看上次 618 参加了某活动的用户人群，在接下来的活跃交易趋势跟大盘的差异对比；或者客户想验证对比某些人群对优惠券的敏感程度、圈选人群后通过 AB 实验进行验证。上述类似的应用会非常多。&lt;/p&gt;
 &lt;p&gt;在功能设计上，平台需要做到数据丰富、规则灵活、查询快速，需要支持丰富的人群圈选数据，并且预置标签、人群标签、平台行为、自定义上报行为等。支持灵活的标签及人群创建方式，让客户能按照自己的想法任意圈选出想要的人群，按不同周期手动或自动选出人群包，支持人群的跟踪分析、人群在多场景的应用能力。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;4.2 人群包实时预估&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;人群包实时预估是根据使用者客户定义的规则，计算出当前规则下有多少用户命中了该规则。产品交互通常如下所示：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/64fae313e0b24c569537ca5443211edb~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;4.2.1 数据加工&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;为了满足客户能随意根据自己的想法圈出想要的人群，平台支持丰富的数据源。整体画像的数据量较大，其中预置的标签画像在离线 HDFS 上的竖表存储达近万亿/天，平台行为百亿级/天，且维度细，自定义上报行为百亿级/天。&lt;/p&gt;
 &lt;p&gt;怎么设计能  &lt;strong&gt;节省存储同时加速查询&lt;/strong&gt;是重点考虑的问题之一。大体的思路是：对预置标签画像转成 Bitmap 进行压缩存储，对平台行为明细进行预聚合及对维度枚举值进行 ID 自增编码，字符串转成数据整型节省存储空间。同时在产品层面增加启用按钮，开通后导入近期数据，从而控制存储消耗，具体细节如下。&lt;/p&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;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c96b7967657d4895a1feb3b5a548b72a~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;例如活跃标签 10002 ，对标签的每个标签值进行编码如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7e8e7fbe1bf9436ab9ca3d675acb1362~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;对特定人群进行编码，基准人群是作为必选的过滤条件，用于限定用户的范围：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/efbfdfa671bc47bc875ff183ba34afcc~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&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;p&gt;采用大宽表方式的存储如 Elasticsearch 和 Hermes 存储，等待全部需要线上用到的画像标签在离线计算环节加工完成才能开始入库。而像 ClickHouse 、Doris 则可以采用与竖表相对应的表结构，标签加工完成就可以马上出库到线上集群，从而减小因为一个标签的延时而导致整体延时的风险。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;CREATE TABLE table_xxx(  
    ds BIGINT COMMENT &amp;apos;数据日期&amp;apos;,  
    label_name STRING COMMENT &amp;apos;标签名称&amp;apos;,  
    label_id BIGINT COMMENT &amp;apos;标签id&amp;apos;,  
    appid STRING COMMENT &amp;apos;小程序appid&amp;apos;,  
    useruin BIGINT COMMENT &amp;apos;useruin&amp;apos;,  
    tag_name STRING COMMENT &amp;apos;tag名称&amp;apos;,  
    tag_id BIGINT COMMENT &amp;apos;tag id&amp;apos;,  
    tag_value BIGINT COMMENT &amp;apos;tag权重值&amp;apos;  
)  
PARTITION BY LIST( ds )  
SUBPARTITION BY LIST( label_name )(  
    SUBPARTITION sp_xxx VALUES IN ( &amp;apos;xxx&amp;apos; ),  
    SUBPARTITION sp_xxxx VALUES IN ( &amp;apos;xxxx&amp;apos; )  
)
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;‍&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;strong&gt;第三，标签在线存储。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;如果把标签理解成对用户的分群，那么符合某个标签的某个取值的所有用户 ID（UInt类型） 就构成了一个个的人群。  &lt;strong&gt;Bitmap 是用于存储标签-用户的映射关系的、非常理想的数据结构&lt;/strong&gt;，最终需要的是构建出每个标签的每个取值所对应的 Bitmap。例如性别这个标签组，背后对应的是男性用户群和女性用户群。&lt;/p&gt;
 &lt;p&gt;性别标签：男 -&amp;gt; 男性用户人群包，女 →女性用户人群包。&lt;/p&gt;
 &lt;p&gt;平台行为数据是指官方进行上报的行为数据，例如访问、分享等行为数据，使用者不需要进行任何埋点等操作。团队主要是会对平台行为进行预聚合，计算同一维度下的 PV 数据，已减少后续数据的存储及计算量。&lt;/p&gt;
 &lt;p&gt;同时会对维度枚举值进行 ID 自增编码，目的是减少存储占用，写入以及读取性能；从效果来看，团队对可枚举类型进行字典 ID 编码对比原本字符类型能节省60%的线上存储空间，同时相同数据量条件下带来 2 倍查询速度提升。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a800189ce39d4af6a5a4ea762a376411~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;自定义上报数据是使用者自己埋点进行数据的上报，上报的内容包括公共参数及自定义内容，其中自定义内容是 key-value 的格式，在 OLAP 引擎中，团队会将客户自定义的内容转成 map 结构类型进行存储。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;4.2.2 数据写入存储&lt;/strong&gt;&lt;/h4&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;h4&gt;    &lt;strong&gt;在线OLAP存储选型：&lt;/strong&gt;&lt;/h4&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;首先讲下，在线 OLAP 存储选型。标签及行为明细数据的存储引擎选型对于画像系统至关重要，不同的存储引擎决定了系统不同的设计方式。业务团队调研了解到，行业内建设画像系统时有多种不同的存储方案。团队对常用的画像 OLAP 引擎做了对比，如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fa6f58b832f243f5b7c998b4ec5e486a~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;综合上述调研，  &lt;strong&gt;团队采用 ClickHouse 作为画像数据存储引擎&lt;/strong&gt;。在 ClickHouse 中使用 RoaringBitmap 作为 Bitmap 的解决方案。该方案支持丰富的 Bitmap 操作函数，可以十分灵活方便的判重和进行基数统计操作，如下所示。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/77225122a0a742a9b833e82c53274683~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;采用 RoaringBitmap（RBM）&lt;/strong&gt; 对稀疏位图进行压缩，可以减少内存占用并提高效率。该方案的核心思路是，将 32 位无符号整数按照高 16 位分桶，即最多可能有 216=65536 个桶，称为 container。存储数据时，按照数据的高 16 位找到 container （找不到则会新建一个），再将低 16 位放入 container 中。也就是说，一个 RBM 就是很多 container 的集合，具体参考高效压缩位图 RoaringBitmap 的原理与应用。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;h4&gt;    &lt;strong&gt;数据导入线上存储：&lt;/strong&gt;&lt;/h4&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;接下来讲讲，数据导入线上存储。在确定了采用什么存储引擎存储线上数据后，团队需要将离线集群的数据导入到线上存储。其中对于标签数据通常的做法是将原始明细的 id 数据直接导入到 ClickHouse 表中，再通过创建物化视图的方式构建 RBM 结构进行使用。&lt;/p&gt;
 &lt;p&gt;然而，业务明细数据非常大，每天近万亿。这样的导入方式给 ClickHouse 集群带来了很大资源开销。而通常业务团队处理大规模数据都是用 Spark 这样的离线计算框架来完成处理。  &lt;strong&gt;最后团队把预处理工作全部交给了 Spark 框架，这种方式大大的减少了写入的数据量，同时也减少了 ClickHosue 集群的处理压力。&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;具体步骤是 Spark 任务首先会按照 id 进行分片处理，然后对每个分片中标签的每个标签值生成一个 Bitmap ，保证定制的序列化方式与 ClickHouse 中的 RBM 兼容。其中通过 Spark 处理后的 Bitmap 转成 string 类型，然后写入到线上的标签表中，在表中业务团队定义了一个物化列字段，用于实际存储 Bitmap。在写入过程中会将序列化后的 Bitmap 字符串通过 base64Decode 函数转成 ClickHouse 中的 AggregateFunction (groupBitmap, UInt32) 数据结构。&lt;/p&gt;
 &lt;p&gt;具体表结构如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;CREATE TABLE xxxxx_table_local on CLUSTER xxx  
(  
    `ds` UInt32,  
    `appid` String,  
    `label_group_id` UInt64,  
          `label_id` UInt64,  
          `bucket_num` UInt32,  
    `base64rbm` String,  
         `rbm` AggregateFunction(groupBitmap, UInt32) MATERIALIZED base64Decode(base64rbm)  
)  
ENGINE = ReplicatedMergeTree(&amp;apos;/clickhouse/tables/{layer}-{shard}/xxx_table_local&amp;apos;, &amp;apos;{replica}&amp;apos;)  
PARTITION BY toYYYYMMDD(toDateTime(ds))  
ORDER BY (appid, label_group_id, label_id)  
TTL toDate(ds) + toIntervalDay(5)  
SETTINGS index_granularity = 16
&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;h4&gt;    &lt;strong&gt;存储占用问题：&lt;/strong&gt;&lt;/h4&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;值得关注的还有存储占用问题。标签类型数据用 Bitmap 类型存储，平台行为采用编码方式存储，存储占用大幅减少。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;4.2.3 数据查询&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;数据查询方式：  &lt;strong&gt;人群圈选过程中，如何保障大的APP查询、在复杂规则情况下的查询速度&lt;/strong&gt;？团队在导入过程中对预置画像、平台行为、自定义上报行为，均按相同分桶规则导入集群。这保证一个用户仅会在同一台机器，查询时始终进行本地表查询，避免进行分布式表查询。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5cfda53600a64aff99a6b2bc078589a3~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;对于查询性能的保障，团队始终保证所有查询均在  &lt;strong&gt;本地表&lt;/strong&gt;完成。上面已经介绍到数据在入库时，均会按照相同用户 ID 的 hash 分桶规则出库到相应的机器节点中。使用维度数字编码，测试数字编码后对比字符方式查询性能有2倍以上提升。对标签对应的人群转成 Bitmap 方式处理，用户的不同规则到最后都会转成针 Bitmap 的交并差补集操作。&lt;/p&gt;
 &lt;p&gt;对于平台行为，如果在用户用模糊匹配的情况下，会先查询维度 ID 映射表，将用户可见维度转化成维度编码 ID，后通过编码 ID 及规则构建查询 SQL。整个查询的核心逻辑是根据圈选规则组合不同查询语句，然后将不同子查询通过规则组合器最终判断该用户是否命中人群规则。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;基于rpc开发服务接口&lt;/strong&gt;：查询的服务接口采用 rpc 框架进行开发。&lt;/p&gt;
 &lt;p&gt;在数据服务的上一层是团队的数据中间件，统一做了流量控制、异步调用、调用监控及参数安全校验，特别是针对用户量较大的 app 在多规则查询时，耗时较大，因此业务团队配置了细粒度的流量控制，保障查询请求的有序及服务的稳定可用。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;查询性能数据&lt;/strong&gt;：不同 DAU 等级小程序查询性能。&lt;/h4&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d41c4b0238c141f9b919307ea0c727a9~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;从性能数据看，对用户量大的 app 来说，在规则非常多的情况下还是要大几十秒，等待这么长时间体验不佳。因此对于这部分用户量大的 app，业务团队采用的策略是抽样。通过抽样，速度能得到非常大的提升，并且预估的准确率误差不大，在可接受的范围内。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;4.3 人群创建&lt;/strong&gt;&lt;/h4&gt;
 &lt;h4&gt;  &lt;strong&gt;4.3.1 人群实时创建&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;人群包实时创建类似上面描述的人群大小实时预估，区别是在最后人群创建是需要将圈选的人群包用户明细写入到存储中，然后返回人群包的大小给到用户。同样是在本地表执行，生成的人群包写入到同一台机器中，保持分桶规则的一致。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;4.3.2 人群例行化创建&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;客户创建的例行化人群包，需要每天计算。  &lt;strong&gt;如何持续跟踪分析趋势，并且不会对集群造成过大的计算压力&lt;/strong&gt;？团队的做法利用离线超大规模计算的能力，在凌晨启动所有人群计算任务，从而减小对线上 ClickHouse 集群的计算压力。所有小程序客户创建的例行化人群包计算集中到凌晨的一个任务中进行，做到读一次数据，计算完成所有人群包，最大限度节省计算资源，详细的设计如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eb31cad7dea048c897a9b15cc53c8a73~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;首先，团队会先将  &lt;strong&gt;全量的数据&lt;/strong&gt;（标签属性数据+行为数据）  &lt;strong&gt;按照小程序粒度及选择的时间范围进行过滤&lt;/strong&gt;，保留有效的数据；&lt;/p&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;h4&gt;  &lt;strong&gt;4.4 人群跟踪应用&lt;/strong&gt;&lt;/h4&gt;
 &lt;h4&gt;  &lt;strong&gt;4.4.1 人群跟踪分析&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;在按照用户规则圈选出人群后，统一对人群进行常用指标（如活跃、交易等指标）的跟踪。整个过程用离线任务进行处理，会从在线存储中导出实时生成的人群包，以及离线批量生成的定时人群包，汇总一起，后关联对应指标表，输出到线上 OLTP 存储进行在线的查询分析。其中，导出在线人群包会在凌晨空闲时间进行，通过将人群 RBM 转成用户明细 ID。&lt;/p&gt;
 &lt;p&gt;具体方法为：arrayJoin(bitmapToArray(groupBitmapMergeState(rbm)))。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/32111f6d5e624008aeb68180b92239b1~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;4.4.2 人群基础分析&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;人群基础分析对一个自定义的用户分群进行基础标签的分析，如该人群的省份、城市、交易等标签分布。人群行为分析，分析该人群不同的事件行为等。&lt;/p&gt;
 &lt;h4&gt;  &lt;strong&gt;4.4.3 实验人群定向&lt;/strong&gt;&lt;/h4&gt;
 &lt;p&gt;在 AB 实验中的人群实验，使用者通过规则圈选出指定人群作为实验组（如想验证某地区的符合某条件的人群是否更喜欢参与该活动），跟对照组做相应指标的对比，以便验证假设。&lt;/p&gt;
 &lt;h2&gt;05‍、总结‍&lt;/h2&gt;
 &lt;p&gt;本篇回顾了 We 画像分析系统各模块的设计思路。在基础模块中，业务团队根据功能特性，选用了腾讯云 TDSQL 作为在线数据的存储引擎，将所有预计算数据都使用 TDSQL 进行存储。在人群分析模块中，为了实现灵活的人群创建、分析及应用，业务团队使用 ClickHouse 作为画像数据的存储引擎，根据该存储的特性进行上层服务的开发，以达到最优的性能。&lt;/p&gt;
 &lt;p&gt;后续，小程序 We 画像分析系统在产品能力上会持续丰富功能及体验，同时扩展更多的应用场景。以上是 We 画像分析系统模块设计与实现思路的全部内容，欢迎感兴趣的读者在评论区交流。&lt;/p&gt;
 &lt;p&gt;-End-&lt;/p&gt;
 &lt;p&gt;原创作者｜钟文波‍‍‍&lt;/p&gt;
 &lt;p&gt;技术责编｜钟文波、谢慧志‍‍‍‍‍‍&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;你可能感兴趣的腾讯工程师作品&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;|&lt;/strong&gt;   &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247588018&amp;idx=1&amp;sn=91639f3f2d83565ab320e92d0ab49616&amp;chksm=eaa982e2ddde0bf4f00be43de589a21d6f359e7efa185941276aa00a7141a2d89eafdb9e718b&amp;scene=21#wechat_redirect"&gt;ChatGPT深度解析：GPT家族进化史&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;|    &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247588095&amp;idx=1&amp;sn=4e68b4a7e5e719dc4c28396feca08f4c&amp;chksm=eaa982afddde0bb92d5bffa73bce37fec0e64e4c79c3e4a46dd013bc3efe36f9de32d00e2db8&amp;scene=21#wechat_redirect"&gt;&lt;/a&gt;&lt;/strong&gt;   &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247588095&amp;idx=1&amp;sn=4e68b4a7e5e719dc4c28396feca08f4c&amp;chksm=eaa982afddde0bb92d5bffa73bce37fec0e64e4c79c3e4a46dd013bc3efe36f9de32d00e2db8&amp;scene=21#wechat_redirect"&gt;腾讯工程师聊 ChatGPT 技术「文集」&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;|&lt;/strong&gt;   &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247582311&amp;idx=1&amp;sn=33949a7d43a4b6c088f5c506222112fe&amp;chksm=eaa99837ddde11214ec7e7c4ccfcb73435317dfda22702931ad946d185e44cc891414e8a71e5&amp;scene=21#wechat_redirect"&gt;微信全文搜索耗时降94%？我们用了这种方案&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;|    &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247583332&amp;idx=1&amp;sn=646f9423bed5990f75c0d99e618c0fa6&amp;chksm=eaa99c34ddde15228c45f00fa6e8d07de8097dfa4c0fb2ba448288748dec534165ac6538168e&amp;scene=21#wechat_redirect"&gt;&lt;/a&gt;&lt;/strong&gt;   &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247583332&amp;idx=1&amp;sn=646f9423bed5990f75c0d99e618c0fa6&amp;chksm=eaa99c34ddde15228c45f00fa6e8d07de8097dfa4c0fb2ba448288748dec534165ac6538168e&amp;scene=21#wechat_redirect"&gt;10w单元格滚动卡顿如何解决？腾讯文档的7个秘笈&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;技术盲盒：&lt;/strong&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247568617&amp;idx=1&amp;sn=d3409583764c4877964765a6b774b1de&amp;chksm=eaa9d6b9ddde5faff511c416033948f76b056b209df76c6eb12adfea3f618422297b9b11895b&amp;scene=21#wechat_redirect"&gt;前端&lt;/a&gt;  &lt;strong&gt;｜&lt;/strong&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247568512&amp;idx=1&amp;sn=5a2e887c0ac511e9a4fe5cd68a388e48&amp;chksm=eaa9d6d0ddde5fc6376f1ffcc6e7b050fefded23d5b24c5f7b801885f509df06cd53d99f0a45&amp;scene=21#wechat_redirect"&gt;后端&lt;/a&gt;  &lt;strong&gt;｜&lt;/strong&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247568656&amp;idx=1&amp;sn=98f7033418fc1fd7d019eeb18008b616&amp;chksm=eaa9d740ddde5e56aa0b7df55dc2f70c65f329d37246453c2b3316356f3f84cc9f87eb6b8db4&amp;scene=21#wechat_redirect"&gt;AI与算法&lt;/a&gt;  &lt;strong&gt;｜&lt;/strong&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247568672&amp;idx=1&amp;sn=85e4b3e1c46289058398b216edb40941&amp;chksm=eaa9d770ddde5e669cfaa25c37887ae058c433e4296ca04f8ff5373184bc76d4420f1d2049a7&amp;scene=21#wechat_redirect"&gt;运维   &lt;strong&gt;｜&lt;/strong&gt;&lt;/a&gt;  &lt;a href="http://mp.weixin.qq.com/s?__biz=MzI2NDU4OTExOQ==&amp;mid=2247568677&amp;idx=1&amp;sn=e95255553777c53d38cb1e64c1c16432&amp;chksm=eaa9d775ddde5e633a75d20eb484181c0e03cb6f8237a4141c599e4f13ad3af6748c5e8d1a9a&amp;scene=21#wechat_redirect"&gt;工程师文化&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;公众号后台回复“小程序”，领本文作者推荐的更多资料&lt;/p&gt;
 &lt;p&gt;  &lt;a href="https://mp.weixin.qq.com/s/9HPciYWiqsdxEv4-55urWQ"&gt;阅读原文&lt;/a&gt;&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62645-%E7%A8%8B%E5%BA%8F-%E8%AE%BE%E8%AE%A1-%E7%99%BE%E4%BA%BF</guid>
      <pubDate>Thu, 02 Mar 2023 10:18:00 CST</pubDate>
    </item>
    <item>
      <title>MySQL扛不住？B站千亿级点赞系统服务架构设计</title>
      <link>https://itindex.net/detail/62637-mysql-%E5%8D%83%E4%BA%BF-%E7%B3%BB%E7%BB%9F</link>
      <description>&lt;p&gt;为了在提供上述能力的前提下经受住流量、存储、容灾三大压力，点赞目前的系统实现方式如下：&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;   &lt;p&gt;    &lt;strong&gt; 1、系统架构简介&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2023/0224/20230224110233662.png"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;整个点赞服务的系统可以分为五个部分&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;流量路由层&lt;/strong&gt;（决定流量应该去往哪个机房）&lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;业务网关层&lt;/strong&gt;（统一鉴权、反黑灰产等统一流量筛选）&lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;点赞服务&lt;/strong&gt;（thumbup-service）,提供统一的RPC接口&lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;点赞异步任务&lt;/strong&gt;（thumbup-job）&lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;数据层&lt;/strong&gt;（db、kv、redis）&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;下文将重点分享下    &lt;strong&gt;数据存储层、点赞服务层（thumbup-service）&lt;/strong&gt;与     &lt;strong&gt;异步任务层（thumbup-job）&lt;/strong&gt;的系统设计&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;   &lt;p&gt;    &lt;strong&gt;2、三级数据存储&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;基本数据模型：&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;点赞记录表：记录用户在什么时间对什么实体进行了什么类型的操作(是赞还是踩，是取消点赞还是取消点踩)等&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;点赞计数表：记录被点赞实体的累计点赞（踩）数量&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;（1）第一层存储：DB层 - （TiDB）&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;点赞系统中最为重要的就是点赞记录表（likes）和点赞计数表（counts），负责整体数据的持久化保存，以及提供缓存失效时的回源查询能力。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;点赞记录表 - likes : 每一次的点赞记录（用户Mid、被点赞的实体ID（messageID）、点赞来源、时间）等信息，并且在Mid、messageID两个维度上建立了满足业务求的联合索引。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;点赞数表 - counts : 以业务ID（BusinessID）+实体ID(messageID)为主键，聚合了该实体的点赞数、点踩数等信息。并且按照messageID维度建立满足业务查询的索引。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;由于DB采用的是分布式数据库TiDB，所以对业务上无需考虑分库分表的操作&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;（2）第二层存储&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;缓存层Cache：点赞作为一个高流量的服务，缓存的设立肯定是必不可少的。点赞系统主要使用的是CacheAside模式。这一层缓存主要基于Redis缓存：以点赞数和用户点赞列表为例&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;①点赞数&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2023/0224/20230224110245667.png"&gt;&lt;/img&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;/ul&gt;  &lt;pre&gt;    &lt;code&gt;key-value= count:patten:{business_id}:{message_id} - {likes},{disLikes}&lt;/code&gt;    &lt;code&gt;用业务ID和该业务下的实体ID作为缓存的Key,并将点赞数与点踩数拼接起来存储以及更新&lt;/code&gt;&lt;/pre&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;②用户点赞列表&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2023/0224/20230224110254998.png"&gt;&lt;/img&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;pre&gt;    &lt;code&gt;key-value= user:likes:patten:{mid}:{business_id} - member(messageID)-score(likeTimestamp)&lt;/code&gt;    &lt;code&gt;* 用mid与业务ID作为key，value则是一个ZSet,member为被点赞的实体ID，score为点赞的时间。当改业务下某用户有新的点赞操作的时候，被点赞的实体则会通过 zadd的方式把最新的点赞记录加入到该ZSet里面来&lt;/code&gt;    &lt;code&gt;为了维持用户点赞列表的长度（不至于无限扩张），需要在每一次加入新的点赞记录的时候，按照固定长度裁剪用户的点赞记录缓存。该设计也就代表用户的点赞记录在缓存中是有限制长度的，超过该长度的数据请求需要回源DB查询&lt;/code&gt;&lt;/pre&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;（3）第三层存储&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;LocalCache - 本地缓存&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;本地缓存的建立，目的是为了应对缓存热点问题。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;利用最小堆算法，在可配置的时间窗口范围内，统计出访问最频繁的缓存Key,并将热Key（Value）按照业务可接受的TTL存储在本地内存中。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;其中热点的发现之前也有同步过：https://mp.weixin.qq.com/s/C8CI-1DDiQ4BC_LaMaeDBg&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;（4）针对TIDB海量历史数据的迁移归档&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;迁移归档的原因(初衷)，是为了减少TIDB的存储容量,节约成本的同时也多了一层存储，可以作为灾备数据。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;以下是在KV数据库（taishan）中点赞的数据与索引的组织形式：&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;①点赞记录&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt; &lt;/li&gt;&lt;/ul&gt;  &lt;pre&gt;    &lt;code&gt;1_{mid}_${business_id}_${type}_${message_id}=&amp;gt; {origin_id}_{mtime}&lt;/code&gt;&lt;/pre&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;②用户点赞列表索引&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2023/0224/20230224110305476.png"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt; &lt;/li&gt;&lt;/ul&gt;  &lt;pre&gt;    &lt;code&gt;2_{mid}_${business_id}_${type}_${mtime}_{message_id} =&amp;gt; {origin_id}&lt;/code&gt;&lt;/pre&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;③实体维度点赞记录索引&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2023/0224/20230224110315814.png"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt; &lt;/li&gt;&lt;/ul&gt;  &lt;pre&gt;    &lt;code&gt;3_{message_id}_${business_id}_${type}_${mtime}_${mid}=&amp;gt;{origin_id}&lt;/code&gt;&lt;/pre&gt;  &lt;p&gt; &lt;/p&gt;   &lt;p&gt;    &lt;strong&gt; 3、存储层的优化和思考&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;作为一个典型的大流量基础服务，点赞的存储架构需要最大程度上满足两个点：&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;（1）满足业务读写需求的同时具备最大的可靠性&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;（2）选择合适的存储介质与数据存储形态，最小化存储成本&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;从以上两点触发，考虑到KV数据在业务查询以及性能上都更契合点赞的业务形态，且TaiShan可以水平扩容来满足业务的增长。点赞服务从当前的关系型数据库（TiDB）+ 缓存（Redis）逐渐过渡至KV型数据库（Taishan）+ 缓存（Redis），以具备更强的可靠性。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;同时TaiShan作为公司自研的KV数据库，在成本上也能更优于使用TiDB存储。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;   &lt;p&gt;    &lt;strong&gt; 4、点赞服务层（thumbup-service）&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;作为面对C端流量的直接接口，在提供服务的同时，需要思考在面对各种未知或者可预知的灾难时，如何尽可能提供服务&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;存储（db、redis等）的容灾设计&lt;/strong&gt;（同城多活）&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;在DB的设计上，点赞服务有两地机房互为灾备，正常情况下，机房1承载所有写流量与部分读流量，机房2承载部分读流量。当DB发生故障时，通过db-proxy（sidercar）的切换可以将读写流量切换至备份机房继续提供服务。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2023/0224/20230224110326315.png"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2023/0224/20230224110336144.png"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;在缓存（Redis）上，点赞服务也拥有两套处于不同机房的集群，并且通过异步任务消费TiDB的binLog维护两地缓存的一致性。可以在需要时切换机房来保证服务的提供，而不会导致大量的冷数据回源数据库。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;服务的容灾与降级&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;（以点赞数、点赞状态、点赞列表为例），点赞作为一个用户强交互的社区功能服务，对于灾难发生时用户体验的保证是放在第一位的。所以针对重点接口，我们都会有兜底的数据作为返回。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;多层数据存储互为灾备&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;点赞的热数据在redis缓存中存有一份。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;kv数据库中存有全量的用户数据，当缓存不可用时，KV数据库会扛起用户的所有流量来提供服务。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;TIDB目前也存储有全量的用户数据，当缓存、KV均不可用时，tidb会依托于限流，最大程度提供用户数据的读写服务。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;因为存在多重存储，所以一致性也是业务需要衡量的点。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;首先写入到每一个存储都是有错误重试机制的，且重要的环节，比如点赞记录等是无限重试的。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;另外，在拥有重试机制的场景下，极少数的不同存储的数据不一致在点赞的业务场景下是可以被接受的&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;多地方机房互为灾备&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;点赞机房、缓存、数据库等都在不同机房有备份数据，可以在某一机房或者某地中间件发生故障时快速切换。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;点赞重点接口的降级&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;点赞数、点赞、列表查询、点赞状态查询等接口，在所有兜底、降级能力都已失效的前提下也不会直接返回错误给用户，而是会以空值或者假特效的方式与用户交互。后续等服务恢复时，再将故障期间的数据写回存储。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;   &lt;p&gt;    &lt;strong&gt; 5、异步任务层（thumbup-job）&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;异步任务主要作为点赞数据写入、刷新缓存、为下游其他服务发送点赞、点赞数消息等功能&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;首先是最重要的用户行为数据（点赞、点踩、取消等）的写入。搭配对数据库的限流组件以及消费速度监控，保证数据的写入不超过数据库的负荷的同时也不会出现数据堆积造成的C数据端查询延迟问题。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2023/0224/20230224110351401.png"&gt;&lt;/img&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;缓存刷新：点赞状态缓存、点赞列表缓存、点赞计数缓存&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;同步点赞消息&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;点赞事件异步消息、点赞计数异步消息&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;针对 WriteBack方式的思考&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;由于目前点赞系统异步处理能力或者说速率是能够满足业务的。所以当前写DB与写缓存都放在异步流程中。&lt;/p&gt;      &lt;p&gt; &lt;/p&gt;&lt;/li&gt;    &lt;li&gt;      &lt;p&gt;后续随着流量的增加，实施流程中写缓存，再由异步Job写入持久层相对来说是一个更好的方案。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;    &lt;strong&gt;点赞job对binLog的容灾设计&lt;/strong&gt;&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;由于点赞的存储为TiDB,且数据量较大。在实际生产情况中，binLog会偶遇数据延迟甚至是断流的问题。为了减少binLog数据延迟对服务数据的影响。服务做了以下改造。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;监控：&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;首先在运维层面、代码层面都对binLog的实时性、是否断流做了监控&lt;/p&gt;  &lt;p&gt; &lt;/p&gt;  &lt;ul&gt;    &lt;li&gt;      &lt;p&gt;        &lt;strong&gt;应对：&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;脱离binlog，由业务层（thumb-service）发送重要的数据信息（点赞数变更、点赞状态事件）等。当发生数据延迟时，程序会自动同时消费由thumbup-service发送的容灾消息，继续向下游发送。&lt;/p&gt;  &lt;p&gt; &lt;/p&gt; &lt;p&gt;三、未来规划&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;1、点赞服务单元化：要陆续往服务单元化的方向迭代、演进。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;2、点赞服务平台化：在目前的业务接入基础上增加迭代数据分库存储能力，做到服务、数据自定义隔离。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;3、点赞业务形态探索：以社区为基础，继续探索通过点赞衍生的业务形态。&lt;/p&gt;
    &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62637-mysql-%E5%8D%83%E4%BA%BF-%E7%B3%BB%E7%BB%9F</guid>
      <pubDate>Sun, 26 Feb 2023 14:57:21 CST</pubDate>
    </item>
    <item>
      <title>随机高并发查询结果一致性设计实践</title>
      <link>https://itindex.net/detail/62606-%E9%9A%8F%E6%9C%BA-%E5%B9%B6%E5%8F%91-%E7%BB%93%E6%9E%9C</link>
      <description>&lt;p&gt;  &lt;strong&gt;作者：京东物流 赵帅 姚再毅 王旭东 孟伟杰 孔祥东&lt;/strong&gt;&lt;/p&gt;
 &lt;h3&gt;1 前言&lt;/h3&gt;
 &lt;p&gt;物流合约中心是京东物流合同管理的唯一入口。为商家提供合同的创建，盖章等能力，为不同业务条线提供合同的定制，归档，查询等功能。由于各个业务条线众多，为各个业务条线提供高可用查询能力是物流合约中心重中之重。同时计费系统在每个物流单结算时，都需要查询合约中心，确保商家签署的合同内容来保证计费的准确性。&lt;/p&gt;
 &lt;h3&gt;2 业务场景&lt;/h3&gt;
 &lt;h5&gt;1.查询维度分析&lt;/h5&gt;
 &lt;p&gt;从业务调用的来源来看，合同的大部分是计费系统在每个物流单计费的时候，需要调用合约中心来判断，该商家是否签署合同。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="31" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/74b8aae612f64b40be8fa78a8a8298fa~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;从业务调用的入参来看，绝大部分是多个条件来查询合同，但基本都是查询某个商家，或通过商家的某个属性（例如业务账号）来查询合同。&lt;/p&gt;
 &lt;p&gt;从调用的结果来看，40%的查询是没有结果的，其中绝大部分是因为商家没有签署过合同，导致查询为空。其余的查询结果，每次返回的数量较少，一般一个商家只有3到5个合同。&lt;/p&gt;
 &lt;h5&gt;2.调用量分析&lt;/h5&gt;
 &lt;p&gt;调用量
目前合同的调用量，大概是在每天2000W次。&lt;/p&gt;
 &lt;p&gt;一天的调用量统计：
  &lt;img alt="32" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/34cc23f499ab4d3f9e36a4b42d5b4a41~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;调用时间
每天高峰期为上班时间，最高峰为4W/min。&lt;/p&gt;
 &lt;p&gt;一个月的调用量统计：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="33.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0761285e22f4e658b4f657928cd3a40~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;由上可以看出，合同每日的调用量比较平均，主要集中在9点到12点和13点到18点，也就是上班时间，整体调用量较高，基本不存在调用暴增的情况。&lt;/p&gt;
 &lt;p&gt;总体分析来看，合约中心的查询，调用量较高，且较平均，基本都是随机查询，也并不存在热点数据，其中无效查询占比较多，每次查询条件较多，返回数据量比不大。&lt;/p&gt;
 &lt;h3&gt;3 方案设计&lt;/h3&gt;
 &lt;p&gt;从整体业务场景分析来看，我们决定做三层防护来保证调用量的支撑，同时需要对数据一致性做好处理。第一层是布隆过滤器，来拦截绝大部分无效的请求。第二层是redis缓存数据，来保证各种查询条件的查询尽量命中redis。第三层是直接查询数据库的兜底方案。同时再保证数据一致性的问题，我们借助于广播mq来实现。
  &lt;img alt="35" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3f22a5dce7ba4edc817c47d0542a8ad6~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h5&gt;1.第一层防护&lt;/h5&gt;
 &lt;p&gt;由于近一半的查询都是空，我们首先这是缓存穿透的现象。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;缓存穿透问题&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;缓存穿透（cache penetration）是用户访问的数据既不在缓存当中，也不在数据库中。出于容错的考虑，如果从底层数据库查询不到数据，则不写入缓存。这就导致每次请求都会到底层数据库进行查询，缓存也失去了意义。当高并发或有人利用不存在的Key频繁攻击时，数据库的压力骤增，甚至崩溃，这就是缓存穿透问题。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;常规解决方案&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;缓存特定值&lt;/p&gt;
 &lt;p&gt;一般对于缓存穿透我们比较常规的做法就是，将不存在的key 设置一个固定值，比如说NULL,&amp;amp;&amp;amp;等等，在查询返回这个值的时候，我们应用就可以认为这是一个不存在的key,那我们应用就可以决定是否继续等待，还是继续访问，还是直接放弃，如果继续等待访问的话，设置一个轮询时间，再次请求，如果取到的值不再是我们预设的，那就代表已经有值了，从而避免了透传到数据库，从而把大量的类似请求挡在了缓存之中。&lt;/p&gt;
 &lt;p&gt;缓存特定值并同步更新&lt;/p&gt;
 &lt;p&gt;特定值做了缓存，那就意味着需要更多的内存存储空间。当存储层数据变化了，缓存层与存储层的数据会不一致。有人会说，这个问题，给key 加上一个过期时间不就可以了，确实，这样是最简单的，也能在一定程度上解决这两个问题，但是当并发比较高的时候（缓存并发），其实我是不建议使用缓存过期这个策略的，我更希望缓存一直存在；通过后台系统来更新缓存中的数据一致性的目的。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;布隆过滤器&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;布隆过滤器的核心思想是这样的，它不保存实际的数据，而是在内存中建立一个定长的位图用0,1来标记对应数据是否存在系统；过程是将数据经过多个哈希函数计算出不同的哈希值，然后用哈希值对位图的长度进行取模，最后得到位图的下标位，然后在对应的下标位上进行标记；找数的时候也是一样，先通过多个哈希函数得到哈希值，然后哈希值与位图的长度进行取模得到多个下标。如果多个下标都被标记成1了，那么说明数据存在于系统，不过只要有一个下标为0那么就说明该数据肯定不存在于系统中。&lt;/p&gt;
 &lt;p&gt;在这里先通过一个示例介绍一下布隆过滤器的场景：&lt;/p&gt;
 &lt;p&gt;以ID查询文章为例，如果我们要知道数据库是否存在对应的文章，那么最简单的方式就是我们把所有数据库存在的ID都保存到缓存去，这个时候当请求过进入系统，先从这个缓存数据里判断系统是否存在对应的数据ID，如果不存在的话直接返回出去，避免请求进入到数据库层，存在的话再从获取文章的信息。但是这个不是最好的方式，因为当文章的数量很多很多的时候，那缓存中就需要存大量的文档id而且只能持续增长，所以我们得想一种方式来节省内存资源当又能是请求都能命中缓存，这个就是布隆过滤器要做的。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="36" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b3e9d5fe277d4ce897ed46adb08d1b48~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;我们分析布隆过滤器的优缺点&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;优点&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;1.不需要存储数据，只用比特表示，因此在空间占用率上有巨大的优势
2.检索效率高，插入和查询的时间复杂度都为 O(K)（K 表示哈希函数的个数）
3.哈希函数之间相互独立，可以在硬件指令层次并行计算，因此效率较高。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;缺点&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;1.存在不确定的因素，无法判断一个元素是否一定存在，所以不适合要求 100% 准确率的场景
2.只能插入和查询元素，不能删除元素。&lt;/p&gt;
 &lt;p&gt;布隆过滤器分析：面对优点，完全符合我们的诉求，针对缺点1，会有极少的数据穿透对系统来说并无压力。针对缺点2，合同的数据，本来就是不可删除的。如果合同过期，我们可以查出单个商家的所有合同，从合同的结束时间来判断合同是否有效，并不需要取删除布隆过滤器里的元素。&lt;/p&gt;
 &lt;p&gt;考虑到调用redis布隆过滤器，会走一次网络，而我们的查询近一半都是无效查询，我们决定使用本地布隆过滤器，这样就可以减少一次网络请求。但是如果是本地布隆过滤器，在更新时，就需要对所有机器的本地布隆过滤器更新，我们监听合同的状态来更新，通过mq的广播模式，来对布隆过滤器插入元素，这样就做到了所有机器上的布隆过滤器统一元素插入。&lt;/p&gt;
 &lt;h5&gt;2.第二层防护&lt;/h5&gt;
 &lt;p&gt;面对高并发，我们首先想到的是缓存。&lt;/p&gt;
 &lt;p&gt;引入缓存，我们就要考虑缓存穿透，缓存击穿，缓存雪崩的三大问题。&lt;/p&gt;
 &lt;p&gt;其中缓存穿透，我们已再第一层防护中处理，这里只解决缓存击穿，缓存雪崩的问题。&lt;/p&gt;
 &lt;p&gt;缓存击穿（Cache Breakdown）缓存雪崩是指只大量热点key同时失效的情况，如果是单个热点key，在不停的扛着大并发，在这个key失效的瞬间，持续的大并发请求就会击破缓存，直接请求到数据库，好像蛮力击穿一样。这种情况就是缓存击穿。&lt;/p&gt;
 &lt;p&gt;常规解决方案&lt;/p&gt;
 &lt;p&gt;缓存失效分散&lt;/p&gt;
 &lt;p&gt;这个问题其实比较好解决，就是在设置缓存的时效时间的时候增加一个随机值，例如增加一个1-3分钟的随机，将失效时间分散开，降低集体失效的概率；把过期时间控制在系统低流量的时间段,比如凌晨三四点，避过流量的高峰期。&lt;/p&gt;
 &lt;p&gt;加锁&lt;/p&gt;
 &lt;p&gt;加锁，就是在查询请求未命中缓存时，查询数据库操作前进行加锁，加锁后后面的请求就会阻塞，避免了大量的请求集中进入到数据库查询数据了。&lt;/p&gt;
 &lt;p&gt;永久不失效&lt;/p&gt;
 &lt;p&gt;我们可以不设置过期时间来保证缓存永远不会失效，然后通过后台的线程来定时把最新的数据同步到缓存里去&lt;/p&gt;
 &lt;p&gt;解决方案：使用分布式锁，针对同一个商家，只让一个线程构建缓存，其他线程等待构建缓存执行完毕，重新从缓存中获取数据。&lt;/p&gt;
 &lt;p&gt;缓存雪崩（Cache Avalanche）当缓存中大量热点缓存采用了相同的实效时间，就会导致缓存在某一个时刻同时实效，请求全部转发到数据库，从而导致数据库压力骤增，甚至宕机。从而形成一系列的连锁反应，造成系统崩溃等情况，这就是缓存雪崩。&lt;/p&gt;
 &lt;p&gt;解决方案：缓存雪崩的解决方案是将key的过期设置为固定时间范围内的一个随机数，让key均匀的失效即可。&lt;/p&gt;
 &lt;p&gt;我们考虑使用redis缓存，因为每次查询的条件都不一样，返回的结果数据又比较少，我们考虑限制查询都必须有一个固定的查询条件，商家编码。如果查询条件中没有查商家编码，我们可以通过商家名称，商家业务账号这些条件来反查查商家编码。&lt;/p&gt;
 &lt;p&gt;这样我们就可以缓存单个商家编码的所有合同，然后再通过代码使用filter对其他查询条件做支持，避免不同的查询条件都去缓存数据而引发的缓存数据更新，缓存数据淘汰已经缓存数据一致等问题。&lt;/p&gt;
 &lt;p&gt;同时只缓存单个商家编码的所有合同，缓存的数据量也是可控，每个缓存的大小也可控，基本不会出现redis大key的问题。&lt;/p&gt;
 &lt;p&gt;引入缓存，我们就要考虑缓存数据一致性的问题。&lt;/p&gt;
 &lt;p&gt;有关缓存一致性问题，可自行百度，这个就不在叙述。&lt;/p&gt;
 &lt;p&gt;如图所示 对于商家编码维度的缓存数据，我们通过监听合同的状态，使用mq广播来删除对应商家的缓存，从而避免出现缓存和数据一致性的相关问题。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="38" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1ff1b92ef70c450199df9d739bba10af~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;3.第三层防护&lt;/h3&gt;
 &lt;p&gt;第三层防护，自然是数据库，如果有查询经过了第一层和第二层，那我们需要直接查询数据库来返回结果，同时，我们对直接调用到数据库的线程进行监控。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="37" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/53a66aeb185e41a58b5b3591738634ce~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;为避免一些未知的查询大量查询涌入，导致数据库调用保证的问题，尤其是大促时，我们可以提前对数据库里的所有商家合同进行提前缓存。在缓存时，为避免缓存雪崩问题，我们对将key的过期设置为固定时间范围内的一个随机数，让key均匀的失效。&lt;/p&gt;
 &lt;p&gt;同时，为避免依然存在意外的情况，有大量查询涌入。我们通过ducc开关控制数据库的查询，如调用量太高导致无法支撑，则直接关闭数据库的调用，保证数据库不会直接宕机导致整个业务不可用。&lt;/p&gt;
 &lt;h3&gt;4 总结&lt;/h3&gt;
 &lt;p&gt;本文主要分析了面对高并发调用的调用场景设计及的技术方案，在引入缓存的同时，也要考虑实际的调用入参及结果，面对增加的网络请求，是否可以进一步减少。面对redis缓存，是否可以通过一些手段避免所有查询条件都需要缓存，带来的缓存爆炸，缓存淘汰策略等问题，以及解决缓存与数据一致等一系列问题。&lt;/p&gt;
 &lt;p&gt;本方案是根据具体的查询业务场景设计具体的技术方案，针对不同的业务场景，对应的技术方案也是不一样的。&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62606-%E9%9A%8F%E6%9C%BA-%E5%B9%B6%E5%8F%91-%E7%BB%93%E6%9E%9C</guid>
      <pubDate>Wed, 01 Feb 2023 15:44:10 CST</pubDate>
    </item>
    <item>
      <title>浅谈服务接口的高可用设计</title>
      <link>https://itindex.net/detail/62589-%E6%9C%8D%E5%8A%A1-%E6%8E%A5%E5%8F%A3-%E8%AE%BE%E8%AE%A1</link>
      <description>&lt;h5&gt;作者：京东零售 王磊&lt;/h5&gt;
 &lt;h3&gt;前言&lt;/h3&gt;
 &lt;blockquote&gt;
  &lt;p&gt;作为一个后端研发人员，开发服务接口是我正常不过的工作了，这些接口不管是面向前端HTTP或者是供其他服务RPC远程调用的，都绕不开一个共同的话题就是“高可用”，接口开发往往看似简单，但保证高可用这块实现起来却不并没有想想的那么容易，接下来我们就看一下，一个高可用的接口是该考虑哪些内容，同时文中有不足的欢迎批评指正。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h3&gt;到底啥是高可用&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;用一句简单的话来概就是我们的系统具不具备应对和规避风险的能力。
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;为啥做高可用&lt;/h3&gt;
 &lt;pre&gt;  &lt;code&gt;1. 程序都是有人开发的，在开发过程中会犯错从而导致线上事故的发生
2. 系统运行依赖各种运行环境：CPU、内存、硬盘、网络等等，而这些都有可能损坏
3. 业务拉新用户正在注册账号，结果注册接口挂了用户体验受影响
4. 双十一、618等大促大量用户下单，结果下单服务接口挂了GMV受影响等等
5. 其他未知因素等等
总之为了应对这些不可控因素的发生，我们必须要做高可用
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;高可用的关键点&lt;/h3&gt;
 &lt;blockquote&gt;
  &lt;p&gt;我们说过高可用的本质是系统是否具备应对和规避风险的能力，那么从这个角度出发来设计高可用接口的有以下几个关键因素：Dependence（依赖）、Probability（概率）、Time（时长）、Scope（范围）&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;1. 依赖的资源相对少
2. 风险的概率足够低
3. 影响的范围足够小
4. 影响时长足够短
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;接口高可用设计的几个原则&lt;/h3&gt;
 &lt;blockquote&gt;
  &lt;p&gt;结合这些关键点，我们来看一下具体具体注意事项&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h4&gt;1、控制依赖&lt;/h4&gt;
 &lt;blockquote&gt;
  &lt;p&gt;能少依赖就少依赖，能不强依赖就不强依赖&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;少依赖
例如：日常每分钟10个请求，查询Mysql数据即可满足，此时盲目引入Redis中间件，不仅浪费资源而且增加系统复杂性

弱依赖
例如：用户注册服务强依赖新用户优惠券发放服务，当优惠券发放服务故障后，整个注册不可用，好的方式是采用弱依赖，使用异步化的
方式，这样优惠券发送服务不可用时，不会影响注册链路。
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;2、避免单点&lt;/h4&gt;
 &lt;blockquote&gt;
  &lt;p&gt;避免单点故障的核心是通过备份或者冗余快速的进行容错&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;1. 我们采用多机房多实力部署我们应用来保障故障风险分摊，一旦有一台服务器出现问题，其他服务仍然能够继续支撑我们的服务
2. 每次上线我们都会保留上一次上线发布版本，这样一旦上线的程序出现问题我们能够快速回滚到上一版本
3. 每个接口至少保障2人知道相关业务，一旦线上服务出现问题，其中任何一人一个能够快速处理相关线上问题
4. 不管是Mysql还是Redis等中间件都支持数据主备机群部署

类似的例子很多这里就不再一一列举了
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;3、负载均衡&lt;/h4&gt;
 &lt;blockquote&gt;
  &lt;p&gt;将风险进行分摊避免分险扩散&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;例如：无论是Ngnix或者JSF的，其负载均衡目的就是尽量的将流量分散到不同的服务器节点上，这样可以有效的保障单节点因系统瓶颈
问题而引发一系列的风险。 

像上面这个例子我想每个研发人员都知道也都会这么做，但是是不是所有的场景我们都考虑到均衡这个问题？

例如：通常为了提高读并发的能力，我们会把数据缓存到JIMDB中，但是因为缓存的key出现了热点数据导致JIMDB单分片负载过高，恰
好，这个分片上也缓存了其他数据，但是因为CPU负载过高，导致查询性能变差，大量的超时，影响了业务。所以，我们在接口设计
的时候，假如遇到类似场景，也要充分考虑数据存储的均衡性，同时针对热点数据做好监控，随时支持动态均衡。
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;4、资源隔离&lt;/h4&gt;
 &lt;blockquote&gt;
  &lt;p&gt;隔离的目的将风险控制在可控范围内，避免风险扩散&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;例如：接口部署之间服务部署物理上是相互隔离的，避免单机房或者单服务器出现故障影响整个服务

例如：我们在存储业务数据的时候会将数据分库分表，数据通过不同分片存储，这样就不会导致某个服务器挂掉影响到整个服务
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;5、接口限流&lt;/h4&gt;
 &lt;blockquote&gt;
  &lt;p&gt;限流是一种保护措施，目的是将风险控制在可控范围内&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;我们在开发接口的时候，一定要结合业务流量情况进行限流措施，限流一方面处于对自身服务资源的保护，同时也是对依赖资源的一种
保护措施。

目前集团JSF在流量控制这块已经有了对应的限流处理能力，同时我们也可以结合实际业务进行限流模块的开发。
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;6、服务熔断&lt;/h4&gt;
 &lt;blockquote&gt;
  &lt;p&gt;熔断也是一种保护措施，目的是将风险控制在可控范围内，避免风险扩散&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;例如：经常我们服务A会同时调用B、C、D多个服务，当我们依赖的服务其中一个出现故障或者性能下降的时候，就是导致整体服务
可用率下降，所以我们在开发此类服务的时候，一定要注意接口之间的隔离。我们可以利用类似Hystrix组件实现，也可以借助DUCC
进行手动隔离。

其实熔断也是一种控制资源依赖的一种，将强依赖降级为弱依赖
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;7、异步处理&lt;/h4&gt;
 &lt;blockquote&gt;
  &lt;p&gt;将同步操作转为异步操作&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;例如：用户页面领取一些权益，针对领取这个服务在大促期间因为用户流量较大，为了避免系统负载，此时采用MQ异步接收用户领取
请求然后进行优惠券发放,这样不仅极大的减少了事故的影响范围，也减少问题发生概率。
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;8、降级方案&lt;/h4&gt;
 &lt;blockquote&gt;
  &lt;p&gt;服务降级属于一种问题发生后的补救措施，通过服务降级可以减少一部分风险影响范围&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;对于重要的服务接口我们都要具备完善的降级方案，这里需要说明的是，降级有损的，我们一定要在系统开发前就要考虑各种问题
发生的可能，降级的前提是通过降级非核心业务保证核心业务运行。

例如：大促峰值期间，一般会提前降级掉很多功能，同时限流，主要是为了保护峰值绝大部分人的交易支付体验。
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;9、灰度发布&lt;/h4&gt;
 &lt;blockquote&gt;
  &lt;p&gt;通过灰度发布降低风险影响范围&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;例如：我们上线一个新服务，通过一定的灰度策略，让用户先行体验新版本的应用，通过收集这部分用户对新版本应用的反馈以及
对新版本功能、性能、稳定性等指标进行评论，进而决定继续放大新版本投放范围直至全量升级或回滚至老版本。根据线上反馈结果，
做到查漏补缺，发现重大问题，可回滚“旧版本”
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;10、混沌工程&lt;/h4&gt;
 &lt;blockquote&gt;
  &lt;p&gt;通过提前对系统进行一些破坏性的手段，提前发现潜在问题&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;例如：一个复杂接口系统依赖了太多的服务和组件，这些组件随时随地都可能会发生故障，而一旦它们发生故障，会不会如蝴蝶效应
一般造成整体服务不可用呢，我们并不知道，因此我们可以借助泰山平台混沌工程进行演练，针对发生的场景制定各种预案，将风险
控制在可控范围内。
&lt;/code&gt;&lt;/pre&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62589-%E6%9C%8D%E5%8A%A1-%E6%8E%A5%E5%8F%A3-%E8%AE%BE%E8%AE%A1</guid>
      <pubDate>Fri, 13 Jan 2023 09:35:58 CST</pubDate>
    </item>
    <item>
      <title>工作流引擎架构设计</title>
      <link>https://itindex.net/detail/62583-%E5%B7%A5%E4%BD%9C%E6%B5%81-%E5%BC%95%E6%93%8E-%E6%9E%B6%E6%9E%84</link>
      <description>&lt;p&gt;  &lt;strong&gt;原文链接：&lt;/strong&gt;   &lt;a href="https://mp.weixin.qq.com/s/z2lbTDl5G0fcwlGB7jCMAg"&gt;工作流引擎架构设计&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;最近开发的安全管理平台新增了很多工单申请流程需求，比如加白申请，开通申请等等。最开始的两个需求，为了方便，也没多想，就直接开发了对应的业务代码。&lt;/p&gt;
 &lt;p&gt;但随着同类需求不断增多，感觉再这样写可要累死人，于是开始了工作流引擎的开发之路。查找了一些资料之后，开发了现阶段的工作流引擎，文章后面会有介绍。&lt;/p&gt;
 &lt;p&gt;虽然现在基本上能满足日常的需求，但感觉还不够智能，还有很多的优化空间，所以正好借此机会，详细了解了一些完善的工作流引擎框架，以及在架构设计上需要注意的点，形成了这篇文章，分享给大家。&lt;/p&gt;
 &lt;h2&gt;什么是工作流&lt;/h2&gt;
 &lt;p&gt;先看一下维基百科对于工作流的定义：&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;工作流（Workflow），是对工作流程及其各操作步骤之间业务规则的抽象、概括描述。工作流建模，即将工作流程中的工作如何前后组织在一起的逻辑和规则，在计算机中以恰当的模型表达并对其实施计算。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;工作流要解决的主要问题是：为实现某个业务目标，利用计算机在多个参与者之间按某种预定规则自动传递文档、信息或者任务。&lt;/p&gt;
 &lt;p&gt;简单来说，工作流就是对业务的流程化抽象。WFMC（工作流程管理联盟） 给出了工作流参考模型如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="1.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/61e112e98e754d85bb8084099ceb6b87~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;举一个例子，比如公司办公的 OA 系统，就存在大量的申请审批流程。而在处理这些流程时，如果每一个流程都对应一套代码，显然是不现实的，这样会造成很大程度上的代码冗余，而且开发工作量也会骤增。&lt;/p&gt;
 &lt;p&gt;这个时候就需要一个业务无关的，高度抽象和封装的引擎来统一处理。通过这个引擎，可以灵活配置工作流程，并且可以自动化的根据配置进行状态变更和流程流转，这就是工作流引擎。&lt;/p&gt;
 &lt;h2&gt;简单的工作流&lt;/h2&gt;
 &lt;p&gt;那么，一个工作流引擎需要支持哪些功能呢？&lt;/p&gt;
 &lt;p&gt;这个问题并没有一个标准答案，需要根据实际的业务场景和需求来分析。在这里，我通过一个工单流程的演进，从简单到复杂，循序渐进地介绍一下都需要包含哪些基础功能。&lt;/p&gt;
 &lt;h3&gt;最简单流程&lt;/h3&gt;
 &lt;p&gt;  &lt;img alt="base-flow.drawio.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88e0c22ab26f42e3a0c30d4455c3c63b~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;最简单的一个流程工单，申请人发起流程，每个节点审批人逐个审批，最终流程结束。&lt;/p&gt;
 &lt;h3&gt;会签&lt;/h3&gt;
 &lt;p&gt;  &lt;img alt="countersign-flow.drawio.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3759a564ecd446afa2883e566bd2cb56~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在这个过程中，节点分成了两大类：简单节点和复杂节点。&lt;/p&gt;
 &lt;p&gt;简单节点处理逻辑不变，依然是处理完之后自动到下一个节点。复杂节点比如说会签节点，则不同，需要其下的所有子节点都处理完成，才能到下一个节点。&lt;/p&gt;
 &lt;h3&gt;并行&lt;/h3&gt;
 &lt;p&gt;  &lt;img alt="parallel-flow.drawio.png" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a8b4e97797714724a10c21d08bb7b9f5~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;同样属于复杂节点，其任何一个子节点处理完成后，都可以进入到下一个节点。&lt;/p&gt;
 &lt;h3&gt;条件判断&lt;/h3&gt;
 &lt;p&gt;  &lt;img alt="condition-flow.drawio.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/143fad10604c44e7a7cd1039c2d84ea7~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;需要根据不同的表单内容进入不同的分支流程。&lt;/p&gt;
 &lt;p&gt;举一个例子，比如在进行休假申请时，请假一天需要直属领导审批，如果大于三天则需要部门领导审批。&lt;/p&gt;
 &lt;h3&gt;动态审批人&lt;/h3&gt;
 &lt;p&gt;  &lt;img alt="approver-flow.drawio.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3802e8d53923405582d6ed2a5e0aa4f9~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;审批节点的审批人需要动态获取，并且可配置。&lt;/p&gt;
 &lt;p&gt;审批人的获取方式可以分以下几种：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;固定审批人&lt;/li&gt;
  &lt;li&gt;从申请表单中获取&lt;/li&gt;
  &lt;li&gt;根据组织架构，动态获取&lt;/li&gt;
  &lt;li&gt;从配置的角色组或者权限组中获取&lt;/li&gt;
&lt;/ol&gt;
 &lt;h3&gt;撤销和驳回&lt;/h3&gt;
 &lt;p&gt;  &lt;img alt="reject-flow.drawio.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/62c6845c20de42ba9611811a4d7de84e~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;节点状态变更可以有申请人撤回，审批人同意，审批人驳回。那么在驳回时，可以直接驳回到开始节点，流程结束，也可以到上一个节点。更复杂一些，甚至可以到前面流程的任意一个节点。&lt;/p&gt;
 &lt;h3&gt;自动化节点&lt;/h3&gt;
 &lt;p&gt;  &lt;img alt="auto-flow.drawio.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/68bd344074f949a190e7f6f9a5351412~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;有一些节点是不需要人工参与的，比如说联动其他系统自动处理，或者审批节点有时间限制，超时自动失效。&lt;/p&gt;
 &lt;h3&gt;个性化通知&lt;/h3&gt;
 &lt;p&gt;节点审批之后，可以配置不同的通知方式来通知相关人。&lt;/p&gt;
 &lt;p&gt;以上是我列举的一些比较常见的需求点，还有像加签，代理，脚本执行等功能，如果都实现的话，应该会是一个庞大的工作量。当然了，如果目标是做一个商业化产品的话，功能还是需要更丰富一些的。&lt;/p&gt;
 &lt;p&gt;但把这些常见需求点都实现的话，应该基本可以满足大部分的需求了，至少对于我们系统的工单流程来说，目前是可以满足的。&lt;/p&gt;
 &lt;h2&gt;工作流引擎对比&lt;/h2&gt;
 &lt;p&gt;既然这是一个常见的需求，那么需要我们自己来开发吗？市面上有开源项目可以使用吗？&lt;/p&gt;
 &lt;p&gt;答案是肯定的，目前，市场上比较有名的开源流程引擎有 Osworkflow、Jbpm、Activiti、Flowable、Camunda 等等。其中：Jbpm、Activiti、Flowable、Camunda 四个框架同宗同源，祖先都是 Jbpm4，开发者只要用过其中一个框架，基本上就会用其它三个了。&lt;/p&gt;
 &lt;h3&gt;Osworkflow&lt;/h3&gt;
 &lt;p&gt;Osworkflow 是一个轻量化的流程引擎，基于状态机机制，数据库表很少。Osworkflow 提供的工作流构成元素有：步骤（step）、条件（conditions）、循环（loops）、分支（spilts）、合并（joins）等，但不支持会签、跳转、退回、加签等这些操作，需要自己扩展开发，有一定难度。&lt;/p&gt;
 &lt;p&gt;如果流程比较简单，Osworkflow 是一个很不错的选择。&lt;/p&gt;
 &lt;h3&gt;JBPM&lt;/h3&gt;
 &lt;p&gt;JBPM 由 JBoss 公司开发，目前最高版本是 JPBM7，不过从 JBPM5 开始已经跟之前不是同一个产品了，JBPM5 的代码基础不是 JBPM4，而是从 Drools Flow 重新开始的。基于 Drools Flow 技术在国内市场上用的很少，所有不建议选择 JBPM5 以后版本。&lt;/p&gt;
 &lt;p&gt;JBPM4 诞生的比较早，后来 JBPM4 创建者 Tom Baeyens 离开 JBoss，加入 Alfresco 后很快推出了新的基于 JBPM4 的开源工作流系统 Activiti，另外 JBPM 以 hibernate 作为数据持久化 ORM 也已不是主流技术。&lt;/p&gt;
 &lt;h3&gt;Activiti&lt;/h3&gt;
 &lt;p&gt;Activiti 由 Alfresco 软件开发，目前最高版本 Activiti7。Activiti 的版本比较复杂，有 Activiti5、Activiti6、Activiti7 几个主流版本，选型时让人晕头转向，有必要先了解一下 Activiti 这几个版本的发展历史。&lt;/p&gt;
 &lt;p&gt;Activiti5 和 Activiti6 的核心 leader 是 Tijs Rademakers，由于团队内部分歧，在 2017 年 Tijs Rademakers 离开团队，创建了后来的 Flowable。Activiti6 以及 Activiti5 代码已经交接给了 Salaboy 团队，Activiti6 以及 Activiti5 的代码官方已经暂停维护了。&lt;/p&gt;
 &lt;p&gt;Salaboy 团队目前在开发 Activiti7 框架，Activiti7 内核使用的还是 Activiti6，并没有为引擎注入更多的新特性，只是在 Activiti 之外的上层封装了一些应用。&lt;/p&gt;
 &lt;h3&gt;Flowable&lt;/h3&gt;
 &lt;p&gt;Flowable 是一个使用 Java 编写的轻量级业务流程引擎，使用 Apache V2 license 协议开源。2016 年 10 月，Activiti 工作流引擎的主要开发者离开 Alfresco 公司并在 Activiti 分支基础上开启了 Flowable 开源项目。基于 Activiti v6 beta4 发布的第一个 Flowable release 版本为 6.0。&lt;/p&gt;
 &lt;p&gt;Flowable 项目中包括 BPMN（Business Process Model and Notation）引擎、CMMN（Case Management Model and Notation）引擎、DMN（Decision Model and Notation）引擎、表单引擎（Form Engine）等模块。&lt;/p&gt;
 &lt;p&gt;相对开源版，其商业版的功能会更强大。以 Flowable6.4.1 版本为分水岭，大力发展其商业版产品，开源版本维护不及时，部分功能已经不再开源版发布，比如表单生成器（表单引擎）、历史数据同步至其他数据源、ES 等。&lt;/p&gt;
 &lt;h3&gt;Camunda&lt;/h3&gt;
 &lt;p&gt;Camunda 基于 Activiti5，所以其保留了 PVM，最新版本 Camunda7.15，保持每年发布两个小版本的节奏，开发团队也是从 Activiti 中分裂出来的，发展轨迹与 Flowable 相似，同时也提供了商业版，不过对于一般企业应用，开源版本也足够了。&lt;/p&gt;
 &lt;p&gt;以上就是每个项目的一个大概介绍，接下来主要对比一下 Jbpm、Activiti、Flowable 和 Camunda。只看文字的话可能对它们之间的关系还不是很清楚，所以我画了一张图，可以更清晰地体现每个项目的发展轨迹。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="jbpm-history.drawio.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1fffd248ea04416b8804665676d5208e~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;那么，如果想要选择其中一个项目来使用的话，应该如何选择呢？我罗列了几项我比较关注的点，做了一张对比表格，如下：&lt;/p&gt;
 &lt;table&gt;

  &lt;tr&gt;
   &lt;th&gt;&lt;/th&gt;
   &lt;th&gt;Activiti 7&lt;/th&gt;
   &lt;th&gt;Flowable 6&lt;/th&gt;
   &lt;th&gt;Camunda&lt;/th&gt;
   &lt;th&gt;JBPM 7&lt;/th&gt;
&lt;/tr&gt;


  &lt;tr&gt;
   &lt;td&gt;流程协议&lt;/td&gt;
   &lt;td&gt;BPMN2.0、XPDL、PDL&lt;/td&gt;
   &lt;td&gt;BPMN2.0、XPDL、XPDL&lt;/td&gt;
   &lt;td&gt;BPMN2.0、XPDL、XPDL&lt;/td&gt;
   &lt;td&gt;BPMN2.0&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;商业和开源版&lt;/td&gt;
   &lt;td&gt;开源&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;开发基础&lt;/td&gt;
   &lt;td&gt;JBPM4&lt;/td&gt;
   &lt;td&gt;Activiti 5 &amp;amp; 6&lt;/td&gt;
   &lt;td&gt;Activiti 5&lt;/td&gt;
   &lt;td&gt;版本 5 之后 Drools Flow&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;数据库&lt;/td&gt;
   &lt;td&gt;Oracle、SQL Server、MySQL&lt;/td&gt;
   &lt;td&gt;Oracle、SQL Server、MySQL、postgre&lt;/td&gt;
   &lt;td&gt;Oracle、SQL Server、MySQL、postgre&lt;/td&gt;
   &lt;td&gt;MySQL，postgre&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;架构&lt;/td&gt;
   &lt;td&gt;spring boot 2&lt;/td&gt;
   &lt;td&gt;spring boot 1.5&lt;/td&gt;
   &lt;td&gt;spring boot 2&lt;/td&gt;
   &lt;td&gt;Kie&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;独立运行和内嵌&lt;/td&gt;
   &lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;流程设计器&lt;/td&gt;
   &lt;td&gt;AngularJS&lt;/td&gt;
   &lt;td&gt;AngularJS&lt;/td&gt;
   &lt;td&gt;bpmn.js&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;相对活跃&lt;/td&gt;
   &lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;表数量&lt;/td&gt;
   &lt;td&gt;引入 25 张表&lt;/td&gt;
   &lt;td&gt;引入 47 张表&lt;/td&gt;
   &lt;td&gt;引入 19 张表&lt;/td&gt;
   &lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;jar 包数量&lt;/td&gt;
   &lt;td&gt;引入 10 个 jar&lt;/td&gt;
   &lt;td&gt;引入 37 个 jar&lt;/td&gt;
   &lt;td&gt;引入 15 个 jar&lt;/td&gt;
   &lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;

&lt;/table&gt;
 &lt;h2&gt;Flowable 应用举例&lt;/h2&gt;
 &lt;p&gt;如果选择使用开源项目来开发自己的引擎，或者嵌入到现有的项目中，应该如何使用呢？这里通过 Flowable 来举例说明。&lt;/p&gt;
 &lt;p&gt;使用 Flowable 可以有两种方式，分别是内嵌和独立部署方式，现在来分别说明：&lt;/p&gt;
 &lt;h3&gt;内嵌模式&lt;/h3&gt;
 &lt;h4&gt;创建 maven 工程&lt;/h4&gt;
 &lt;p&gt;先建一个普通的 maven 工程，加入 Flowable 引擎的依赖以及 h2 内嵌数据库的依赖，也可以使用 MySQL 数据库来做持久化。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;!-- https://mvnrepository.com/artifact/org.flowable/flowable-engine --&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.flowable&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;flowable-engine&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;6.7.2&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;com.h2database&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;h2&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;1.4.192&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;创建流程引擎实例&lt;/h4&gt;
 &lt;pre&gt;  &lt;code&gt;import org.flowable.engine.ProcessEngine;
import org.flowable.engine.ProcessEngineConfiguration;
import org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration;

public class HolidayRequest {

  public static void main(String[] args) {
    ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration()
      .setJdbcUrl(&amp;quot;jdbc:h2:mem:flowable;DB_CLOSE_DELAY=-1&amp;quot;)
      .setJdbcUsername(&amp;quot;sa&amp;quot;)
      .setJdbcPassword(&amp;quot;&amp;quot;)
      .setJdbcDriver(&amp;quot;org.h2.Driver&amp;quot;)
      .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE);

    ProcessEngine processEngine = cfg.buildProcessEngine();
  }

}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;接下来，我们就可以往这个引擎实例上部署一个流程 xml。比如，我们想建立一个员工请假流程：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot;?&amp;gt;
&amp;lt;definitions xmlns=&amp;quot;http://www.omg.org/spec/BPMN/20100524/MODEL&amp;quot;
             xmlns:xsi=&amp;quot;http://www.w3.org/2001/XMLSchema-instance&amp;quot;
             xmlns:activiti=&amp;quot;http://activiti.org/bpmn&amp;quot;
             typeLanguage=&amp;quot;http://www.w3.org/2001/XMLSchema&amp;quot;
             expressionLanguage=&amp;quot;http://www.w3.org/1999/XPath&amp;quot;
             targetNamespace=&amp;quot;http://www.flowable.org/processdef&amp;quot;&amp;gt;

    &amp;lt;process id=&amp;quot;holidayRequest&amp;quot; name=&amp;quot;Holiday Request&amp;quot; isExecutable=&amp;quot;true&amp;quot;&amp;gt;

        &amp;lt;startEvent id=&amp;quot;startEvent&amp;quot;/&amp;gt;
        &amp;lt;sequenceFlow sourceRef=&amp;quot;startEvent&amp;quot; targetRef=&amp;quot;approveTask&amp;quot;/&amp;gt;

&amp;lt;!--        &amp;lt;userTask id=&amp;quot;approveTask&amp;quot; name=&amp;quot;Approve or reject request&amp;quot;/&amp;gt;--&amp;gt;
        &amp;lt;userTask id=&amp;quot;approveTask&amp;quot; name=&amp;quot;Approve or reject request&amp;quot; activiti:candidateGroups=&amp;quot;managers&amp;quot;/&amp;gt;

        &amp;lt;sequenceFlow sourceRef=&amp;quot;approveTask&amp;quot; targetRef=&amp;quot;decision&amp;quot;/&amp;gt;

        &amp;lt;exclusiveGateway id=&amp;quot;decision&amp;quot;/&amp;gt;
        &amp;lt;sequenceFlow sourceRef=&amp;quot;decision&amp;quot; targetRef=&amp;quot;externalSystemCall&amp;quot;&amp;gt;
            &amp;lt;conditionExpression xsi:type=&amp;quot;tFormalExpression&amp;quot;&amp;gt;
                &amp;lt;![CDATA[
          ${approved}
        ]]&amp;gt;
            &amp;lt;/conditionExpression&amp;gt;
        &amp;lt;/sequenceFlow&amp;gt;
        &amp;lt;sequenceFlow sourceRef=&amp;quot;decision&amp;quot; targetRef=&amp;quot;sendRejectionMail&amp;quot;&amp;gt;
            &amp;lt;conditionExpression xsi:type=&amp;quot;tFormalExpression&amp;quot;&amp;gt;
                &amp;lt;![CDATA[
          ${!approved}
        ]]&amp;gt;
            &amp;lt;/conditionExpression&amp;gt;
        &amp;lt;/sequenceFlow&amp;gt;

        &amp;lt;serviceTask id=&amp;quot;externalSystemCall&amp;quot; name=&amp;quot;Enter holidays in external system&amp;quot;
                     activiti:class=&amp;quot;org.example.CallExternalSystemDelegate&amp;quot;/&amp;gt;
        &amp;lt;sequenceFlow sourceRef=&amp;quot;externalSystemCall&amp;quot; targetRef=&amp;quot;holidayApprovedTask&amp;quot;/&amp;gt;

&amp;lt;!--        &amp;lt;userTask id=&amp;quot;holidayApprovedTask&amp;quot; name=&amp;quot;Holiday approved&amp;quot;/&amp;gt;--&amp;gt;
        &amp;lt;userTask id=&amp;quot;holidayApprovedTask&amp;quot; name=&amp;quot;Holiday approved&amp;quot; activiti:assignee=&amp;quot;${employee}&amp;quot;/&amp;gt;

        &amp;lt;sequenceFlow sourceRef=&amp;quot;holidayApprovedTask&amp;quot; targetRef=&amp;quot;approveEnd&amp;quot;/&amp;gt;

        &amp;lt;serviceTask id=&amp;quot;sendRejectionMail&amp;quot; name=&amp;quot;Send out rejection email&amp;quot;
                     activiti:class=&amp;quot;org.flowable.SendRejectionMail&amp;quot;/&amp;gt;
        &amp;lt;sequenceFlow sourceRef=&amp;quot;sendRejectionMail&amp;quot; targetRef=&amp;quot;rejectEnd&amp;quot;/&amp;gt;

        &amp;lt;endEvent id=&amp;quot;approveEnd&amp;quot;/&amp;gt;

        &amp;lt;endEvent id=&amp;quot;rejectEnd&amp;quot;/&amp;gt;

    &amp;lt;/process&amp;gt;

&amp;lt;/definitions&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;此 xml 是符合 bpmn2.0 规范的一种标准格式，其对应的流程图如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="2.png" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f0901613415a46dbbde894f9fb8f58e7~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;接下来，我们就把这个文件传给流程引擎，让它基于该文件，创建一个工作流。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;RepositoryService repositoryService = processEngine.getRepositoryService();
Deployment deployment = repositoryService.createDeployment()
  .addClasspathResource(&amp;quot;holiday-request.bpmn20.xml&amp;quot;)
  .deploy();
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;创建后，实际就写到内存数据库 h2 了，我们还可以把它查出来：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
  .deploymentId(deployment.getId())
  .singleResult();
System.out.println(&amp;quot;Found process definition : &amp;quot; + processDefinition.getName());
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;创建工作流实例&lt;/h4&gt;
 &lt;p&gt;创建工作流实例，需要提供一些输入参数，比如我们创建的员工请假流程，参数就需要：员工姓名、请假天数、事由等。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;Scanner scanner= new Scanner(System.in);

System.out.println(&amp;quot;Who are you?&amp;quot;);
String employee = scanner.nextLine();

System.out.println(&amp;quot;How many holidays do you want to request?&amp;quot;);
Integer nrOfHolidays = Integer.valueOf(scanner.nextLine());

System.out.println(&amp;quot;Why do you need them?&amp;quot;);
String description = scanner.nextLine();


RuntimeService runtimeService = processEngine.getRuntimeService();

Map&amp;lt;String, Object&amp;gt; variables = new HashMap&amp;lt;String, Object&amp;gt;();
variables.put(&amp;quot;employee&amp;quot;, employee);
variables.put(&amp;quot;nrOfHolidays&amp;quot;, nrOfHolidays);
variables.put(&amp;quot;description&amp;quot;, description);
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;参数准备好后，就可以传给工作流了：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;ProcessInstance processInstance =
    runtimeService.startProcessInstanceByKey(&amp;quot;holidayRequest&amp;quot;, variables);
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;此时，就会根据流程定义里的：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;userTask id=&amp;quot;approveTask&amp;quot; name=&amp;quot;Approve or reject request&amp;quot; activiti:candidateGroups=&amp;quot;managers&amp;quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;创建一个任务，任务有个标签，就是   &lt;code&gt;candidateGroups&lt;/code&gt;，这里的   &lt;code&gt;managers&lt;/code&gt;，可以猜得出，是给   &lt;code&gt;managers&lt;/code&gt; 建了个审批任务。&lt;/p&gt;
 &lt;h4&gt;查询并审批任务&lt;/h4&gt;
 &lt;p&gt;基于 manager 查询任务：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;TaskService taskService = processEngine.getTaskService();
List&amp;lt;Task&amp;gt; tasks = taskService.createTaskQuery().taskCandidateGroup(&amp;quot;managers&amp;quot;).list();
System.out.println(&amp;quot;You have &amp;quot; + tasks.size() + &amp;quot; tasks:&amp;quot;);
for (int i=0; i&amp;lt;tasks.size(); i++) {
  System.out.println((i+1) + &amp;quot;) &amp;quot; + tasks.get(i).getName());
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;审批任务：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;boolean approved = scanner.nextLine().toLowerCase().equals(&amp;quot;y&amp;quot;);
variables = new HashMap&amp;lt;String, Object&amp;gt;();
variables.put(&amp;quot;approved&amp;quot;, approved);
taskService.complete(task.getId(), variables);
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这里就是把全局变量   &lt;code&gt;approved&lt;/code&gt;，设为了   &lt;code&gt;true&lt;/code&gt;，然后提交给引擎。引擎就会根据这里的变量是   &lt;code&gt;true&lt;/code&gt; 还是   &lt;code&gt;false&lt;/code&gt;，选择走不同分支。如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;sequenceFlow sourceRef=&amp;quot;decision&amp;quot; targetRef=&amp;quot;externalSystemCall&amp;quot;&amp;gt;
    &amp;lt;conditionExpression xsi:type=&amp;quot;tFormalExpression&amp;quot;&amp;gt;
        &amp;lt;![CDATA[
  ${approved}
]]&amp;gt;
    &amp;lt;/conditionExpression&amp;gt;
&amp;lt;/sequenceFlow&amp;gt;
&amp;lt;sequenceFlow sourceRef=&amp;quot;decision&amp;quot; targetRef=&amp;quot;sendRejectionMail&amp;quot;&amp;gt;
    &amp;lt;conditionExpression xsi:type=&amp;quot;tFormalExpression&amp;quot;&amp;gt;
        &amp;lt;![CDATA[
  ${!approved}
]]&amp;gt;
    &amp;lt;/conditionExpression&amp;gt;
&amp;lt;/sequenceFlow&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
 &lt;h4&gt;回调用户代码&lt;/h4&gt;
 &lt;p&gt;审批后，就会进入下一个节点：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;&amp;lt;serviceTask id=&amp;quot;externalSystemCall&amp;quot; name=&amp;quot;Enter holidays in external system&amp;quot;
             activiti:class=&amp;quot;org.example.CallExternalSystemDelegate&amp;quot;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这里有个   &lt;code&gt;class&lt;/code&gt;，就是需要我们自己实现的：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="3.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fff7646f8f9d4942bad40c9a9b9f96db~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;最后，流程就走完结束了。&lt;/p&gt;
 &lt;h3&gt;REST API 模式&lt;/h3&gt;
 &lt;p&gt;上面介绍的方式是其作为一个 jar，内嵌到我们的程序里。创建引擎实例后，由我们业务程序去驱动引擎的运行。引擎和业务代码在同一个进程里。&lt;/p&gt;
 &lt;p&gt;第二种方式，Flowable 也可以作为一个独立服务运行，提供 REST API 接口，这样的话，非 Java 语言开发的系统就也可以使用该引擎了。&lt;/p&gt;
 &lt;p&gt;这个只需要我们下载官方的 zip 包，里面有个 rest 的 war 包，可以直接放到 tomcat 里运行。&lt;/p&gt;
 &lt;h4&gt;部署工作流&lt;/h4&gt;
 &lt;p&gt;在这种方式下，如果要实现上面举例的员工请假流程，可以通过调接口来实现：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="4.png" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/742fb0e5d9064bde8067c5a5ea29cb39~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;启动工作流：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="5.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bb98a6175d39414699f08f3328b56d15~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;其他接口就不一一展示了，可以参考官方文档。&lt;/p&gt;
 &lt;h3&gt;通过页面进行流程建模&lt;/h3&gt;
 &lt;p&gt;截止到目前，创建工作流程都是通过建立 xml 来实现的，这样还是非常不方便的。因此，系统也提供了通过页面可视化的方式来创建流程，使用鼠标拖拽相应组件即可完成。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="6.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e32810f7ae41455a81076d89cd5b6374~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;但是体验下来还是比较辛苦的，功能很多，名词更多，有很多都不知道是什么意思，只能不断尝试来理解。&lt;/p&gt;
 &lt;h2&gt;开源 VS 自研&lt;/h2&gt;
 &lt;p&gt;既然已经有成熟的开源产品了，还需要自研吗？这算是一个老生常谈的问题了。那到底应该如何选择呢？其实并不困难，归根结底就是要符合自身的业务特点，以及实际的需求。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="open-self.drawio.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/73f094929f0e4d6a9c1561db6564c297~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&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;p&gt;  &lt;strong&gt;开源劣势：&lt;/strong&gt;&lt;/p&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;p&gt;产品核心技术掌控程度高，可以更好的贴着业务需求做，可以定制的更好，基于上述两点，通常更容易做到良好的性能表现。  &lt;strong&gt;一句话总结，量身定制。&lt;/strong&gt;&lt;/p&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;p&gt;基于以上的分析，再结合我们自身业务，我总结了以下几点可供参考：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;开源项目均为 Java 技术栈，而我们使用 Python 和 Go 比较多，技术栈不匹配&lt;/li&gt;
  &lt;li&gt;开源项目功能丰富，而我们业务相对简单，使用起来比较重&lt;/li&gt;
  &lt;li&gt;开源项目并非开箱即用，需要结合业务特点做定制开发，学习成本和维护成本比较高&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;综上所述，我觉得自研更适合我们现阶段的产品特点。&lt;/p&gt;
 &lt;h2&gt;工作流引擎架构设计&lt;/h2&gt;
 &lt;p&gt;如果选择自研，架构应该如何设计呢？有哪些比较重要的模块和需要注意的点呢？下面来详细说说。&lt;/p&gt;
 &lt;h3&gt;BPMN&lt;/h3&gt;
 &lt;p&gt;BPMN 全称是 Business Process Model And Notation，即业务流程模型和符号。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="7.png" src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/07c5d27d94df4091a66648388992c20c~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;可以理解成一种规范，在这个规范里，哪些地方用空心圆，哪些地方用矩形，哪些地方用菱形，都是有明确定义的。&lt;/p&gt;
 &lt;p&gt;也就是说，只要是基于这个规范开发的系统，其所创建的流程就都是可以通用的。&lt;/p&gt;
 &lt;p&gt;其实，如果只是开发一个内部系统，不遵守这个规范也没有问题。但要是做一个产品的话，为了通用性更强，最好还是遵守这个规范。&lt;/p&gt;
 &lt;h3&gt;流程设计器&lt;/h3&gt;
 &lt;p&gt;对于工作流引擎来说，流程设计器的选型至关重要，它提供了可视化的流程编排能力，决定了用户体验的好坏。&lt;/p&gt;
 &lt;p&gt;目前主流的流程设计器有 Activiti-Modeler，mxGraph，bpmn-js 等，下面来做一个简单介绍。&lt;/p&gt;
 &lt;h4&gt;Activiti-Modeler&lt;/h4&gt;
 &lt;p&gt;Activiti 开源版本中带了 Web 版流程设计器，在 Activiti-explorer 项目中有 Activiti-Modeler，优点是集成简单，开发工作量小，缺点是界面不美观，用户体验差。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="8.jpeg" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9ff5430dc58c438e9f11c9dcb2e6e91a~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h4&gt;mxGraph&lt;/h4&gt;
 &lt;p&gt;mxGraph 是一个强大的 JavaScript 流程图前端库，可以快速创建交互式图表和图表应用程序，国内外著名的 ProcessOne 和 draw.io 都是使用该库创建的强大的在线流程图绘制网站。&lt;/p&gt;
 &lt;p&gt;由于 mxGraph 是一个开放的 js 绘图开发框架，我们可以开发出很炫的样式，或者完全按照项目需求定制。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="9.jpeg" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1322a6fdbf684d5595513c6471f2b403~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;官方网站：  &lt;a href="http://jgraph.github.io/mxgrap"&gt;http://jgraph.github.io/mxgrap&lt;/a&gt;&lt;/p&gt;
 &lt;h4&gt;bpmn-js&lt;/h4&gt;
 &lt;p&gt;bpmn-js 是 BPMN2.0 渲染工具包和 Web 模型。bpmn-js 正在努力成为 Camunda BPM 的一部分。bpmn-js 使用 Web 建模工具可以很方便的构建 BPMN 图表，可以把 BPMN 图表嵌入到你的项目中，容易扩展。&lt;/p&gt;
 &lt;p&gt;bpmn-js 是基于原生 js 开发，支持集成到 vue、react 等开源框架中。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="10.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f465f76b59e04498ad0ee48994ebb34e~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;官方网站：  &lt;a href="https://bpmn.io/"&gt;https://bpmn.io/&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;以上介绍的都属于是功能强大且完善的框架，除此之外，还有其他基于 Vue 或者 React 开发的可视化编辑工具，大家也可以根据自己的实际需求进行选择。&lt;/p&gt;
 &lt;h3&gt;流程引擎&lt;/h3&gt;
 &lt;p&gt;最后来说说流程引擎，整个系统的核心。引擎设计的好坏决定了整个系统的稳定性，可用性，扩展性等等。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="workflow-arch.drawio.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2bd0e29d8f5a4bd6adbe2cd361bf4849~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;整体架构如图所示，主要包括一下几个部分：&lt;/p&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;p&gt;三、  &lt;strong&gt;存储服务&lt;/strong&gt;提供具体模型及流程流转产生的信息的存储空间，工作流系统通常需要支持各种常见的数据库存储。&lt;/p&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;RESTful 服务&lt;/strong&gt;。&lt;/p&gt;
 &lt;h2&gt;后记&lt;/h2&gt;
 &lt;p&gt;下面来说说我现在开发的系统支持到了什么程度，以及未来可能的发展方向。由于毕竟不是一个专门的工单系统，工单申请也只是其中的一个模块，所以在整体的功能上肯定和完整的工作流引擎有很大差距。&lt;/p&gt;
 &lt;h3&gt;第一版&lt;/h3&gt;
 &lt;p&gt;第一版并没有流程引擎，开发方式简单粗暴，每增加一个流程，就需要重新开发对应的表和业务代码。&lt;/p&gt;
 &lt;p&gt;这样做的缺点是非常明显的：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;每个流程需要单独开发，工作量大，开发效率低&lt;/li&gt;
  &lt;li&gt;流程功能相近，代码重复量大，冗余，不利于维护&lt;/li&gt;
  &lt;li&gt;定制化开发，缺少扩展性#&lt;/li&gt;
&lt;/ol&gt;
 &lt;h3&gt;第二版&lt;/h3&gt;
 &lt;p&gt;第二版，也就是目前的版本。&lt;/p&gt;
 &lt;p&gt;随着工单流程逐渐增多，工作量逐渐增大，于是开始对流程进行优化，开发了现阶段的工作流引擎。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="sc_20221230173444.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/087c945df1754d19a201905b13088b70~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在新增一个工单流程时，需要先进行工作流配置，配置其基础信息，自定义字段，状态和流转这些信息。还支持配置自动化节点，可以根据条件由程序自动完成相关操作并审批。&lt;/p&gt;
 &lt;p&gt;配置好之后，后端无需开发，由统一的引擎代码进行处理，包括节点审批流转，状态变更等。只需要开发前端的创建和查询页面即可，相比于第一版，已经在很大程度上提高了开发效率。&lt;/p&gt;
 &lt;p&gt;目前版本需要优化的点：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;缺少可视化流程设计器，无法做到拖拽式设计流程&lt;/li&gt;
  &lt;li&gt;节点之间状态流转不够灵活&lt;/li&gt;
  &lt;li&gt;缺少分布式事物支持，以及异常处理机制&lt;/li&gt;
&lt;/ol&gt;
 &lt;h3&gt;下一个版本&lt;/h3&gt;
 &lt;p&gt;针对以上不足，下一个版本准备主要优化三点，如下：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;需要支持可视化流程设计器，使流程设计更加简单，灵活&lt;/li&gt;
  &lt;li&gt;根据流程配置自动生成前端页面，做到新增一种类型的工单，无需开发&lt;/li&gt;
  &lt;li&gt;增加节点自动化能力，异常处理机制，提高系统的稳定性&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;以上就是本文的全部内容，如果觉得还不错的话欢迎  &lt;strong&gt;点赞&lt;/strong&gt;，  &lt;strong&gt;转发&lt;/strong&gt;和  &lt;strong&gt;关注&lt;/strong&gt;，感谢支持。&lt;/p&gt;
 &lt;hr&gt;&lt;/hr&gt;
 &lt;p&gt;  &lt;strong&gt;参考文章：&lt;/strong&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://www.cnblogs.com/grey-wolf/p/15963839.html"&gt;https://www.cnblogs.com/grey-wolf/p/15963839.html&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://www.cnblogs.com/duck-and-duck/p/14436373.html#!comments"&gt;https://www.cnblogs.com/duck-and-duck/p/14436373.html#!comments&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://zhuanlan.zhihu.com/p/369761832"&gt;https://zhuanlan.zhihu.com/p/369761832&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://zhuanlan.zhihu.com/p/143739835"&gt;https://zhuanlan.zhihu.com/p/143739835&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://bbs.qolome.com/?p=365"&gt;https://bbs.qolome.com/?p=365&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;   &lt;a href="https://workflowengine.io/blog/java-workflow-engines-comparison/"&gt;https://workflowengine.io/blog/java-workflow-engines-comparison/&lt;/a&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;a href="https://mp.weixin.qq.com/s/hRd1UNMRutmA6MGmswweBw"&gt;Git 分支管理策略&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62583-%E5%B7%A5%E4%BD%9C%E6%B5%81-%E5%BC%95%E6%93%8E-%E6%9E%B6%E6%9E%84</guid>
      <pubDate>Wed, 11 Jan 2023 12:51:24 CST</pubDate>
    </item>
    <item>
      <title>异地多活架构设计关键几点</title>
      <link>https://itindex.net/detail/62535-%E6%9E%B6%E6%9E%84-%E8%AE%BE%E8%AE%A1</link>
      <description>&lt;div&gt;在异地多活项目整体推进过程中的一些注意事项和设计点归纳和整理，抛砖引玉，其中一些点还有待深入探讨和优化。&lt;/div&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;一、指导事项归纳&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 1、多活原因归纳 &lt;p&gt; &lt;/p&gt; &lt;p&gt;推动多活的原因大体可归纳为以下三种。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;高可用架构部署&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;业务整体的容灾&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;单机房容量限制&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; 2、多活指导归纳 &lt;p&gt; &lt;/p&gt; &lt;p&gt;多活牵扯公司业务方方面面，整体来讲业务改造和基础设施中间件改造两大块。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;核心链路自包含可逻辑分片&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;调用尽可能收敛在本单元&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;流量分片逻辑尽可能均衡&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;中间件多活架构改造升级&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;业务改造支持多活方案&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;业务场景验证中间件能力&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; 3、推动事项归纳 &lt;p&gt; &lt;/p&gt; &lt;p&gt;顺利推进多活事项是公司重要战略，需要统一思想，将多活项目当成最高优先级推进。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;统一思想认识自觉对齐到公司级战略项目&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;设置总架构师级别建议对齐部门负责人，对整体架构方案和结果负责&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;例如：总架构师拥有对各个部门牵头同学拥有不低于60%的绩效考核权&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;部门负责人作为该部门领导需要全力推动&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;每个业务线设置接口人并负责该业务线所有对接和推动事务，对本业务线或者部门的推动结果负责&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;例如：业务线接口人拥本业务参与多活事项同学不低于60%的绩效考核权&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;项目架构师与各业务负责人周会例会及时跟进问题和进度&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;各个牵头人梳理的问题对外沟通前，先部门内部对齐，提升沟通效率&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; 4、抓核心链路 &lt;p&gt; &lt;/p&gt; &lt;p&gt;先保证核心链路的多活，避免面面俱到严重拖累进度，例如：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;优惠券库存类扣减先中心机房统一扣减&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;管理运营类等无实时要求的先不做多活&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;流量切换过程中容忍分钟级不可用，切换结束后恢复&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;二、多活规则与流量选择&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 1、路由因子选择与映射 &lt;p&gt; &lt;/p&gt; &lt;p&gt;路由因子选择： 需要根据公司业务场景选择，常见的路由因子有地域、用户ID。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2022/1208/20221208104200394.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;路由因子与机房映射：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;地域因子：将地域编号与机房建立映射，例如：001-&amp;gt;unit-a&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;用户因子：将UID与机房建立映射，例如：123456与机房编号哈希后映射到unit-a&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; 2、请求分配正确机房 &lt;p&gt; &lt;/p&gt; &lt;p&gt;一个请求有了多活规则后如何将请求路由到正确机房，归纳了以下几种方式：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;终端服务通过多域名切换：将请求直接路由到正确机房&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;在反向代理层转发：转发属于异地机房流量&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;在网关层转发：转发属于异地机房流量&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2022/1208/20221208104210842.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 3、多活管控中心服务 &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;多活部署通过双向同步或者双写方式保证数据的一致性&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;提供SDK和服务接口供中间件或者服务服务映射规则&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;提供流量切换的整个闭环流程&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;三、RPC跨机房调用能力&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 1、注册中心架构图 &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;节点注册时需要将机房信息一并注册&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;注册中心提供跨机房双向同步能力&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2022/1208/20221208104220111.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 2、RPC框架跨机房调用 &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;默认本机房调用策略&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;提供自定义路由功能供业务选择是否跨机房调用&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;需要注意新老版本以及发布时是否存在流量倾斜问题&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2022/1208/20221208104230312.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;四、消息跨机房复制&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 1、复制插件管理与监控 &lt;p&gt; &lt;/p&gt; &lt;p&gt;在一些业务场景中需要消息集群提供跨机房复制能力，将其他机房的流量收敛到一个机房去消费。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;通过复制器插件将消息跨机房复制&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;通过管理平台对复制器的监控和管理&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2022/1208/20221208104239417.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 2、流量隔离与动态订阅 &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;通过不同主题进行流量隔离规避重复复制问题&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;动态唤醒消费SDK订阅复制流量&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;复制流量来源机房打标&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2022/1208/20221208104249874.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;五、存储双向同步&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 1、Redis双向同步 &lt;p&gt; &lt;/p&gt; &lt;p&gt;Redis双向同步并不是做多活的公司都需要，如果能作为极短时间过期使用无需进行同步。然而也有的作为较长时间去存储，如果业务改造成本巨大需要提供双向复制能力。方案有很多种，有改源码的，下面介绍一种RedisSyncer，java实现，详见下面github连接。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;https://github.com/TraceNature/redissyncer-server&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;可根据实际场景进行改造，主要功能有：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;断点续传&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;数据同步&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;数据迁移&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;数据校验&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2022/1208/20221208104259534.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;实现原理：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;复制器伪装成从节点复制数据&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;同步时通过写入辅助key的方式来识别流量来源，规避重复复制问题&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;注意事项：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;是否需要redis双向复制提早规划&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;过滤过短时间key无效复制，比如：小于3秒的不再同步&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;批量写入提升性能&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; 2、MySql双向同步 &lt;p&gt; &lt;/p&gt; &lt;p&gt;数据库的双向同步在异地多活通常是必须要做的事情，下面是阿里开源otter，可基于其二次定制开发。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;https://github.com/alibaba/otter&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2022/1208/20221208104309301.jpg"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;解决循环复制实现原理：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;通过事务表解决数据循环复制&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;复制数据时同时写入一条数据到事务表在同一个事物中&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;同步数据时只同步不再事务表中的数据到异地机房&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;还需要提供其他周边工具：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;提供数据校验工具&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;提供数据订正工具&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;提供DDL双向同步&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;提供数据冲突策略&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;注意事项提点：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;统一关系数据库存储，多种数据库PostgreSQL、MySql等的建议统一为一种&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;相关任务提早同步进行&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;中间件与DBA开发协同推进，例如可以将周边工具交由DBA开发&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;另外，在存储侧流量切换时需要提供数据库禁写功能，避免实现切流过程数据的不一致，禁写的实现可以通过sql动态拼接一个很大的时间戳实现。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;六、其他改造事项&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;除了中间件和业务核心服务改造外，还有一些其他的改造事项，例如：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;发布系统支持不同机房发布&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;CMDB中的资源和应用标识&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;监控体系支持不同机房流量标识&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;其他存储相关（ES、Hbase等）尽量不复制&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;七、流量切换过程&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 1、流量切换大体流程 &lt;p&gt; &lt;/p&gt; &lt;p&gt;从机房A流量切换机房B的大体流程图如下：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;多活规则中心下发禁写通知和禁写时间基线&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;数据库SDK收到禁写数据库写入和更新&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;双向复制器收到超过禁写时间基线不再复制&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;双向复制器上报复制完成状态&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;多活规则中心下发流量切换通知&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;Nginx&amp;amp;网关层收到将流量切换到机房B并上报切换完成状态&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;多活规则中心下发取消禁写通知&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://dbaplus.cn/uploadfile/2022/1208/20221208104323602.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 2、流量切换注意问题 &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;部分流量切换的问题，场景一：切某个地域的10%流量，场景二：切某个场景用户的10%流量&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;部分流量切换时数据库禁止设计判断&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;部分流量切换时复制器完成的判断和替代方案&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; 3、复制器监控与思考 &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;针对复制器自身稳定性和性能的监控&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;复制器复制进度的监控思考&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62535-%E6%9E%B6%E6%9E%84-%E8%AE%BE%E8%AE%A1</guid>
      <pubDate>Sat, 10 Dec 2022 14:06:05 CST</pubDate>
    </item>
    <item>
      <title>详解服务幂等性设计</title>
      <link>https://itindex.net/detail/62520-%E6%9C%8D%E5%8A%A1-%E5%B9%82%E7%AD%89-%E8%AE%BE%E8%AE%A1</link>
      <description>&lt;p&gt;  &lt;strong&gt;本文正在参加   &lt;a href="https://juejin.cn/post/7162096952883019783" title="https://juejin.cn/post/7162096952883019783"&gt;「金石计划 . 瓜分6万现金大奖」&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;hello，大家好，我是张张，「架构精进之路」公号作者。&lt;/p&gt;
 &lt;h2&gt;引子&lt;/h2&gt;
 &lt;p&gt;在日常工作中的一些技术设计方案评审会上，经常会有人提到注意服务接口的幂等性问题，最近就有个组内同学就跑到跟前问我，幂等性到底是个啥？&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;在目前分布式/微服务化的今天，提供的服务能力丰富多样，基于 HTTP 协议的 Web API 是时下最为流行的一种分布式服务提供方式，对于服务的幂等性保障尤为重要。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;我想了想，觉得有必要好好给他普及一下才行，否则以后做事还是一头雾水&lt;/p&gt;
 &lt;p&gt;今天计划就关于服务幂等性的一系列问题，在此将材料总结整理，顺便分享给大家~&lt;/p&gt;
 &lt;h2&gt;1、何为幂等性？&lt;/h2&gt;
 &lt;blockquote&gt;
  &lt;p&gt;幂等（idempotence），来源于数学中的一个概念，例如：幂等函数/幂等方法（指用相同的参数重复执行，并能获得相同结果的函数，这些函数不影响系统状态，也不用担心重复执行会对系统造成改变）。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;简单理解即：  &lt;strong&gt;多次调用对系统的产生的影响是一样的，即对资源的作用是一样的&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5e68d25f7c6649bf9ee0e3f2acc61839~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;幂等性&lt;/p&gt;
 &lt;p&gt;幂等性强调的是外界通过接口对系统内部的影响, 只要一次或多次调用对某一个资源应该具有同样的副作用就行。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;注意：这里指对资源造成的副作用必须是一样的，但是返回值允许不同！&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h2&gt;2、幂等性主要场景有哪些？&lt;/h2&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;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;微服务架构下，不同微服务间会有大量的基于 http,rpc 或者 mq 消息的网络通信，会有第三个情况【未知】，也就是超时。如果超时了，微服务框架会进行重试。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;用户交互的时候多次点击,无意地触发多笔交易。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;MQ 消息中间件，消息重复消费&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;第三方平台的接口（如：支付成功回调接口），因为异常也会导致多次异步回调&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;其他中间件/应用服务根据自身的特性，也有可能进行重试。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;3、幂等性的作用是什么？&lt;/h2&gt;
 &lt;p&gt;幂等性主要保证  &lt;strong&gt;多次调用对资源的影响是一致的&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;在阐述作用之前，我们利用资源处理应用来说明一下：&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;HTTP 与数据库的 CRUD 操作对应：&lt;/p&gt;
  &lt;ul&gt;
   &lt;li&gt;PUT ：CREATE&lt;/li&gt;
   &lt;li&gt;GET ：READ&lt;/li&gt;
   &lt;li&gt;POST ：UPDATE&lt;/li&gt;
   &lt;li&gt;DELETE ：DELETE&lt;/li&gt;
&lt;/ul&gt;
  &lt;p&gt;（其实不光是数据库，任何数据如文件图表都是这样）&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;1）  &lt;strong&gt;查询&lt;/strong&gt;&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;SELECT * FROM users WHERE xxx;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;不会对数据产生任何变化，天然具备幂等性。&lt;/p&gt;
 &lt;p&gt;2）  &lt;strong&gt;新增&lt;/strong&gt;&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;INSERT INTO users (user_id, name) VALUES (1, &amp;apos;zhangsan&amp;apos;);
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;case1：带有唯一索引（如：`user_id`），重复插入会导致后续执行失败，具有幂等性；&lt;/p&gt;
 &lt;p&gt;case2：不带有唯一索引，多次插入会导致数据重复，不具有幂等性。&lt;/p&gt;
 &lt;p&gt;3）  &lt;strong&gt;修改&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;case1：直接赋值，不管执行多少次 score 都一样，具备幂等性。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;UPDATE users SET score = 30 WHERE user_id = 1;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;case2：计算赋值，每次操作 score 数据都不一样，不具备幂等性。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;UPDATE users SET score = score + 30 WHERE user_id = 1;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;4）  &lt;strong&gt;删除&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;case1：绝对值删除，重复多次结果一样，具备幂等性。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;DELETE FROM users WHERE id = 1;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;case2：相对值删除，重复多次结果不一致，不具备幂等性。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;DELETE top(3) FROM users;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;总结：  &lt;strong&gt;通常只需要对写请求（新增 &amp;amp;更新）作幂等性保证&lt;/strong&gt;。&lt;/p&gt;
 &lt;h2&gt;4、如何解决幂等性问题？&lt;/h2&gt;
 &lt;p&gt;我们在网上搜索幂等性问题的解决方案，会有各种各样的解法，但是如何判断哪种解决方案对于自己的业务场景是最优解，这种情况下，就需要我们抓问题本质。&lt;/p&gt;
 &lt;p&gt;经过以上分析，我们得到了解决幂等性问题就是  &lt;strong&gt;要控制对资源的写操作&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;我们从问题各个环节流程来分析解决：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1d1bea9028eb4242a55748de7f2668cd~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;幂等性问题分析&lt;/p&gt;
 &lt;h3&gt;4.1 控制重复请求&lt;/h3&gt;
 &lt;p&gt;控制动作触发源头，即前端做幂等性控制实现&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;相对不太可靠，没有从根本上解决问题，仅算作辅助解决方案。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;  &lt;strong&gt;主要解决方案：&lt;/strong&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;控制操作次数，例如：提交按钮仅可操作一次（提交动作后按钮置灰）&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;及时重定向，例如：下单/支付成功后跳转到成功提示页面，这样消除了浏览器前进或后退造成的重复提交问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;4.2 过滤重复动作&lt;/h3&gt;
 &lt;p&gt;控制过滤重复动作，是指在动作流转过程中控制有效请求数量。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;1）分布式锁&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;利用 Redis 记录当前处理的业务标识，当检测到没有此任务在处理中，就进入处理，否则判为重复请求，可做过滤处理。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;订单发起支付请求，支付系统会去 Redis 缓存中查询是否存在该订单号的 Key，如果不存在，则向 Redis 增加 Key 为订单号。查询订单支付已经支付，如果没有则进行支付，支付完成后删除该订单号的 Key。通过 Redis 做到了分布式锁，只有这次订单订单支付请求完成，下次请求才能进来。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;分布式锁相比去重表，将放并发做到了缓存中，较为高效。思路相同，同一时间只能完成一次支付请求。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;2）token 令牌&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;应用流程如下：&lt;/p&gt;
 &lt;p&gt;1）服务端提供了发送 token 的接口。执行业务前先去获取 token，同时服务端会把 token 保存到 redis 中；&lt;/p&gt;
 &lt;p&gt;2）然后业务端发起业务请求时，把 token 一起携带过去，一般放在请求头部；&lt;/p&gt;
 &lt;p&gt;3）服务器判断 token 是否存在 redis 中，存在即第一次请求，可继续执行业务，执行业务完成后将 token 从 redis 中删除；&lt;/p&gt;
 &lt;p&gt;4）如果判断 token 不存在 redis 中，就表示是重复操作，直接返回重复标记给 client，这样就保证了业务代码不被重复执行。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/feaa6aba126f4e6793858e8ffd05b7d4~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;3）缓冲队列&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;把所有请求都快速地接下来，对接入缓冲管道。后续使用异步任务处理管道中的数据，过滤掉重复的请求数据。&lt;/p&gt;
 &lt;p&gt;优点：同步转异步，实现高吞吐。&lt;/p&gt;
 &lt;p&gt;缺点：不能及时返回处理结果，需要后续监听处理结果的异步返回数据。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/738b6c9bcf284cf7a906810178669d88~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;4.3 解决重复写&lt;/h3&gt;
 &lt;p&gt;实现幂等性常见的方式有：  &lt;strong&gt;悲观锁（for update）、乐观锁、唯一约束&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;1）悲观锁（Pessimistic Lock）&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;简单理解就是：假设每一次拿数据，都有认为会被修改，所以给数据库的行或表上锁。&lt;/p&gt;
 &lt;p&gt;当数据库执行 select for update 时会获取被 select 中的数据行的行锁，因此其他并发执行的 select for update 如果试图选中同一行则会发生排斥（需要等待行锁被释放），因此达到锁的效果。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;select for update 获取的行锁会在当前事务结束时自动释放，因此必须在事务中使用。（注意 for update 要用在索引上，不然会锁表）&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;START TRANSACTION; 
# 开启事务
SELETE * FROM users WHERE id=1 FOR UPDATE;
UPDATE users SET name= &amp;apos;xiaoming&amp;apos; WHERE id = 1;
COMMIT; 
# 提交事务
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;strong&gt;2）乐观锁（Optimistic Lock）&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;简单理解就是：就是很乐观，每次去拿数据的时候都认为别人不会修改。更新时如果 version 变化了，更新不会成功。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;不过，乐观锁存在失效的情况，就是常说的 ABA 问题，不过如果 version 版本一直是自增的就不会出现 ABA 的情况。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;pre&gt;  &lt;code&gt;UPDATE users 
SET name=&amp;apos;xiaoxiao&amp;apos;, version=(version+1) 
WHERE id=1 AND version=version;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;缺点：就是在操作业务前，需要先查询出当前的 version 版本&lt;/p&gt;
 &lt;p&gt;另外，还存在一种：  &lt;strong&gt;状态机控制&lt;/strong&gt;&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;例如：支付状态流转流程：待支付-&amp;gt;支付中-&amp;gt;已支付&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;具有一定要的前置要求的，严格来讲，也属于乐观锁的一种。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;3）唯一约束&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;常见的就是利用  &lt;strong&gt;数据库唯一索引&lt;/strong&gt;或者  &lt;strong&gt;全局业务唯一标识&lt;/strong&gt;（如：source+序列号等）。&lt;/p&gt;
 &lt;p&gt;这个机制是利用了数据库的主键唯一约束的特性，解决了在 insert 场景时幂等问题。但主键的要求不是自增的主键，这样就需要业务生成全局唯一的主键。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;全局 ID 生成方案&lt;/strong&gt;：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;UUID&lt;/strong&gt;：结合机器的网卡、当地时间、一个随记数来生成 UUID；&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;数据库自增 ID&lt;/strong&gt;：使用数据库的 id 自增策略，如 MySQL 的 auto_increment。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;Redis 实现&lt;/strong&gt;：通过提供像 INCR 和 INCRBY 这样的自增原子命令，保证生成的 ID 肯定是唯一有序的。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;雪花算法-Snowflake&lt;/strong&gt;：由 Twitter 开源的分布式 ID 生成算法，以划分命名空间的方式将 64-bit 位分割成多个部分，每个部分代表不同的含义。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;小结&lt;/strong&gt;：按照应用上的最优收益，推荐排序为：  &lt;strong&gt;乐观锁 &amp;gt; 唯一约束 &amp;gt; 悲观锁&lt;/strong&gt;。&lt;/p&gt;
 &lt;h2&gt;5、总结&lt;/h2&gt;
 &lt;p&gt;通常情况下，非幂等问题，主要是  &lt;strong&gt;由于重复且不确定的写操作&lt;/strong&gt;造成的。&lt;/p&gt;
 &lt;h4&gt;1、解决重复的主要思考点&lt;/h4&gt;
 &lt;p&gt;从请求全流程，控制重复请求触发以及重复数据处理&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;客户端 控制发起重复请求&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;服务端 过滤重复无效请求&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;底层数据处理 避免重复写操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h4&gt;2、控制不确定性主要思考点&lt;/h4&gt;
 &lt;p&gt;从服务设计思路上做改变，尽量避免不确定性：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;统计变量改为数据记录方式&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;范围操作改为确定操作&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;后记&lt;/h2&gt;
 &lt;p&gt;听了我以上大段的讲述后，他好像收获感满满的似的说：大概理解了...&lt;/p&gt;
 &lt;p&gt;但是出于自身责任感，我还得叮嘱他几句：&lt;/p&gt;
 &lt;p&gt;1）幂等性处理 虽然复杂了业务处理，也可能会降低接口的执行效率，但是为了保证系统数据的准确性，是非常有必要的；&lt;/p&gt;
 &lt;p&gt;2）遇到问题，善于发现并挖掘本质问题，这样解决起来才能高效且精准；&lt;/p&gt;
 &lt;p&gt;3）选择自身业务场景适合的解决方案，而不要去硬套一些现成的技术实现，无论是组合还是创新，要记住适合的才是最好的。&lt;/p&gt;
 &lt;p&gt;愿大家能够掌握问题分析以及解决的能力，都不要一上来就急于解决问题，可以多做些深入分析，了解本质问题之后再考虑解决办法进行解决。&lt;/p&gt;
 &lt;p&gt;·················· END ··················&lt;/p&gt;
 &lt;p&gt;希望今天的讲解对大家有所帮助，谢谢！&lt;/p&gt;
 &lt;p&gt;Thanks for reading!&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;作者：架构精进之路，十年研发风雨路，大厂架构师，CSDN 博客专家，专注架构技术沉淀学习及分享，职业与认知升级，坚持分享接地气儿的干货文章，期待与你一起成长。   &lt;br /&gt;
**关注并私信我回复“01”，送你一份程序员成长进阶大礼包，欢迎勾搭。   &lt;br /&gt;
**&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62520-%E6%9C%8D%E5%8A%A1-%E5%B9%82%E7%AD%89-%E8%AE%BE%E8%AE%A1</guid>
      <pubDate>Sun, 04 Dec 2022 14:41:23 CST</pubDate>
    </item>
    <item>
      <title>高可用架构的设计方法</title>
      <link>https://itindex.net/detail/62483-%E6%9E%B6%E6%9E%84-%E8%AE%BE%E8%AE%A1-%E6%96%B9%E6%B3%95</link>
      <description>&lt;blockquote&gt;
  &lt;p&gt;我们来自字节跳动飞书商业应用研发部(Lark Business Applications)，目前我们在北京、深圳、上海、武汉、杭州、成都、广州、三亚都设立了办公区域。我们关注的产品领域主要在企业经验管理软件上，包括飞书 OKR、飞书绩效、飞书招聘、飞书人事等 HCM 领域系统，也包括飞书审批、OA、法务、财务、采购、差旅与报销等系统。欢迎各位加入我们。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;blockquote&gt;
  &lt;p&gt;本文作者：LBA 许家强&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h1&gt;概述&lt;/h1&gt;
 &lt;p&gt;高可用(High Availability)，简称HA，是衡量IT系统服务质量的一个极其重要的参考，高可用一直是IT系统设计中需要重点关注的点。本文总结高可用架构中的一些关键设计思想。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;衡量指标SLA&lt;/strong&gt;  &lt;br /&gt;
SLA是衡量网站服务可用性的一个关键指标，现在互联网公司一般以X个9来表示在系统1年时间的使用过程中，系统可正常使用时间与总时间（1年）之比，9越多代表全年服务可用时间越长、服务更可靠、停机时间越短，反之亦然。&lt;/p&gt;
 &lt;p&gt;一般而言，如果系统达到4个9就非常优秀了，需要在设计上做足功夫。&lt;/p&gt;
 &lt;h1&gt;冗余&lt;/h1&gt;
 &lt;p&gt;冗余是构建高可用的重要手段。其核心思想是对分布式系统中的节点进行备份。备份会分为冷备和热备。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;冷备，一般会有主数据中心和备数据中心，正常情况下是主数据中心提供业务服务，备份中心不会有提供业务服务，而是会定期从主数据中心进行备份（非实时备份），也就是说如果主数据中心出现故障，业务也就中断了，需要人工进行干预，将流量从主数据中心切换到备数据中心。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;热备，主要是对主数据中心进行实时性的备份，以保证在主数据中心出现故障后可以及时的切换，让用户不受影响的继续使用。 关于主数据中心到备数据中心的复制方式，分为同步和异步两种分式。&lt;/p&gt;
   &lt;ul&gt;
    &lt;li&gt;同步方式，当主节点处理完请求后，会同步向多个备节点实时备份数据，只有所有节点数据同步完成，主节点才会向客户端返回成功。这种方式对可用性有较大损耗，一般不推荐使用，&lt;/li&gt;
    &lt;li&gt;异步方式，当主节点处理请求后，会向客户端返回成功，同时会异步触发向多个备节点实时备份数据。该情况下，主备节点的数据会存在数据不一致或者延时，业务上需要能容忍一定程度的数据延迟。互联网系统一般会采用这种方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;从备节点的工作方式划分，又可以分为双机互备和双机热备。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;双机热备，从狭义上讲是使用互为备份的两台服务器共同执行同一服务，在正常情况下，工作机为应用系统提供服务，备份机监视工作机的运行情况，出故障时备机可迅速接管服务。&lt;/li&gt;
  &lt;li&gt;双机互备，是指主机和备机互为备份，备机上运行与主机不同的应用，例如两台server安装相同的系统、应用软件，主机备机同时工作，主机跑ORACLE，备机跑IIS，任意一台服务器故障时，所有服务会自动切换到正常的服务器上。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h1&gt;集群&lt;/h1&gt;
 &lt;p&gt;集群是相对单机而言的，集群部署是分布式系统的典型特征。集群的作用其实就是分流，这里面不得不提核心的分流技术。&lt;/p&gt;
 &lt;h2&gt;负载均衡&lt;/h2&gt;
 &lt;p&gt;分为硬件负载和软件负载。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;硬件负载：通过硬件来进行分流，常见的硬件有比较昂贵的F5和Array等商用的负载均衡器，它的优点就是有专业的维护团队来对这些服务进行维护、缺点就是花销太大，一般在互联网系统较少使用，主要用于金融行业的核心服务；&lt;/li&gt;
  &lt;li&gt;软件负载：通过软件实现分流，如Nginx/LVS/HAProxy的基于Linux的开源免费的负载均衡软件，这些都是通过软件级别来实现，费用非常低廉。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;按所处OSI模型的工作层级，可分为7层负载和4层负载。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;7层负载，是指工作在网络7层，基于URL等应用层信息的负载均衡，主要代表有Nginx。&lt;/li&gt;
  &lt;li&gt;4层负载，就是基于IP+端口的负载均衡，主要代表有LVS。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;DNS&lt;/h2&gt;
 &lt;p&gt;Domain Name System，“域名系统”的英文缩写，是一种组织成域层次结构的计算机和网络服务命名系统，它所提供的服务是用来将主机名和域名转换为IP地址的工作。一般大型网站的域名，背后会绑定多个vipServer的地址，DNS可以通过智能域名解析系统，返回离用户最近的vipServer 的ip。&lt;/p&gt;
 &lt;h1&gt;容错&lt;/h1&gt;
 &lt;p&gt;容错能力也是影响IT系统可用性的一个关键要素。&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;狭义的容错，一般会在设计上体现在以下方面:&lt;/li&gt;
&lt;/ol&gt;
 &lt;ul&gt;
  &lt;li&gt;对用户的输入进行尽早的校验，不信任外部的输入。&lt;/li&gt;
  &lt;li&gt;程序中尽可能的考虑边界及异常的情况。&lt;/li&gt;
&lt;/ul&gt;
 &lt;ol start="2"&gt;
  &lt;li&gt;广义的容错应该是两个具有明确边界的事物（如服务间，系统间）交互时候针对可能发生的一切主客观异常情况的防御性手段。常见的容错机制有failsafe、failback、failover、failfast。&lt;/li&gt;
&lt;/ol&gt;
 &lt;h2&gt;failfast&lt;/h2&gt;
 &lt;p&gt;快速失败，尽可能的发现系统中的错误，使系统能够按照事先设定好的错误的流程执行，避免资源耗尽或积压导致系统滚雪球式崩溃。我们通常讲的熔断就是这个思想。&lt;/p&gt;
 &lt;h2&gt;failover&lt;/h2&gt;
 &lt;p&gt;失效转移，它和前面提到的冗余备份关联很紧密。当主要组件异常时，其功能迅速转移到备份组件。MYSQL的双主模式、Zookeeper的自动选举、Redis的哨兵模式，都是基于这种思想，目的是尽量减少部分节点故障对用户服务的影响&lt;/p&gt;
 &lt;h2&gt;failback&lt;/h2&gt;
 &lt;p&gt;自动恢复，是相对failover而言的，簇网络系统（有两台或多台服务器互联的网络）中，由于要某台服务器故障进行维修，需要网络资源和服务暂时重定向到备用系统。在此之后将网络资源和服务器恢复为由原始主机提供的过程，称为自动恢复。一般依赖心跳探测技术来实现自动恢复，例如dubbo服务中的应用实例出现down机后，在服务恢复后会自动注册到config server，重新对外提供服务。&lt;/p&gt;
 &lt;h2&gt;failsafe&lt;/h2&gt;
 &lt;p&gt;失效安全，即在故障的情况下也不会造成伤害或者尽量减少伤害。并非所有的故障对用户都是致命的，当这类非关键的故障出现时可以忽略，因为这种故障不会造成损失或损失在可接受范围内。&lt;/p&gt;
 &lt;h1&gt;多活&lt;/h1&gt;
 &lt;p&gt;双活/多活架构，关键点是指不同地理位置上的系统都能够提供服务。“活”是指实时提供服务，与“活”对应的是字是“备”——备是备份，正常情况下对外是不提供服务或只提供部分服务（例如DB读写分离），如果需要提供服务，则需要大量的人工干预和操作，花费大量的时间才能让“备”变成“活。&lt;/p&gt;
 &lt;p&gt;实现多活有较高的成本，要考虑数据一致性、网络延时问题。&lt;/p&gt;
 &lt;p&gt;常见的多活方案有同城双活、两地三中心、三地五中心、异地多活等多种技术方案，不同多活方案技术要求、建设成本、运维成本都不一样。&lt;/p&gt;
 &lt;h2&gt;同城双活&lt;/h2&gt;
 &lt;p&gt;是在同城或相近区域内建立两个机房。同城双机房距离比较近，通信线路质量较好，比较容易实现数据的同步复制 ，保证高度的数据完整性和数据零丢失。同城两个机房各承担一部分流量，一般入口流量完全随机，内部RPC调用尽量通过就近路由闭环在同机房，相当于两个机房镜像部署了两个独立集群，数据仍然是单点写到主机房数据库，然后实时同步到另外一个机房。&lt;/p&gt;
 &lt;p&gt;该架构优点是方案简单、成本较低、网络延时小，缺点是由于双机房都在同城，所在城市出现网络故障或自然灾害时，服务可用性无法保障。&lt;/p&gt;
 &lt;h2&gt;两地三中心&lt;/h2&gt;
 &lt;p&gt;指 同城双中心 + 异地灾备中心。异地灾备中心是指在异地的城市建立一个备份的灾备中心，用于双中心的数据备份，数据和服务平时都是冷的，当双中心所在城市或者地区出现异常而都无法对外提供服务的时候，异地灾备中心可以用备份数据进行业务的恢复。&lt;/p&gt;
 &lt;p&gt;该架构的优点是相比同城双活，增加了异地灾备能力；缺点是异地的备份数据中心是冷的，出故障时异地机房是否能顺利接管流量是个大问题。&lt;/p&gt;
 &lt;h2&gt;异地多活&lt;/h2&gt;
 &lt;p&gt;异地的一个核心问题是物理距离带来的延时，因此要避免一次请求在异地的多个机房之间流转，因此异地多活的核心是单元化，要保证单次请求在一个固定机房内封闭。&lt;/p&gt;
 &lt;p&gt;单元化，在技术上的核心挑战在于流量路由和数据同步。这里还需注意，有些应用和数据是没法单元化的，因此还会衍生出中心机房和单元机房的概念，对于没法单元化的数据必须在中心机房。 阿里的淘宝是国内最早完整成功实施单元化的系统，其在全球化部署上也有较多成功的实践，在此就不展开了。&lt;/p&gt;
 &lt;h1&gt;去中心化&lt;/h1&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;p&gt;常见的中心化设计：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;ESB&lt;/li&gt;
  &lt;li&gt;单体系统&lt;/li&gt;
  &lt;li&gt;网关&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;常见的去中心化设计：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;DUBBO、MESH等&lt;/li&gt;
  &lt;li&gt;用SDK替代服务&lt;/li&gt;
&lt;/ul&gt;
 &lt;h1&gt;  &lt;strong&gt;加入我们&lt;/strong&gt;&lt;/h1&gt;
 &lt;p&gt;扫码发现职位&amp;amp;投递简历&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1b24b482ce2f4fc29950a5312bfa1dc8~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;官网投递：  &lt;a href="https://link.juejin.cn/?target=https%3A%2F%2Fjob.toutiao.com%2Fs%2FFyL7DRg" title="https://job.toutiao.com/s/FyL7DRg"&gt;job.toutiao.com/s/FyL7DRg&lt;/a&gt;&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;欢迎大家关注   &lt;a href="https://juejin.cn/user/712139266595784" title="https://juejin.cn/user/712139266595784"&gt;    &lt;strong&gt;飞书技术&lt;/strong&gt;&lt;/a&gt;，每周定期更新飞书技术团队技术干货内容，想看什么内容，欢迎大家评论区留言~&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62483-%E6%9E%B6%E6%9E%84-%E8%AE%BE%E8%AE%A1-%E6%96%B9%E6%B3%95</guid>
      <pubDate>Wed, 09 Nov 2022 18:27:03 CST</pubDate>
    </item>
    <item>
      <title>京东售后系统架构设计：专治多端并发、数据不一致的臭毛病</title>
      <link>https://itindex.net/detail/62476-%E4%BA%AC%E4%B8%9C-%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84-%E8%AE%BE%E8%AE%A1</link>
      <description>&lt;div&gt;前言  &lt;p&gt; &lt;/p&gt;  &lt;p&gt;通过阅读本文，您将了解到一个售后系统应该具备的一些能力、在整个上下游系统中的定位、基本的系统架构，以及针对售后业务场景中常见问题的解决方案。&lt;/p&gt;&lt;/div&gt; &lt;p&gt; &lt;/p&gt;一、核心价值 &lt;p&gt; &lt;/p&gt; &lt;p&gt;京东到家售后系统作为逆向流，强依赖京东到家业务域，目前涵盖了：退款、退货、换货、维修等四大类场景，并且为用户与商家提供申诉、仲裁场景支持，为计费与结算系统提供逆向金额数据支持。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;售后系统业务结构：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://dbaplus.cn/uploadfile/2022/1027/20221027094621493.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;售后系统上下游依赖：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://dbaplus.cn/uploadfile/2022/1027/20221027094632543.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt;二、系统架构 &lt;p&gt; &lt;/p&gt; &lt;p&gt;售后系统使用的就是基础的三层架构。应用层有不同身份的三个端入口，服务层提供了一些业务支持和数据支持，数据层目前使用到了MySQL和Redis以及ElasticSearch。当然还有一些中间件使用，比如rpc框架，zk配置中心，worker分布式定时任务，jmq消息。还有完善的基础设施，统一监控和日志采集。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://dbaplus.cn/uploadfile/2022/1027/20221027094642270.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt;三、业务形态 &lt;p&gt; &lt;/p&gt; &lt;p&gt;当正向订单履约完成后，如订单中商品有缺件、错件、质量等问题可以发起售后申请。目前申请售后支持用户端、商家端、到家客服发起。用户端申请需要根据不同责任方分配到商家或者客服审核。商家端只能选择商家责任原因申请售后，然后自动审核通过。客服代用户申请售后和用户端一致，流转到商家或客服审核。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;用户端申请售后流程：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://dbaplus.cn/uploadfile/2022/1027/20221027094653213.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 1. 申请售后 &lt;p&gt; &lt;/p&gt; &lt;p&gt;1）多端操作并发场景下问题&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;2）售后商品拆分信息如何获取&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;在正向订单履约完成后一定的时效内，可以通过用户端，商家端，运营端基于订单中商品选择性申请售后。当接收到一个售后单提交申请，售后这边会依赖订单数据，拆分数据来构建售后单详情数据。那么对于多端申请售后入口，我们怎么能保证订单中商品不会被重复申请呢？申请时我们使用了redis分布式锁。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;售后申请场景下分布式锁需要注意点：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;①不同的入口使用相同的key，这里我们通过前缀加订单号来区分，来保证对同一订单加锁。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;②加入过期时间，比如第一个申请获取到锁，如果释放锁异常，这里只需要等到超时时间自动过期，防止死锁。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;③等待锁时间，同一个订单多个入口同时申请售后，如果获取不到锁就进入等待，直到获取到锁或者等待超时后退出。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;④使用uuid来保证token唯一性，每次都释放自己当前请求锁。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;我们保证了同一时间只能有一个订单下的售后能够申请，接下来就是组装售后单详情数据。一个完整的售后单数据来源于订单详情和拆分详情。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;通过从订单详情中取用户基础信息，订单信息，商家门店信息来保存到售后单主表中。根据申请选择的商品skuid从订单商品详情中获取对应商品基础信息保存到售后商品表中。接下来就是比较重要的售后商品拆分信息，这个数据来源于拆分系统。先了解下拆分数据结构:&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://dbaplus.cn/uploadfile/2022/1027/20221027094706398.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;可以看到，拆分系统会根据订单中所有商品把金额拆分到每一件商品上，并且通过num_下标来区分。当选择订单中某个商品发起售后我们是怎么去找到这个商品对应的拆分信息呢？&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;我们通过sku_promotionType（商品+促销类型)来区分不同的商品拆分信息，然后通过记录num商品下标来确定找到哪一个商品。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;比如下面的场景：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;假设订单中购买了3个正价A商品，1个促销A商品。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;①第一次申请一个正价A售后。这时售后系统会记录一个售后单，对应售后详情为商品A。从拆分获取sku_A_正价_num0信息并记录到售后商品拆分详情表。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;②再申请一个正价A和一个促销A售后。这里售后会发现此订单已申请过一个正价A，记录的是sku_A_正价_num0。这时就会去取拆分的 sku_A_正价_num1这条数据。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;③第二次申请售后对应一个新售后单，商品详情记录为sku_A_正价，sku_A_促销。商品拆分记录数据为:sku_A_正价_num1，sku_A_促销_num0。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;初步了解了售后商品获取对应拆分数据的逻辑，这时如果同一个订单中购买了相同促销的A商品，但是价格不一样怎么办呢？按照上面获取逻辑，获取的售后商品金额就会出现多退或者少退情况。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;比如下面的捆绑促销：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;A+B捆绑销售，A金额3元。A+C捆绑销售，此时A金额2元。这时拆分的数据结构为：sku_A_捆绑_num0价格3元，sku_A_捆绑_num0价格2元。此时如果两个A都申请了售后，我们再按照sku_promotionType去获取拆分那么永远获取的都是第一个的金额。因此针对这种特殊的促销场景，我们在原有获取拆分维度基础上又增加了一个价格。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;区分维度：sku_promotionType_price(商品+促销类型+价格)&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;上面的方案可以满足各种不同促销场景的售后，但是针对称重退差订单申请售后还会适用么？&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;称重退差订单含义：当正向订单拣货时，商家发现实际拣货的称重品和售卖规格有误差，此时可以发起退差单把差额的钱退给用户。之后订单正常履约，订单完成后用户也可以申请售后。此时再申请售后退给用户的钱就应该是减去退差后的部分。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;比如下面的场景：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;假设一个订单中买了2个原价A+1个促销价A，原价3元，促销价2元，整单共8元。拣货时发现A商品实际重量比标重少，退差1元，此时退差单中会记录商品A退差金额，退差重量。这时选择正价A发起售后申请，售后系统就需要根据实际重量获取退差商品金额，然后计算实际退款金额。这时我们又在原来的基础上增加了一个重量维度。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;sku_promotionType_price_weight(商品+促销类型+价格+重量)&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;系统都是为了业务来服务的，随着业务变更场景的增多，我们的架构也在演变。目前所有的计算拆分逻辑都封装成统一方法，统一入口，未来再增加不同促销，或者其他业务都可以很友好的支持。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 2. 审核售后 &lt;p&gt; &lt;/p&gt; &lt;p&gt;1）多条件复杂查询性能问题&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;当售后单申请成功后，会根据审核方分配给商家或者客服审核。这里涉及到两个列表查询，一个是运营端客服使用，一个是商家端根据商家账号权限来展示可操作的售后单列表。最初我们的售后单表数据并不是很大，随着业务品类扩增以及用户量的增加遇到了一些问题。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;①数据库频繁报警，慢SQL，影响其他业务&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;②商家运营反馈售后单列表查询过慢，影响审核效率。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;通过分析慢SQL日志，我们根据查询字段增加索引来提高查询速率。由于支持各种查询场景过多，目前主表中已经建立了20多个索引。而且基于业务的发展需要支持查询的时间区间也会更长。主表的数据量一直在增长，还是会遇到查询性能问题，过多的索引对于售后单流程中变化更新也有一定的影响。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;因为ES是基于倒排索引实现的搜索，配合分词器在文本模糊搜索上表现比较好，使用的业务场景广泛，因此我们考虑把售后单数据同步到ES中，列表查询走ES。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;基于我们目的是为了解决查询问题，每次操作业务都会根据主键再查询一次mysql库详情，数据迁移同步方案如下：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://dbaplus.cn/uploadfile/2022/1027/20221027094722258.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;①存量数据如何同步？&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;首先增加一个开关来控制操作是走mysql还是es。先关闭开关然后通过批量同步接口，根据主键id范围区间查询把存量数据分批同步到ES中。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;打开开关，这时如果有新的售后单数据，通过MQ异步同步到ES中，同时把开关打开前产生的一部分数据同步到ES中。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;最后再通过count总数校验下数据是否全部同步。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;②如何保证数据同步一致性？&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;涉及到同步数据，难免就会有数据不一致问题。从售后单申请到售后单状态变更，提交事务后每个节点都会发送一个需要同步的MQ消息。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;接收到消息后通过主键id查询mysql获取售后单详情。然后全量字段同步到ES中。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;这样不管先消费哪个节点的MQ，同步的数据都是实时查询的数据库，以此来保证每次同步的数据都是当时最新数据。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;③数据延迟怎么处理？&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;MQ消费有延时，就有可能造成ES和mysql中数据状态不一致问题。我们只是为了解决查询性能问题，因此所有复杂查询都是查的ES数据，但当商家或者客服操作售后单时会根据主键查询mysql售后单详情，然后执行审核操作。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;针对所有的业务操作后端也增加了前置状态校验，来屏蔽这种数据延时带来的问题。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;没有最好的方案，只有最适用自己业务的方案。当然现在也有一些工具类插件可以支持不同的同步方案，比如cancel基于binlog的同步以及CloudCanal。我们的目的是为了解决查询效率问题，因此选择了上面的同步方案。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; 3. 售后退货 &lt;p&gt; &lt;/p&gt; &lt;p&gt;1）合单召唤物流配送方案&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;退货退款售后单，商家或平台审核通过后，需要退回订单中货物。这里就需要与达达交互，召唤配送员走逆向取件流程。在创建运单召唤达达配送前售后这边会有一个合单逻辑。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://dbaplus.cn/uploadfile/2022/1027/20221027094736469.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;①合单思想&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;订单完成后申请售后可以分多次申请，每次可以选择不同数量的商品。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;如果用户同一个订单中商品分多次售后都申请为退货，那么在售后单审核通过后这些售后的商品都需要配送员送回商家。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;这里为了提升用户多次退货体验，也同时为了节约配送成本。因此就需要有一个合单逻辑，同一订单下的售后单退货只需召唤一次物流配送即可。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;②合单逻辑&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;合单worker定时扫描待召唤物流的售后单，当到达用户预计取件开始时间前10分钟就会触发需要合单的任务。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;合单任务会根据订单号获取此订单下所有需合单的售后单，然后获取预计取件开始时间最近的售后单。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;依据最近上门取件开始时间来创建物流运单。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;③创建运单&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;创建运单前需要前置状态校验，只处理待退货售后单。然后组装订单下用户基本信息，需要合单的所有售后单商品信息以及累计重量，创建运单。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;运单接口根据订单号做幂等处理，重复调用会返回相同的运单号。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;④接收结果&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;通过监听运单状态消息，来同步更新配送员信息。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;⑤异常重试&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;针对合单任务失败数据，记录失败标识，等待下次合单worker执行。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;记录失败次数，如果超过失败最大次数，跳过合单并预警处理。避免一直合单失败的数据影响正常合单业务数据。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; 4. 售后退款 &lt;p&gt; &lt;/p&gt; &lt;p&gt;1）退款准确性问题&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;img alt="" src="https://dbaplus.cn/uploadfile/2022/1027/20221027094749603.png"&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;通过上面的流程图了解了售后单审核退款到退款结束的一个过程。那么我们都做了哪些来保证审核退款的售后单金额是正确的呢？&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;①增加分布式锁&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;商家角色审核退款可以通过商家中心、商家端APP、系统对接接口。同时客服端也可以通过运营平台审核退款。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;因为这里也涉及多端操作，所以这里的锁主要为了防止重复审核退款。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;审核退款时已经确定是售后单维度，每个售后单只能审核退款一次，所以这里的key维度是售后单维度。并且获取不到锁直接抛出失败，提示业务异常。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;②单行商品合法性校验&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;为什么要做单行商品合法性校验呢？可以看下下面这个场景:&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;假设当前订单购买了1个A商品和2个B商品，A商品单价10元，B商品单价15元，整单金额40元。申请售后接口参数为：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt; &lt;/li&gt;&lt;/ul&gt; &lt;pre&gt;  &lt;code&gt;skuList:[{&amp;quot;skuCount&amp;quot;:1,&amp;quot;skuName&amp;quot;:&amp;quot;skuA&amp;quot;,&amp;quot;procotionType&amp;quot;:&amp;quot;1&amp;quot;},{&amp;quot;skuCount&amp;quot;:1,&amp;quot;skuName&amp;quot;:&amp;quot;skuA&amp;quot;,&amp;quot;promotionType&amp;quot;:&amp;quot;1&amp;quot;}]&lt;/code&gt;&lt;/pre&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;系统对接的商家通过到家开放平台发布的售后接口创建售后单，由于开放平台入口面对的是所有商家，每个商家系统对接能力不一样，可以看出订单中只买了1个A商品，但是传了两遍。正常我们的做法是解析入参list，然后校验每一行商品的合法性。查询当前订单已申请商品个数，以及订单中总商品个数，然后与当前审核售后单商品个数做比较。但是循环比较等于比较了两次，每次个数都是1。而且由于2个商品A总额小于订单总额，所以即使有后面的台账总额校验，还是会造成多退情况。因此这里需要根据当前申请商品总数加已申请此商品总数与订单中商品总数做校验。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;③订单台账金额校验&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;订单台账金额校验，是最后一道校验，校验的维度不同，是获取每一项支付明细剩余可退金额。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;校验当前要退售后单金额与台账余额比较，必须小于等于台账余额。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;④异步退款结果&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;审核退款后，通过异步接收退款mq来更新退款状态。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;退款成功通知下游依赖系统。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt; &lt;/p&gt;总结 &lt;p&gt; &lt;/p&gt; &lt;p&gt;逆向售后的业务是依赖于正向订单的，随着正向单不同场景玩法的增加，售后需要支持的场景也在增多，我们也在不断的迭代进步。在这当中也遇到了一些需要解决和完善的问题，比如售后系统没有自己的网关，这样会造成业务逻辑维护多处，业务不闭环。整个售后业务中各种不同场景下逻辑配置都不同，我们也在规划通过模板引擎配置做到智能化。最后也非常欢迎大家留言交流，共同进步。&lt;/p&gt; &lt;p&gt; &lt;/p&gt;作者丨姚飞涛&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62476-%E4%BA%AC%E4%B8%9C-%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84-%E8%AE%BE%E8%AE%A1</guid>
      <pubDate>Fri, 28 Oct 2022 08:45:46 CST</pubDate>
    </item>
    <item>
      <title>浅谈有赞搜索QP架构设计</title>
      <link>https://itindex.net/detail/62407-%E6%90%9C%E7%B4%A2-qp-%E6%9E%B6%E6%9E%84</link>
      <description>&lt;h1&gt;一、有赞搜索平台整体设计&lt;/h1&gt;

 &lt;p&gt;  在介绍QP前先简单介绍一下搜索平台的整体结构，方便大家快速了解QP在搜索平台中的作用。下图简单展示了一个搜索请求开始到结束的全部流程。业务通过简洁的api接入los，管理员在搜索平台新建配置并下发，完成整个搜索接入，并通过abtest验证QP带来的优化效果。   &lt;/p&gt;

 &lt;p&gt;  &lt;img alt="image" src="https://tech.youzan.com/content/images/2022/09/1-----.jpeg"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h1&gt;二、QP的作用&lt;/h1&gt;

 &lt;p&gt;   在NLP中，QP被称作Query理解（QueryParser），简单来说就是从词法、句法、语义三个层面对query进行结构化解析。这里query从广义上来说涉及的任务比较多，最常见的就是搜索系统中输入的查询词，也可以是FAQ问答或阅读理解中的问句，又或者可以是人机对话中用户的聊天输入。   &lt;br /&gt;
  在有赞，QP系统专注对查询内容进行结构化解析，整合了有赞NLP能力，提供统一对外接口，与业务逻辑解耦。通过配置化快速满足业务接入需求，同时将算法能力插件化，并支持人工干预插件执行结果。   &lt;br /&gt;
  以精选搜索为例，当用户输入衣服时用户往往想要搜的是衣服类商品，而不是衣服架，衣服配饰等衣服周边用品。通过将衣服类目进行加权，将衣服类的商品排在靠前的位置，优化用户搜索体验。
  &lt;img alt="image" src="https://tech.youzan.com/content/images/2022/09/-----2.jpg"&gt;&lt;/img&gt;
  QP目前应用在新零售，微商城、精选、爱逛买手店、分销市场、帮助中心知识库、官网搜索等场景，通过类目加权，产品词识别，搜索词纠错，同近义词召回提升用户搜索效果。&lt;/p&gt;

 &lt;h1&gt;三、QP应用整体设计&lt;/h1&gt;

 &lt;p&gt;  &lt;img alt="image" src="https://tech.youzan.com/content/images/2022/09/3-QP------.jpeg"&gt;&lt;/img&gt;
  上图完整描述了QP请求流程和配置流程的执行情况。当搜索请求到达QP时，根据请求体中的场景标记获取QP配置。QP配置中包含搜索词位置标记，插件列表，dsl改写脚本等内容。   &lt;br /&gt;
  QP根据配置，按序执行相应插件。插件在执行后，可通过干预配置以及超参数对结果进行人工干预。   &lt;br /&gt;
  QP在获取到算法插件执行结果后，根据改写配置，对搜索dsl进行改写。如将纠错词放置在搜索词同一层级，将dsl改写成fuction score结构进行类目加权。&lt;/p&gt;

 &lt;h1&gt;四、QP应用分层设计&lt;/h1&gt;

 &lt;p&gt;  &lt;img alt="image" src="https://tech.youzan.com/content/images/2022/09/4-QP------.jpeg"&gt;&lt;/img&gt;
上图按照请求流程从上到下展示了QP的分层设计，接下来将简单描述各层作用：   &lt;br /&gt;
  &lt;strong&gt;controller层&lt;/strong&gt;：查询改写服务入口，对请求做预处理。   &lt;br /&gt;
  &lt;strong&gt;service层&lt;/strong&gt;：根据场景获取QP改写配置，获取dsl里的搜索词，调用相应的插件返回qp结果。   &lt;br /&gt;
  &lt;strong&gt;plugin层&lt;/strong&gt;：负责算法插件执行，调用插件对应的算法实现handler，对算法结果做干预并针对调用成功或者失败做处理。   &lt;br /&gt;
  &lt;strong&gt;handler层&lt;/strong&gt;：算法具体实现放置在该层，该层会依赖各种算法服务（如小盒子，Milvus等）。   &lt;br /&gt;
  &lt;strong&gt;Intervener层&lt;/strong&gt;：负责对handler结果做人工干预。   &lt;br /&gt;
  &lt;strong&gt;processor层&lt;/strong&gt;：根据QP改写配置，调用改写插件，完成dsl的改写。&lt;/p&gt;

 &lt;h2&gt;五、QP算法插件设计&lt;/h2&gt;

 &lt;h3&gt;5.1 预处理Preprocess插件&lt;/h3&gt;

 &lt;p&gt;  按照配置规则对搜索词进行预处理，预处理方式如下：   &lt;br /&gt;
  * 删除特殊符号 &amp;quot; “ \ 等；   &lt;br /&gt;
  * 大写转小写，全角转半角；   &lt;br /&gt;
  * 连续英文联合切分，连续数字联合切分，其余单独切分；   &lt;br /&gt;
  * 默认截取list前50个字/词；   &lt;br /&gt;
  * 将list拼接成一个字符串。  &lt;/p&gt;

 &lt;h4&gt;样例&lt;/h4&gt;

 &lt;pre&gt;  &lt;code&gt;输入：&amp;quot;史蒂夫新款\时尚套装夏修身圆领百搭钩花DWF镂空雪纺两件套套裙；&amp;quot;
输出：&amp;quot;史蒂夫新款时尚套装夏修身圆领百搭钩花dwf镂空雪纺两件套套裙&amp;quot;
&lt;/code&gt;&lt;/pre&gt;

 &lt;h3&gt;5.2 纠错Correction插件&lt;/h3&gt;

 &lt;p&gt;  纠错插件的作用是对搜索词中错误内容进行识别，返回正确内容。&lt;/p&gt;

 &lt;h4&gt;样例&lt;/h4&gt;

 &lt;pre&gt;  &lt;code&gt;输入：[上海牛黄皂]
输出：[上海硫磺皂]
&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;  当用户输入“上海牛黄皂”时，通过纠错插件能正确输出“上海硫磺皂”，其技术架构如下图所示。
  &lt;img alt="image" src="https://tech.youzan.com/content/images/2022/09/5-----.jpeg"&gt;&lt;/img&gt;
1、纠错模型在bert基础上采用知识蒸馏，提升模型精度降低模型时延。   &lt;br /&gt;
2、根据同音字召回候选集，使用tri-gram语言模型对候选集排序。&lt;/p&gt;

 &lt;h3&gt;5.3 细粒度分词Tokenizer插件（基础分词）&lt;/h3&gt;

 &lt;h4&gt;样例&lt;/h4&gt;

 &lt;pre&gt;  &lt;code&gt;输入：[雪地靴女2020年新款皮毛一体冬季加绒加厚防滑东北厚底保暖棉鞋子]
输出：[雪地 靴 女 2020 年 新款 皮毛 一体 冬季 加绒 加厚 防滑 东北 厚底 保暖 棉 鞋子]
&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;  该分词插件由Java版结巴 jieba-analysis 修改而来，修改内容如下：   &lt;br /&gt;
  *   从全网商品标题数据，有赞行业数据，开源数据中统计出词频，作为基础分词词典；   &lt;br /&gt;
  *   解决词典中由英文单词导致英文字符串被分开的问题；   &lt;br /&gt;
  *   限制DAG的长度，即匹配词的长度，以此控制分词粒度，目前默认是2，分出来的词除英文和数字外，长度不会超过2。&lt;/p&gt;

 &lt;h3&gt;5.4 语义分词sementicSegment插件&lt;/h3&gt;

 &lt;h4&gt;样例&lt;/h4&gt;

 &lt;pre&gt;  &lt;code&gt;输入：[雪地 靴 女 2020 年 新款 皮毛 一体 冬季 加绒 加厚 防滑 东北 厚底 保暖 棉 鞋子]
输出：[雪地靴 女 2020年 新款 皮毛一体 冬季 加绒加厚 防滑 东北 厚底 保暖 棉鞋子]
&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;  该插件在细粒度分词的基础上，通过模型生成语义树将关联度大的词列表进行合并，输出语义分词结果。在样例中，雪地与靴关联度更大，所以在语义分词中将雪地与靴合并输出。&lt;/p&gt;

 &lt;h3&gt;5.5 实体识别Tagging插件&lt;/h3&gt;

 &lt;h4&gt;样例&lt;/h4&gt;

 &lt;pre&gt;  &lt;code&gt;输入：[&amp;quot;汽车&amp;quot;,&amp;quot;脚垫&amp;quot;,&amp;quot;刷子&amp;quot;]
输出：[{&amp;quot;word&amp;quot;:&amp;quot;汽车&amp;quot;,&amp;quot;tag&amp;quot;:&amp;quot;产品修饰词&amp;quot;},{&amp;quot;word&amp;quot;:&amp;quot;脚垫&amp;quot;,&amp;quot;tag&amp;quot;:&amp;quot;产品修饰词&amp;quot;},{&amp;quot;word&amp;quot;:&amp;quot;刷子&amp;quot;,&amp;quot;tag&amp;quot;:&amp;quot;产品词&amp;quot;}]
&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;  实体识别插件主要用于识别出搜索内容中的产品词。比如用户在输入“汽车脚垫刷子”时，如果没有做产品词识别，“脚垫”相关的商品会因为商品分高而排在“刷子”商品前面，影响用户搜索体验。   &lt;br /&gt;
  反之，经过命名实体识别，对“刷子”做产品词提权，刷子类商品就可以排在脚垫类商品前面，优化搜索体验。   &lt;br /&gt;
  目前有赞规划的实体类别列表如下所示：&lt;/p&gt;

 &lt;pre&gt;  &lt;code&gt;产品词 eg：“修身连衣裙”中的“连衣裙”
产品修饰词 eg：“汽车脚垫”中的“汽车”
普通词
新词
修饰
品牌
机构实体
地点地域
材质
人名
功能功效
专有名词
影视名称
型号
文娱书文曲
系列
游戏名称
款式元素
颜色
场景
风格
营销服务
人群
时间季节
性别
类目
母婴
规格
新品
前缀
后缀
数字
符号
&lt;/code&gt;&lt;/pre&gt;

 &lt;h4&gt;实体识别方法&lt;/h4&gt;

 &lt;p&gt;    &lt;strong&gt;基于正则&lt;/strong&gt;可以识别数字、符号、规格、时间季节。&lt;/p&gt;

 &lt;pre&gt;  &lt;code&gt;// 数字
private numWordRegex = &amp;quot;[0-9]+&amp;quot;;  
// 符号
private String symbolWordRegex = &amp;quot;[\\[\\]\\{\\}【】「」\\\\\\|、｜‘&amp;apos;\&amp;quot;“”’；：;:&amp;gt;.。》/？\\?&amp;lt;《，,~`·！\\!@#\\$¥%\\^…&amp;amp;\\*（\\(\\)）\\-_—=\\+\\s]+&amp;quot;;  
// 规格
private String unitRegex = &amp;quot;(?:\\d+|\\d+\\.\\d+|[一二三四五六七八九十百千万]+)\\s*(?:m|米|cm|厘米|ml|毫升|l|升|度|平米|件|块|元|片|张|本|条|瓶|部|辆|个|桶|包|盒|g|克|kg|千克|吨|寸|斤)&amp;quot;;  
// 时间季节
private String seasonRegex = &amp;quot;[春夏秋冬]+[季天]?&amp;quot;;  
private String yearRegex = &amp;quot;(?:18|19|20)\\d{2}&amp;quot;;  
&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;    &lt;strong&gt;基于词库&lt;/strong&gt;可以识别产品词、品牌、产品修饰词。  &lt;/p&gt;

 &lt;pre&gt;  &lt;code&gt;品牌：query-&amp;gt;二级类目-&amp;gt;品牌，条件：在当前类目品牌词库里且模型预测不是“产品词”，此时打“品牌”实体标。    
产品词：在产品词库且模型预测是普通词。   
产品修饰词：多个词出现时，除最后一个，其余打“产品修饰词”实体标。  
&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;    &lt;strong&gt;基于模型&lt;/strong&gt;可以识别剩余23类实体，类别如下所示：&lt;/p&gt;

 &lt;pre&gt;  &lt;code&gt;产品词
普通词
新词
修饰
品牌
机构实体
地点地域
材质
人名
功能功效
专有名词
影视名称
型号
文娱书文曲
系列
游戏名称
款式元素
颜色
场景
风格
营销服务
人群
时间季节
&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;  模型结构：
  &lt;img alt="image" src="https://tech.youzan.com/content/images/2022/08/6-----.png"&gt;&lt;/img&gt;&lt;/p&gt;

 &lt;h3&gt;5.6 类目预测categoryPredict插件&lt;/h3&gt;

 &lt;h4&gt;样例&lt;/h4&gt;

 &lt;pre&gt;  &lt;code&gt;输入：牛奶绒
输出: {
        &amp;quot;categoryId&amp;quot;: &amp;quot;101000010001&amp;quot;,
        &amp;quot;categoryName&amp;quot;: &amp;quot;被套&amp;quot;,
        &amp;quot;categoryChainList&amp;quot;: [
            &amp;quot;家居建材&amp;quot;,
            &amp;quot;床上用品&amp;quot;,
            &amp;quot;被套&amp;quot;
        ],
        &amp;quot;parentCategoryId&amp;quot;: &amp;quot;10100001&amp;quot;,
        &amp;quot;level&amp;quot;: 3,
        &amp;quot;hasChildren&amp;quot;: true,
        &amp;quot;percent&amp;quot;: 0.9010684490203857
    }
&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;该插件会根据用户的搜索内容输出类目结果，主要应用在类目加权上。   &lt;br /&gt;
例如当用户在有赞精选上输入牛奶绒，期望返回牛奶绒床单。   &lt;br /&gt;
未使用类目加权，返回的商品大多为牛奶相关产品，不符合用户的搜索期望。   &lt;br /&gt;
使用类目加权后，将床上用品类产品提权，返回的商品牛奶绒床单，符合用户期望。   &lt;br /&gt;
类目预测模型是在对比学习基础上实现，具体内容可看  &lt;a href="http://tech.youzan.com/dui-bi-xue-xi-zai-you-zan-d/"&gt;对比学习在有赞的应用&lt;/a&gt;   &lt;/p&gt;

 &lt;h3&gt;5.7 同近义词插件&lt;/h3&gt;

 &lt;h4&gt;样例&lt;/h4&gt;

 &lt;pre&gt;  &lt;code&gt;输入：[衬衣]
输出：[衬衫]
&lt;/code&gt;&lt;/pre&gt;

 &lt;p&gt;  同近义词插件目前非常实现轻量，通过离线同义词表，搜索内容中的产品词作为输入，输出同义词。&lt;/p&gt;

 &lt;h1&gt;六、总结与展望&lt;/h1&gt;

 &lt;p&gt;  本文从QP整体设计，分层设计，插件设计较为完整的介绍了QP的架构设计。目前经过一年多的迭代，QP已经实现业务场景小时级接入，优化了零售，微商城，精选，爱逛，分销等场景搜索效果。后续将会继续丰富算法插件能力同时完成QP可视化配置能力方便业务自主接入。&lt;/p&gt;

 &lt;p&gt;本文由吴鑫强，任艳萍负责收集整理。&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>大数据 搜索</category>
      <guid isPermaLink="true">https://itindex.net/detail/62407-%E6%90%9C%E7%B4%A2-qp-%E6%9E%B6%E6%9E%84</guid>
      <pubDate>Mon, 05 Sep 2022 13:27:28 CST</pubDate>
    </item>
    <item>
      <title>mysql 的精要设计</title>
      <link>https://itindex.net/detail/62405-mysql-%E8%AE%BE%E8%AE%A1</link>
      <description>&lt;hr&gt;&lt;/hr&gt;
 &lt;h2&gt;theme: channing-cyan&lt;/h2&gt;
 &lt;h2&gt;写入流程&lt;/h2&gt;
 &lt;pre&gt;  &lt;code&gt;update user set name = &amp;apos;李四&amp;apos; where id = 100
&lt;/code&gt;&lt;/pre&gt;
 &lt;ol&gt;
  &lt;li&gt;
   &lt;p&gt;先将 id = 100 这个磁盘数据页读取到 buffer pool 中&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;然后插入一条 undolog 日志记录变更前的数据 name = &amp;apos;张三&amp;apos;&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;随后修改 Buffer Pool 中的值，name = &amp;apos;李四&amp;apos;&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;然后将 Buffer pool 变更写入到 redo log 中 Prepare 阶段&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;然后写入 Binlog&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;最后写入 redo log commit 标识&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;h3&gt;写入优化&lt;/h3&gt;
 &lt;p&gt;可以发现在每次执行写入的时候都需要先将更新所在的数据加载到缓存中，导致写入行为变得比较慢，所以有 change buffer 的优化方案&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;
   &lt;p&gt;检查 id = 100 如果在内存中就更新 buffer pool 如果不在的话将更新数据插入 change buffer 中&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;然后插入一条 undolog 日志记录变更前的数据 name = &amp;apos;张三&amp;apos;&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;然后将 Buffer poll 或者 change buffer 记录的变更写入到 redo log，prepare 阶段&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;然后写入 Binlog&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;最后写入 redo log commit 标识&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;由于每次不需要读取磁盘数据所以存在大量写的情况下性能很高&lt;/p&gt;
 &lt;p&gt;change buffer 也是会写入磁盘中的进行持久化存储的&lt;/p&gt;
 &lt;h3&gt;什么情况下 change buffer 无法使用&lt;/h3&gt;
 &lt;p&gt;当插入或者修改的数据被标记为唯一索引的时候，此时必须从磁盘去检索目标数据是否满足唯一性校验，所以无法使用 change buffer&lt;/p&gt;
 &lt;h3&gt;change buffer 对查询的影响&lt;/h3&gt;
 &lt;p&gt;由于将变更的数据写入了 change buffer，磁盘中留存的是老数据，所以写一次查询请求过来的时候，首先需要从磁盘读取数据，然后再执行 merge change buffer 操作&lt;/p&gt;
 &lt;p&gt;而读取 change buffer 又会涉及到磁盘 IO 所以当修改少读取多的时候会导致性能大幅度下降&lt;/p&gt;
 &lt;h2&gt;数据存储&lt;/h2&gt;
 &lt;h3&gt;在磁盘中&lt;/h3&gt;
 &lt;p&gt;以 InnoDB 为例，数据索引分为聚簇索引、普通索引、唯一索引&lt;/p&gt;
 &lt;p&gt;其中聚簇索引将索引和数据存储在一起为整个完整的数据&lt;/p&gt;
 &lt;p&gt;非主键索引只是存储了索引和主键ID字段数据&lt;/p&gt;
 &lt;p&gt;索引的实现机制为 B+ 树，非叶子节点存储索引、叶子节点存储数据，在底层磁盘存放数据的时候索引也是按照数据块将索引数据存储在一起的通过存储下一个节点的指针进行定位，所以每当加载一个数据页的时候能够读取到大量的索引，再基于索引做二分搜索就能快速定位到数据所在的下一层索引区间或者数据区间&lt;/p&gt;
 &lt;p&gt;同时数据也是通过磁盘数据块进行存储并且通过指针进行连接，同时由于 B+ 树的特性，底层数据是按照索引从小到大的顺序挨个存放的&lt;/p&gt;
 &lt;h3&gt;在内存中&lt;/h3&gt;
 &lt;p&gt;数据在内存中存储在 Buffer Pool 中，就是还原了读取磁盘中的数据为一颗 B+ 树，只不过 Buffer Pool 是有固定大小的，当写满了之后就需要进行数据页淘汰&lt;/p&gt;
 &lt;h3&gt;数据何时进入磁盘&lt;/h3&gt;
 &lt;ol&gt;
  &lt;li&gt;Buffer Pool 刷脏页：后台定时刷，关机刷，满了刷。在刷脏页的时候同时也会推进 redo log checkpoint&lt;/li&gt;
  &lt;li&gt;redo log 写满了必须进行推进 redo log 中记录的变更到磁盘中：当 redo log 写满了之后需要停止所有写操作来进行推进 checkpoint&lt;/li&gt;
&lt;/ol&gt;
 &lt;h2&gt;查询流程&lt;/h2&gt;
 &lt;p&gt;SQL 解析器解析 SQL 检查语法是否正确&lt;/p&gt;
 &lt;p&gt;查询优化器为查询选择合适的索引&lt;/p&gt;
 &lt;p&gt;执行器调用存储引擎执行 SQL&lt;/p&gt;
 &lt;p&gt;存储引擎读取磁盘数据加载内存 buffer pool 中过滤后返回给执行器&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0a611089203545fe91eed2376698bc09~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;数据检索&lt;/h2&gt;
 &lt;h3&gt;单表检索&lt;/h3&gt;
 &lt;p&gt;检索数据分为以下几种检索方式&lt;/p&gt;
 &lt;p&gt;（1）根据主键索引检索&lt;/p&gt;
 &lt;p&gt;（2）根据普通索引检索&lt;/p&gt;
 &lt;p&gt;（3）根据唯一索引检索&lt;/p&gt;
 &lt;p&gt;索引的检索都是在 B+ 树中进行解锁，B+ 树这个数据结构特征决定了可以快速定位到要检索的数据，同时也只能进行左匹配搜索、默认是有序的，内存中可以存储大量的索引非叶子节点，叶子节点的数据都是由链表串联起来的&lt;/p&gt;
 &lt;p&gt;假设 mysql 表中有 2千万数据，主键为 int 4 字节，指针 6 字节，一个数据页默认为 16KB，那么一个数据页能够存储 (16 * 1024) / (4 + 6) = 1638 个索引&lt;/p&gt;
 &lt;p&gt;3层高的 B+ 树可以存放 1638 * 1638 = 268万个索引，假设每一行数据占据 1KB，1个  &lt;strong&gt;叶子节点&lt;/strong&gt;能存储 16 行数据，那么 3 层高的索引一共能存储 268 万 * 16 = 4300万行数据&lt;/p&gt;
 &lt;p&gt;如果索引没有在磁盘中，理论上也只需要 3次随机磁盘 IO 就能定位到要获取的数据，同时读取了索引或者数据之后会在磁盘进行缓存，假设索引节点都已经存在内存中，那么只需要一次磁盘 IO 就能获取到随机（索引等值查询）&lt;/p&gt;
 &lt;p&gt;主键索引的特征是叶子节点存储了完整的数据，不能重复&lt;/p&gt;
 &lt;p&gt;非主键索引特征是只存储索引和主键 ID 值&lt;/p&gt;
 &lt;p&gt;对于普通索引，索引区分度决定了其检索效率如何，检索到对应的索引字段后还会检索后检查直到检查到了一条不满足条件的数据&lt;/p&gt;
 &lt;h3&gt;多表检索&lt;/h3&gt;
 &lt;p&gt;当进行多表关联查询的时候，需要基于驱动表的数据去检索被驱动表的数据，所以驱动表和被驱动表需要被扫描的次数以及其数据量决定了关联查询的效率&lt;/p&gt;
 &lt;p&gt;为了优化查询优化器会选择以数据量小的作为驱动表&lt;/p&gt;
 &lt;p&gt;假设执行如下 sql 根据身份证检索出其所有好友信息&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;select a.* from a left join b on a.name = b.name
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;strong&gt;场景一：假设 a 表 name 为普通索引，b 表 name 没有设置索引&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;检索一次 a 表过滤出 name = zhangsan 的信息扫描出来了 100 条数据&lt;/p&gt;
 &lt;p&gt;将这 100 条数据全部放入 join_buffer 中(因为是 select a.*)&lt;/p&gt;
 &lt;p&gt;然后去全表扫描 friends，每取一条数据就跟 join_buffer 中的数据进行对比，满足条件的数据就存放到 net_buffer 中&lt;/p&gt;
 &lt;p&gt;join_buffer 默认为 256K 由 join_buffer_size 控制，当 join_buffer 存放不小的时候就开始处理当前的数据检索，处理完毕后清空 join_buffer 继续读取后续的数据&lt;/p&gt;
 &lt;p&gt;net_buffer 被写满或者 friends 扫描完毕之后，就会将数据发送给客户端&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;mysql 是边读边发的利用 net buffer 作为应用的缓冲区，如果不利用 net buffer 等技术将全部结果放入内存中可能会将内存打爆
net buffer 默认 16K 大小&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;这种无法利用被驱动表的索引的查询叫做   &lt;strong&gt;Block Nested-Loop Join&lt;/strong&gt;&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;实际工作场景中几乎不可能用这样无法使用到索引的关联查询的，尤其是在驱动表中检索出来的数据量比较大的情况，因为驱动表一条数据就会进行一次被驱动表的全表扫描&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;  &lt;strong&gt;场景二：假设 a 表 name 为普通索引，b 表 name 设置了索引&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;检索一次 user 表过滤出 name = zhangsan 的信息得到对应行信息，然后基于 name 索引去检索 friends 表，每扫描一条数据就放入 net_buffer&lt;/p&gt;
 &lt;p&gt;这种可以用上被驱动表的索引的查询叫做   &lt;strong&gt;Index Nested-Loop Join&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;由于在普通索引中查询到的数据只有索引数据和主键 ID 值，同时索引是有序的但是检索出来的主键 ID 可能是无序的，这里是 select * 所以需要进行回表返回完整数据的时候，可能就需要多次随机 IO 查询数据了&lt;/p&gt;
 &lt;p&gt;为了优化回表导致的随机 IO 采用  &lt;strong&gt;Multi-Range Read&lt;/strong&gt;，将驱动表读取到的所有数据 id 值放入 read_rnd_buffer 中，然后进行排序，最后基于有序的数据去检索被驱动表的数据，因为是有序的所以  &lt;strong&gt;有可能更能高效利用 buffer_pool&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;read_rnd_buffer 由 read_rnd_buffer_size 控制优化器策略，判断消耗的时候，会更倾向于不使用 MRR，要默认全部开启的话需要设置 set optimizer_switch=&amp;quot;mrr_cost_based=off&amp;quot;&lt;/p&gt;
 &lt;p&gt;跟 join_buffer 类似如果 read_rnd_buffer 写满了的话，就先处理当前的数据，然后清空继续读取后续数据&lt;/p&gt;
 &lt;h2&gt;排序&lt;/h2&gt;
 &lt;p&gt;由于 B+ 树默认就是按照数据从小到达组织的所以通过索引 order by asc/desc 返回的数据天然有序&lt;/p&gt;
 &lt;p&gt;如果 order by column 字段没有索引的话，首先需要初始化 sort_buffer 内存，放入查询的字段，然后继续索引检索到满足条件的数据放入 sort_buffer 中，然后在 sort_buffer 基于快速排序算法进行排序，最后返回结果给客户端&lt;/p&gt;
 &lt;p&gt;sort_buffer_size 控制 sort_buffer 大小，如果内存中存放不下就需要借助磁盘文件进行排序，将有序数据输出到多个文件中，最后采用归并排序算法将多个有序小文件合并进行输出&lt;/p&gt;
 &lt;h2&gt;事务控制&lt;/h2&gt;
 &lt;h3&gt;事务的 ACID&lt;/h3&gt;
 &lt;p&gt;  &lt;strong&gt;原子性（Atomicity）&lt;/strong&gt;：事务操作要么整个事务里面的操作全部成功要么全部失败&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;一致性（Consistency）&lt;/strong&gt;：事务操作不伦成功或者失败完成性必须保持一致，比如成功后数据、索引全部维护正确&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;隔离性（Isolation）&lt;/strong&gt;：事务之间是互相隔离的&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;持久性&lt;/strong&gt;：事务提交的数据会持久化到磁盘中&lt;/p&gt;
 &lt;h3&gt;事务的隔离级别&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;p&gt;  &lt;strong&gt;可重复度&lt;/strong&gt;：在整个事务过程中读取到的数据始终保持一致&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;串行化&lt;/strong&gt;：事务串行化排队挨个处理&lt;/p&gt;
 &lt;h3&gt;update 的工作原理&lt;/h3&gt;
 &lt;p&gt;理解事务以及回滚需要先简单理解增删改的工作原理&lt;/p&gt;
 &lt;p&gt;mysql 会为表创建 2 个隐藏的字段&lt;/p&gt;
 &lt;p&gt;（1）trx_id：表示当前行是哪一个事务更新的&lt;/p&gt;
 &lt;p&gt;（2）roll_pointer：指向上一个版本的 undo log&lt;/p&gt;
 &lt;p&gt;如果是 insert 的话 undo log 指向的就是空数据、如果是 delete undo log 指向的就是原数据&lt;/p&gt;
 &lt;p&gt;所以可以理解 msyql 将我们每一次变更通过 undo log 指针串起来了，便于回滚&lt;/p&gt;
 &lt;h3&gt;可重复读的实现原理&lt;/h3&gt;
 &lt;p&gt;默认开启事务后第一次执行 SQL 语句的时候创建一个 readView 一致性快照，后续所有的查询都是基于快照进行查询，这样就能保证同样的条件在事务前后读取到的数据总是一样的&lt;/p&gt;
 &lt;p&gt;那么这个 readView 的实现原理就是可重复读取的关键，创建 readView 的数据结构如下&lt;/p&gt;
 &lt;p&gt;（1）m_ids: s事务开启这一刻系统活跃事务 id 列表&lt;/p&gt;
 &lt;p&gt;（2）min_trx_id：m_ids 中最小值&lt;/p&gt;
 &lt;p&gt;（3）max_trx_id：m_ids 中最大值 + 1&lt;/p&gt;
 &lt;p&gt;（4）creator_trx_id：当前事务 ID&lt;/p&gt;
 &lt;p&gt;基于 readView 查询的工作原理，首先检查当前行是否是自己修改的，然后在基于 undo log 指针检查变更记录&lt;/p&gt;
 &lt;p&gt;（1）trx_id 等于 creator_trx_id 是自己更新的可见&lt;/p&gt;
 &lt;p&gt;（2）trx_id 在 m_ids 中，并且不是自己更新的不可见&lt;/p&gt;
 &lt;p&gt;（3）trx_id 小于 min_trx_id，表明这个版本的数据是当前事务开启之前提交的可见&lt;/p&gt;
 &lt;p&gt;（4）大于 max_trx_id，表明是当前事务开启之后开启的不可见&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;undo log 过多的问题&lt;/strong&gt;：如果每次都改全部都要维护 undo log 指针是不太现实的，所以肯定需要在合理的时机进行删除，这个时机就是当前系统中所有 readView 中的 min_trx_id 之前的 undo log 指针和数据可以进行清空了&lt;/p&gt;
 &lt;h3&gt;读已提交的工作原理&lt;/h3&gt;
 &lt;p&gt;读已提交的工作原理也是根据 readView 来实现的，只不过是创建的时机不同，可重复读在第一次执行 SQL 的时候创建全局只使用一个视图，而读已提交在每次执行 SQL 的时候都会创建一个 readView&lt;/p&gt;
 &lt;h3&gt;当前读&lt;/h3&gt;
 &lt;p&gt;如果在读已提交的隔离级别中，想要读取到最新的版本的数据可以基于当前读特性来读取最新版本的数据&lt;/p&gt;
 &lt;p&gt;这个数据会对 id = 1 的数据加写锁，同时会读取最新版本的数据，如果此时有其它事务正在修改这条数据（也会写写锁）那么就会阻塞等待对方事务提交，然后读取到最新的值&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;select * from table where id = 1 for update
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;还可以对数据加共享锁（可重复加锁，但是读写锁互斥）也是基于当前读的特性&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;select * from table where id = 1 lock in share mode
&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;幻读&lt;/h3&gt;
 &lt;p&gt;mysql 事务在可重复度这个隔离级别下存在幻读的问题&lt;/p&gt;
 &lt;h2&gt;锁&lt;/h2&gt;
 &lt;p&gt;数据准备&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;
 
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;mysql 中的锁按照种类划分为  &lt;strong&gt;读锁&lt;/strong&gt;、  &lt;strong&gt;写锁&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;按照锁的粒度来进行划分  &lt;strong&gt;库全局锁&lt;/strong&gt;、  &lt;strong&gt;表锁&lt;/strong&gt;、  &lt;strong&gt;行锁&lt;/strong&gt;、  &lt;strong&gt;间隙锁&lt;/strong&gt;、  &lt;strong&gt;元数据锁（MDL锁）&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;读锁可以重复加锁，读写锁互斥、写写锁互斥&lt;/p&gt;
 &lt;p&gt;粒度划分的每个锁都可以跟读锁、写锁进行组合，如行写锁（对主键 id 执行 for update 或者 update table），如元数据读锁（执行 crud 都会加元数据读锁用于和 ddl 互斥）&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;全局锁&lt;/strong&gt;：Flush tables with read lock，加了读锁会阻塞所有的增删改、数据库 ddl 的执行（因为它们需要加写锁，读写互斥）&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;表锁&lt;/strong&gt;：lock table read/write 对表加读锁或者写锁&lt;/p&gt;
 &lt;p&gt;（1）read：会阻塞 table 的写语句，不会阻塞 table 的 select（表读锁之后 select 还可以加读锁，因为读锁可以重入）&lt;/p&gt;
 &lt;p&gt;（2）write：会阻塞 table 的读写语句（读写锁互斥嘛，因为写语句会加行写锁，但是表写锁已经加了）&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;行锁&lt;/strong&gt;：&lt;/p&gt;
 &lt;p&gt;（1）行写锁：执行 update table id = 10&lt;/p&gt;
 &lt;p&gt;（2）行写锁：select * from table id = 10 for update&lt;/p&gt;
 &lt;p&gt;（3）行读锁：select * from table id = 10 lock in share model&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;间隙锁&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;1. 如果精准根据主键 ID 进行 update&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;那么只需要加行锁如   &lt;code&gt;update t set d = d + 1 where id = 5&lt;/code&gt; 只会加 id = 5 那一行的锁&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;2. 如果主键 ID 没有定位到值&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;那么需要加间隙锁   &lt;code&gt;update t set d = d + 1 where id = 7&lt;/code&gt; 会在 (5,10) 这个区间加锁，因为 id = 7 没有定位到数据，为了保证可重复度特性必须加间隙锁防止在这个间隙添加到 id = 7 的数据导致后续再次执行 update 时候语义混乱&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;3. 如果根据普通索引进行 update&lt;/strong&gt;
  &lt;code&gt;update t set d = d + 1 where c = 5&lt;/code&gt; 这种情况下会加 (0, 10) 的间隙锁 + 行锁，因为普通索引不具备主键索引或者唯一索引的唯一性，是可能存在重复的，为了防止当前数据的前置区间或者后置区间再次出现 c = 5 的数据，所以需要将 (0, 5]，（5， 10) 这 2 个区间进行加锁&lt;/p&gt;
 &lt;p&gt;next-key lock 是间隙锁 + 行锁的称呼，总之  &lt;strong&gt;加锁间隙锁是为了保证可重复度的语义，只会在可重复隔离级别下存在，所以说读已提交的性能更高&lt;/strong&gt;，在可能破坏可重复度的语义场景中都会加间隙锁&lt;/p&gt;
 &lt;h2&gt;读写分离&lt;/h2&gt;
 &lt;p&gt;可以通过做读写分离来提高系统的吞吐量、同时还可以在主库宕机后提升 slave 为主库来保证系统的高可用&lt;/p&gt;
 &lt;p&gt;master/slave 架构下如何保证数据一致性是很多分布式系统都面临的一个问题，大部分都是基于集群中过半节点写入 os cache 成功就进行返回，我们来看下 Mysql 中的实现机制&lt;/p&gt;
 &lt;p&gt;主从数据同步机制如下&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/983427a8ef904fe1a2a0024af93d121b~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在 mysql5.6 之前就是采用这种模式进行通信，即主库支持并发，从库同步处理 binlog 的时候基于单线程进行处理，当主库 TPS 过高的时候，从库会存在较高的延迟&lt;/p&gt;
 &lt;p&gt;在 mysql5.7.22 支持的并行复制策略，主要的思想是&lt;/p&gt;
 &lt;p&gt;（1）通过 WRITESET 记录对于事务涉及更新到的每一行数据，如果 2 个事务没有涉及到同一行的数据变更（即两个事务的 WRITESET 没有交集）表明可以并行执行（WRITESET 会在提交事务的时候写入到 binlog）&lt;/p&gt;
 &lt;p&gt;（2）同时处于 prepare 的事务可以并行执行，同时处于 prepare 和 commit 的事务可以并行执行&lt;/p&gt;
 &lt;p&gt;（3）WRITESET_SESSION 对于 WRITESET 的约束，即在主库上同一个线程先后执行的两个事务，在备库执行的时候，要保证相同的先后顺序&lt;/p&gt;
 &lt;p&gt;就算支持多线程同步了，然后也是没有保证数据一致性的，没有保证数据一致性，那么在主库宕机切换从库的时候就可能导致数据丢失&lt;/p&gt;
 &lt;p&gt;所以有了  &lt;strong&gt;半同步机制（semi-sync replication）&lt;/strong&gt;：数据写入到 binlog 之后再从库同步完成之后会响应 ack，主库只要收到了一条 ack 指令就给客户端响应成功&lt;/p&gt;
 &lt;p&gt;其实绝大部分机器保证数据一致性的方案都是过半写入从库，但是大都是逻辑简答的内存读写或者磁盘顺序读写，操作非常快，所以延时很低，对于客户端的 TPS 依然很高，但是对于 MySQL 这种系统同步执行事务大都是几百 MS，这样会严重影响主库的 TPS 吞吐量，所以对于 MySQL 来说大都还是采用异步复制方案，如果真的宕机了需要运维通过脚本将主库未同步的 binlog 在某一个从库执行完毕之后，再手动提升从库为主库即可&lt;/p&gt;
 &lt;p&gt;读写分离在客户端的实现方案可以是基于客户端模式，返回给一个 MySQL 主库的 proxy 节点，proxy 结点维护与 master 的连接，当 master 宕机后切换到另外一个数据库，保证客户端可以实现平滑过渡，对于 slave 库也可以同样返回 proxy 节点（proxy 节点只做数据转发）&lt;/p&gt;
 &lt;p&gt;读写分离在客户端的实现方案还可以选用中间件代理层模式，比如 mycat 用户只需要配置与 mycat 的连接，底层数据的宕机和切换均由 mycat 感知和切换（mycat 要负责解析 SQL 重量级的 Proxy 层）&lt;/p&gt;
 &lt;h2&gt;分库分表&lt;/h2&gt;
 &lt;p&gt;单库面临的最严重的问题就是资源优先包括存储空间、带宽、CPU 等等，在海量数据和大量 TPS 的情况下需要分库分表来解决问题&lt;/p&gt;
 &lt;p&gt;分库分表之前必须要做好的一件事情就是预估后期可能会存在的数据量尽量在分库分表的时候一次性设计好后面很多年都能用的策略，因为存储层分库分表扩容是一个非常麻烦的过程&lt;/p&gt;
 &lt;p&gt;比如我们用户表 2 亿用户从一开始就设计了 64 个库每个库 8 张表的设计，每张表存储 40万数据左右，每个数据库存储了 320 万用户，并且按照单表 500 万来计算可以满足存储 20多亿的用户，同时设计这么多库的原因希望支撑规划场景下的数万或者数十万的 TPS，以及海量的 QPS&lt;/p&gt;
 &lt;p&gt;分库分表中有以下几种设计&lt;/p&gt;
 &lt;p&gt;（1）将相关联的数据通关相关联的属性值关联路由存储到同一个数据库中&lt;/p&gt;
 &lt;p&gt;比如用户、订单、支付在单号设计上都混淆上 user 的后 4 位数，这样在路由的时候统一都根据这 4 位 user_id 关键值 % 数据库就路由到同一个数据库中，这样就可以借鉴本地事务来避免出现分布式事务的问题&lt;/p&gt;
 &lt;p&gt;同时用户在查看自己的订单时候根据 user_id 关键字路由后，可以直接进行简单的分页查询&lt;/p&gt;
 &lt;p&gt;（2）通过单数据源MySQL/ES/缓存等维护数据与服务器之间的存储关系&lt;/p&gt;
 &lt;p&gt;比如用户和订单分别根据各自的单号进行路由存储到不同的数据库中，然后在 ES 中做一个宽表存储买家、卖家、订单、商品、创建时间等等信息&lt;/p&gt;
 &lt;p&gt;此时买家卖家查看订单的时候，只需要根据自身编号过滤然后通过时间排序即可&lt;/p&gt;
 &lt;p&gt;但是在处理事务问题时候需要注意的是&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;分布式事务问题：由于写入分散到不同的数据源中可以基于 seata、mq 最终一致性、可靠消息服务来保证，同时配合手动补偿&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;数据实时性问题：由于 ES 同步总是存在延迟的，所以需要一般是需要基于 MySQL 中实时的数据进行处理，这个时候可以考虑单独针对这样的场景将数据写入到一个公共的数据源中再返回给客户端成功（需要评估单点写入压力，为极少数功能进行配置），或者在处理的时候尽量多冗余数据，在业务处理的时候直接就拥有的数据的路由键&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;（3）复杂业务&lt;/p&gt;
 &lt;p&gt;复杂业务可以基于 ES 存储宽表索引数据&lt;/p&gt;
 &lt;p&gt;客户端的框架可以选用 sharding 灵活的实现按照不同的或者自定义策略路由库表、配置单库路由、复杂检索等功能&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62405-mysql-%E8%AE%BE%E8%AE%A1</guid>
      <pubDate>Sun, 04 Sep 2022 17:21:52 CST</pubDate>
    </item>
    <item>
      <title>Java 技术栈中间件优雅停机方案设计与实现全景图</title>
      <link>https://itindex.net/detail/62377-java-%E6%8A%80%E6%9C%AF-%E4%B8%AD%E9%97%B4%E4%BB%B6</link>
      <description>&lt;blockquote&gt;  &lt;p&gt;本系列 Netty 源码解析文章基于     &lt;strong&gt;4.1.56.Final&lt;/strong&gt;版本&lt;/p&gt;&lt;/blockquote&gt; &lt;h2&gt;本文概要&lt;/h2&gt; &lt;p&gt;在上篇文章  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247485060&amp;idx=1&amp;sn=736360af6eb3a4db496de2d6665ebd3c&amp;chksm=ce77c0c3f90049d5e44692c2cf837e8d85bb28758b243d505c43c48ce703da1edadfc19360b1&amp;scene=21#wechat_redirect"&gt;我为 Netty 贡献源码 | 且看 Netty 如何应对 TCP 连接的正常关闭，异常关闭，半关闭场景&lt;/a&gt;中笔者为大家详细介绍了 Netty 在处理连接关闭时的完整过程，并详细介绍了 Netty 如何应对 TCP 连接在关闭时会遇到的各种场景。&lt;/p&gt; &lt;p&gt;在连接关闭之后，接下来就轮到 Netty 的谢幕时刻了，本文笔者会为大家详尽 Java 技术栈中间件中关于优雅停机方案的详细设计和实现。&lt;/p&gt; &lt;p&gt;笔者会从日常开发工作中常见的版本发布，服务上下线的场景聊起，引出服务优雅启停的需求，并从这个需求出发，一步一步带大家探究各个中间件里的优雅停机的相关设计。&lt;/p&gt; &lt;p&gt;熟悉笔者文风的读者朋友应该知道，笔者肯定不会只是简单的介绍，要么不讲，要讲就要把整个技术体系的前世今生给大家讲清楚，讲明白。&lt;/p&gt; &lt;p&gt;基于这目的，笔者会先从支持优雅停机的底层技术基石--内核中的信号量开始聊起。&lt;/p&gt; &lt;img&gt;&lt;/img&gt;image.png &lt;p&gt;从内核层我们接着会聊到 JVM 层，在 JVM 层一探优雅停机底层的技术玄机。&lt;/p&gt; &lt;img&gt;&lt;/img&gt;image.png &lt;p&gt;随后我们会从 JVM 层一路奔袭到 Spring 然后到 Dubbo。在这个过程中，笔者还会带大家一起 Shooting Dubbo 在优雅停机下的一个 Bug，并为大家详细介绍修复过程。&lt;/p&gt; &lt;img&gt;&lt;/img&gt;image.png &lt;p&gt;最后由 Dubbo 层的优雅停机，引出我们的主角--Netty 优雅停机的设计与实现：&lt;/p&gt; &lt;img&gt;&lt;/img&gt;Reactor优雅关闭总流程.png &lt;p&gt;下面我们来正式开始本文的内容~~&lt;/p&gt; &lt;img&gt;&lt;/img&gt;本文概要.png &lt;h2&gt;1. Java 进程的优雅启停&lt;/h2&gt; &lt;p&gt;在我们的日常开发工作中，业务需求的迭代和优化伴随围绕着我们整个开发周期，当我们加班加点完成了业务需求的开发，然后又历经各种艰难险阻通过了测试的验证，最后经过和产品经理的各种纠缠相爱相杀之后，终于到了最最激动人心的时刻程序要部署上线了。&lt;/p&gt; &lt;p&gt;               &lt;img&gt;&lt;/img&gt;&lt;/p&gt;上线时的情绪波动.png &lt;p&gt;那么在程序部署上线的过程中势必会涉及到线上服务的关闭和重启，关于对线上服务的启停这里面有很多的讲究，万万不能简单粗暴的进行关闭和重启，因为此时线上服务可能承载着生产的流量，可能正在进行重要的业务处理流程。&lt;/p&gt; &lt;p&gt;比如：用户正在购买商品，钱已经付了，恰好这时赶上程序上线，如果我们这时简单粗暴的对服务进行关闭，重启，可能就会导致用户付了钱，但是订单未创建或者商品未出现在用户的购物清单中，给用户造成了实质的损失，这是非常严重的后果。&lt;/p&gt; &lt;p&gt;为了保证能在程序上线的过程中做到业务无损，所以线上服务的  &lt;code&gt;优雅关闭&lt;/code&gt;和  &lt;code&gt;优雅启动&lt;/code&gt;显得就非常非常重要了。&lt;/p&gt; &lt;p&gt;                                 &lt;img&gt;&lt;/img&gt;&lt;/p&gt;保持优雅很重要.png &lt;h3&gt;1.1 优雅启动&lt;/h3&gt; &lt;p&gt;在 Java 程序的运行过程中，程序的运行速度一般会随着程序的运行慢慢的提高，所以从线上表现上来看 Java 程序在运行一段时间后往往会比程序刚启动的时候会快很多。&lt;/p&gt; &lt;p&gt;这是因为 Java 程序在运行过程中，JVM 会不断收集到程序运行时的动态数据，这样可以将高频执行代码通过即时编译成机器码，随后程序运行就直接执行机器码，运行速度完全不输 C 或者 C++ 程序。&lt;/p&gt; &lt;p&gt;同时在程序执行过程中，用到的类会被加载到 JVM 中缓存，这样当程序再次使用到的时候不会触发临时加载，影响程序执行性能。&lt;/p&gt; &lt;p&gt;我们可以将以上几点当做 JVM 带给我们的性能红利，  &lt;strong&gt;而当应用程序重新启动之后，这些性能红利也就消失了&lt;/strong&gt;，如果我们让新启动的程序继续承担之前的流量规模，那么就会导致程序在刚启动的时候在没有这些性能红利的加持下直接进入高负荷的运转状态，这就可能导致线上请求大面积超时，对业务造成影响。&lt;/p&gt; &lt;p&gt;所以说优雅地启动一个程序是非常重要的，优雅启动的核心思想就是让程序在刚启动的时候不要承担太大的流量，让程序在低负荷的状态下运行一段时间，使其提升到最佳的运行状态时，在逐步的让程序承担更大的流量处理。&lt;/p&gt; &lt;p&gt;下面我们就来看下常用于优雅启动场景的两个技术方案：&lt;/p&gt; &lt;h4&gt;1.1.1 启动预热&lt;/h4&gt; &lt;p&gt;启动预热就是让刚刚上线的应用程序不要一下就承担之前的全部流量，而是在一个时间窗口内慢慢的将流量打到刚上线的应用程序上，目的是让 JVM 先缓慢的收集程序运行时的一些动态数据，将高频代码即时编译为机器码。&lt;/p&gt; &lt;p&gt;这个技术方案在众多 RPC 框架的实现中我们都可以看到，服务调用方会从注册中心拿到所有服务提供方的地址，然后从这些地址中通过特定的负载均衡算法从中选取一个服务提供方的发送请求。&lt;/p&gt; &lt;p&gt;为了能够使刚刚上线的服务提供方有时间去预热，所以我们就要从源头上控制服务调用方发送的流量，服务调用方在发起 RPC 调用时应该尽量少的去负载均衡到刚刚启动的服务提供方实例。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;那么服务调用方如何才能判断哪些是刚刚启动的服务提供方实例呢？&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;服务提供方在启动成功后会向注册中心注册自己的服务信息，我们可以将服务提供方的真实启动时间包含在服务信息中一起向注册中心注册，这样注册中心就会通知服务调用方有新的服务提供方实例上线并告知其启动时间。&lt;/p&gt; &lt;p&gt;服务调用方可以根据这个启动时间，慢慢的将负载权重增加到这个刚启动的服务提供方实例上。这样就可以解决服务提供方冷启动的问题，调用方通过在一个时间窗口内将请求慢慢的打到提供方实例上，这样就可以让刚刚启动的提供方实例有时间去预热，达到平滑上线的效果。&lt;/p&gt; &lt;h4&gt;1.1.2 延迟暴露&lt;/h4&gt; &lt;p&gt;启动预热更多的是从服务调用方的角度通过降低刚刚启动的服务提供方实例的负载均衡权重来实现优雅启动。&lt;/p&gt; &lt;p&gt;而延迟暴露则是从服务提供方的角度，延迟暴露服务时间，利用延迟的这段时间，服务提供方可以预先加载依赖的一些资源，比如：缓存数据，spring 容器中的 bean 。等到这些资源全部加载完毕就位之后，我们在将服务提供方实例暴露出去。这样可以有效降低启动前期请求处理出错的概率。&lt;/p&gt; &lt;p&gt;比如我们可以在 dubbo 应用中可以配置服务的延迟暴露时间：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;//延迟5秒暴露服务   &lt;br /&gt;&amp;lt;dubbo:service delay=&amp;quot;5000&amp;quot; /&amp;gt;    &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;1.2 优雅关闭&lt;/h3&gt; &lt;p&gt;优雅关闭需要考虑的问题和处理的场景要比优雅启动要复杂的多，因为一个正常在线上运行的服务程序正在承担着生产的流量，同时也正在进行业务流程的处理。&lt;/p&gt; &lt;p&gt;要对这样的一个服务程序进行优雅关闭保证业务无损还是非常有挑战的，一个好的关闭流程，可以确保我们业务实现平滑的上下线，避免上线之后增加很多不必要的额外运维工作。&lt;/p&gt; &lt;p&gt;下面我们就来讨论下具体应该从哪几个角度着手考虑实现优雅关闭：&lt;/p&gt; &lt;h4&gt;1.2.1 切走流量&lt;/h4&gt; &lt;p&gt;                                          &lt;img&gt;&lt;/img&gt;&lt;/p&gt;image.png &lt;p&gt;第一步肯定是要将程序承担的现有流量全部切走，告诉服务调用方，我要进行关闭了，请不要在给我发送请求。那么如果进行切流呢？？&lt;/p&gt; &lt;p&gt;在 RPC 的场景中，服务调用方通过服务发现的方式从注册中心中动态感知服务提供者的上下线变化。在服务提供方关闭之前，首先就要自己从注册中心中取消注册，随后注册中心会通知服务调用方，有服务提供者实例下线，请将其从本地缓存列表中剔除。这样就可以使得服务调用方之后的 RPC 调用不在请求到下线的服务提供方实例上。&lt;/p&gt; &lt;p&gt;但是这里会有一个问题，就是通常我们的注册中心都是 AP 类型的，它只会保证最终一致性，并不会保证实时一致性，基于这个原因，服务调用方感知到服务提供者下线的事件可能是延后的，那么在这个延迟时间内，服务调用方极有可能会向正在下线的服务发起 RPC 请求。&lt;/p&gt; &lt;p&gt;因为服务提供方已经开始进入关闭流程，那么很多对象在这时可能已经被销毁了，这时如果在收到请求过来，肯定是无法处理的，甚至可能还会抛出一个莫名其妙的异常出来，对业务造成一定的影响。&lt;/p&gt; &lt;p&gt;那么既然这个问题是由于注册中心可能存在的延迟通知引起的，那么我们就很自然的想到了让准备下线的服务提供方主动去通知它的服务调用方。&lt;/p&gt; &lt;p&gt;这种服务提供方  &lt;strong&gt;主动通知&lt;/strong&gt;在加上注册中心  &lt;strong&gt;被动通知&lt;/strong&gt;的两个方案结合在一起应该就能确保万无一失了吧。&lt;/p&gt; &lt;p&gt;事实上，在大部分场景下这个方案是可行的，但是还有一种极端的情况需要应对，就是当服务提供方通知调用方自己下线的网络请求在到达服务调用方之前的很极限的一个时间内，服务调用者向正在下线的服务提供方发起了 RPC 请求，这种极端的情况，就需要服务提供方和调用方一起配合来应对了。&lt;/p&gt; &lt;p&gt;首先服务提供方在准备关闭的时候，就把自己设置为正在关闭状态，在这个状态下不会接受任何请求，如果这时遇到了上边这种极端情况下的请求，那么就抛出一个 CloseException （这个异常是提供方和调用方提前约定好的），调用方收到这个 CloseException ，则将该服务提供方的节点剔除，并从剩余节点中通过负载均衡选取一个节点进行重试，通过让这个请求快速失败从而保证业务无损。&lt;/p&gt; &lt;p&gt;这三种方案结合在一起，笔者认为就是一个比较完美的切流方案了。&lt;/p&gt; &lt;h4&gt;1.2.2 尽量保证业务无损&lt;/h4&gt; &lt;p&gt;当把流量全部切走后，可能此时将要关闭的服务程序中还有正在处理的部分业务请求，那么我们就必须得等到这些业务处理请求全部处理完毕，并将业务结果响应给客户端后，在对服务进行关闭。&lt;/p&gt; &lt;p&gt;当然为了保证关闭流程的可控，我们需要引入关闭超时时间限制，当剩下的业务请求处理超时，那么就强制关闭。&lt;/p&gt; &lt;p&gt;为了保证关闭流程的可控，我们只能做到尽可能的保证业务无损而不是百分之百保证。所以在程序上线之后，我们应该对业务异常数据进行监控并及时修复。&lt;/p&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;通过以上介绍的优雅关闭方案我们知道，当我们将要优雅关闭一个应用程序时，我们需要做好以下两项工作：&lt;/p&gt; &lt;ol&gt;  &lt;li&gt;   &lt;p&gt;我们首先要做的就是将当前将要关闭的应用程序上承载的生产流量全部切走，保证不会有新的流量打到将要关闭的应用程序实例上。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;当所有的生产流量切走之后，我们还需要保证当前将要关闭的应用程序实例正在处理的业务请求要使其处理完毕，并将业务处理结果响应给客户端。以保证业务无损。当然为了使关闭流程变得可控，我们需要引入关闭超时时间。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt; &lt;p&gt;以上两项工作就是我们在应用程序将要被关闭时需要做的，  &lt;strong&gt;那么问题是我们如何才能知道应用程序要被关闭呢&lt;/strong&gt;？换句话说，我们在应用程序里怎么才能感知到程序进程的关闭事件从而触发上述两项优雅关闭的操作执行呢？&lt;/p&gt; &lt;p&gt;既然我们有这样的需求，那么操作系统内核肯定会给我们提供这样的机制，事实上我们可以通过捕获操作系统给进程发送的信号来获取关闭进程通知，并在相应信号回调中触发优雅关闭的操作。&lt;/p&gt; &lt;p&gt;接下来让我们来看一下操作系统内核提供的信号机制：&lt;/p&gt; &lt;h2&gt;2. 内核信号机制&lt;/h2&gt; &lt;p&gt;信号是操作系统内核为我们提供用于在进程间通信的机制，内核可以利用信号来通知进程，当前系统所发生的的事件（包括关闭进程事件）。&lt;/p&gt; &lt;p&gt;信号在内核中并没有用特别复杂的数据结构来表示，只是用一个代号一样的数字来标识不同的信号。Linux 提供了几十种信号，分别代表不同的意义。信号之间依靠它们的值来区分&lt;/p&gt; &lt;p&gt;信号可以在任何时候发送给进程，进程需要为这个信号配置信号处理函数。当某个信号发生的时候，就默认执行对应的信号处理函数就可以了。这就相当于一个操作系统的应急手册，事先定义好遇到什么情况，做什么事情，提前准备好，出了事情照着做就可以了。&lt;/p&gt; &lt;p&gt;内核发出的信号就代表当前系统遇到了某种情况，我们需要应对的步骤就封装在对应信号的回调函数中。&lt;/p&gt; &lt;p&gt;信号机制引入的目的就在于：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;让应用进程知道当前已经发生了某个特定的事件（比如进程的关闭事件）。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;强制进程执行我们事先设定好的信号处理函数（比如封装优雅关闭逻辑）。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;通常来说程序一旦启动就会一直运行下去，除非遇到 OOM 或者我们需要重新发布程序时会在运维脚本中调用 kill 命令关闭程序。Kill 命令从字面意思上来说是杀死进程，但是其本质是向进程发送信号，从而关闭进程。&lt;/p&gt; &lt;p&gt;下面我们使用 kill -l 命令查看下 kill 命令可以向进程发送哪些信号：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;# kill -l   &lt;br /&gt;1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP   &lt;br /&gt;6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1   &lt;br /&gt;11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM   &lt;br /&gt;16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP   &lt;br /&gt;21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ   &lt;br /&gt;26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR   &lt;br /&gt;31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3   &lt;br /&gt;38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8   &lt;br /&gt;43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13   &lt;br /&gt;48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12   &lt;br /&gt;53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7   &lt;br /&gt;58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2   &lt;br /&gt;63) SIGRTMAX-1  64) SIGRTMAX&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;笔者这里提取几个常见的信号来简要说明下：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;SIGINT：&lt;/code&gt;信号代号为 2 。比如我们在终端以非后台模式运行一个进程实例时，要想关闭它，我们可以通过 Ctrl+C 来关闭这个前台程序。这个 Ctrl+C 向进程发送的正是 SIGINT 信号。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;SIGQUIT：&lt;/code&gt;信号代号为 3 。比如我们使用 Ctrl+\ 来关闭一个前台进程，此时会向进程发送 SIGQUIT 信号，    &lt;strong&gt;与 SIGINT 信号不同的是&lt;/strong&gt;，通过 SIGQUIT 信号终止的进程会在退出时，通过 Core Dump 将当前进程的运行状态保存在 core dump 文件里面，方便后续查看。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;SIGKILL：&lt;/code&gt;信号代号为 9 。通过 kill -9 pid 命令结束进程是非常非常危险的动作，    &lt;strong&gt;我们应该坚决制止这种关闭进程的行为&lt;/strong&gt;，因为 SIGKILL 信号是不能被进程捕获和忽略的，只能执行内核定义的默认操作直接关闭进程。    &lt;strong&gt;而我们的优雅关闭操作是需要通过捕获操作系统信号，从而可以在对应的信号处理函数中执行优雅关闭的动作&lt;/strong&gt;。由于 SIGKILL 信号不能被捕获，所以优雅关闭也就无法实现。现在大家就赶快检查下自己公司生产环境的运维脚本是否是通过 kill -9 pid 命令来结束进程的，一定要避免用这种方式，因为这种方式是极其无情并且略带残忍的关闭进程行为。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;           &lt;img&gt;&lt;/img&gt;&lt;/p&gt;image.png &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;SIGSTOP ：&lt;/code&gt;信号代号为 19 。该信号和 SIGKILL 信号一样都是无法被应用程序忽略和捕获的。向进程发送 SIGSTOP 信号也是无法实现优雅关闭的。通过 Ctrl+Z 来关闭一个前台进程，发送的信号就是 SIGSTOP 信号。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;SIGTERM：&lt;/code&gt;信号代号为 15 。我们通常会使用 kill 命令来关闭一个后台运行的进程，kill 命令发送的默认信号就是 SIGTERM ，    &lt;strong&gt;该信号也是本文要讨论的优雅关闭的基础&lt;/strong&gt;，我们通常会使用 kill pid 或者 kill -15 pid 来向后台进程发送 SIGTERM 信号用以实现进程的优雅关闭。大家如果发现自己公司生产环境的运维脚本中使用的是 kill -9 pid 命令来结束进程，那么就要马上换成 kill pid 命令。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;以上列举的都是我们常用的一些信号，大家也可以通过 man 7 signal 命令查看每种信号对应的含义：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;Signal     Value     Action   Comment   &lt;br /&gt;──────────────────────────────────────────────────────────────────────   &lt;br /&gt;SIGHUP        1       Term    Hangup detected on controlling terminal   &lt;br /&gt;or death of controlling process   &lt;br /&gt;SIGINT        2       Term    Interrupt from keyboard   &lt;br /&gt;SIGQUIT       3       Core    Quit from keyboard   &lt;br /&gt;SIGILL        4       Core    Illegal Instruction   &lt;br /&gt;   &lt;br /&gt;   &lt;br /&gt;SIGABRT       6       Core    Abort signal from abort(3)   &lt;br /&gt;SIGFPE        8       Core    Floating point exception   &lt;br /&gt;SIGKILL       9       Term    Kill signal   &lt;br /&gt;SIGSEGV      11       Core    Invalid memory reference   &lt;br /&gt;SIGPIPE      13       Term    Broken pipe: write to pipe with no   &lt;br /&gt;readers   &lt;br /&gt;SIGALRM      14       Term    Timer signal from alarm(2)   &lt;br /&gt;SIGTERM      15       Term    Termination signal   &lt;br /&gt;SIGUSR1   30,10,16    Term    User-defined signal 1   &lt;br /&gt;SIGUSR2   31,12,17    Term    User-defined signal 2   &lt;br /&gt;……&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;而应用进程对于信号的处理一般分为以下三种方式：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;内核定义的默认操作：&lt;/code&gt;系统内核对每种信号都规定了默认操作，比如上面列表 Action 列中的 Term ，就是终止进程的意思。前边介绍的 SIGINT 信号和 SIGTERM 信号的默认操作就是 Term 。Core 的意思是 Core Dump ，即终止进程后会通过 Core Dump 将当前进程的运行状态保存在文件里面，方便我们事后进行分析问题在哪里。前边介绍的 SIGQUIT 信号默认操作就是 Core 。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;捕获信号：&lt;/code&gt;应用程序可以利用内核提供的系统调用来捕获信号，并将优雅关闭的步骤封装在对应信号的处理函数中。当向进程发送关闭信号 SIGTERM  的时候，在进程内我们可以通过捕获 SIGTERM 信号，随即就会执行我们自定义的信号处理函数。我们从而可以在信号处理函数中执行进程优雅关闭的逻辑。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;忽略信号：&lt;/code&gt;当我们不希望处理某些信号的时候，就可以忽略该信号，不做任何处理，    &lt;strong&gt;但是前边介绍的 SIGKILL 信号和 SIGSTOP 是无法被捕获和忽略的，内核会直接执行这两个信号定义的默认操作直接关闭进程。&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;当我们不希望信号执行内核定义的默认操作时，我们就需要在进程内捕获信号，并注册信号的回调函数来执行我们自定义的信号处理逻辑。&lt;/p&gt; &lt;p&gt;比如我们在本文中要讨论的优雅关闭场景，当进程接收到 SIGTERM 信号时，为了实现进程的优雅关闭，我们并不希望进程执行 SIGTERM 信号的默认操作直接关闭进程，所以我们要在进程中捕获 SIGTERM 信号，并将优雅关闭的操作步骤封装在对应的信号处理函数中。&lt;/p&gt; &lt;h3&gt;2.1 如何捕获信号&lt;/h3&gt; &lt;p&gt;在介绍完了内核信号的分类以及进程对于信号处理的三种方式之后，下面我们来看下如何来捕获内核信号，并在对应信号回调函数中自定义我们的处理逻辑。&lt;/p&gt; &lt;p&gt;内核提供了 sigaction 系统调用，来供我们捕获信号以及与相应的信号处理函数绑定起来。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;int sigaction(int signum, const struct sigaction *act,   &lt;br /&gt;                     struct sigaction *oldact);   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;int signum：&lt;/code&gt;表示我们想要在进程中捕获的信号，比如本文中我们要实现优雅关闭就需要在进程中捕获 SIGTERM 信号，对应的 signum = 15 。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;struct sigaction *act：&lt;/code&gt;内核中会用一个 sigaction 结构体来封装我们自定义的信号处理逻辑。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;struct sigaction *oldact：&lt;/code&gt;这里是为了兼容老的信号处理函数，了解一下就可以了，和本文主线无关。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;sigaction 结构体用来封装信号对应的处理函数，以及更加精细化控制信号处理的信息。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;struct sigaction {   &lt;br /&gt;  __sighandler_t sa_handler;   &lt;br /&gt;  unsigned long sa_flags;   &lt;br /&gt;        .......   &lt;br /&gt;  sigset_t sa_mask;    &lt;br /&gt;};   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;__sighandler_t sa_handler：&lt;/code&gt;其实本质上是一个函数指针，用来保存我们为信号注册的信号处理函数，    &lt;strong&gt;优雅关闭的逻辑就封装在这里&lt;/strong&gt;。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;long sa_flags：&lt;/code&gt;为了更加精细化的控制信号处理逻辑，这个字段保存了一些控制信号处理行为的选项集合。常见的选项有：&lt;/p&gt;&lt;/li&gt;  &lt;ul&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;SA_ONESHOT&lt;/strong&gt;：意思是我们注册的信号处理函数，仅仅只起一次作用。响应完一次后，就设置回默认行为。&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;SA_NOMASK&lt;/strong&gt;：表示信号处理函数在执行的过程中会被中断。比如我们进程捕获到一个感兴趣的信号，随后会执行注册的信号处理函数，但是此时进程又收到其他的信号或者和上次相同的信号，此时正在执行的信号处理函数会被中断，从而转去执行最新到来的信号处理函数。     &lt;strong&gt;如果连续产生多个相同的信号，那么我们的信号处理函数就要做好同步，幂等等措施&lt;/strong&gt;。&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;SA_INTERRUPT&lt;/strong&gt;：当进程正在执行一个非常耗时的系统调用时，如果此时进程接收到了信号，那么这个系统调用将会被信号中断，进程转去执行相应的信号处理函数。那么当信号处理函数执行完时，如果这里设置了 SA_INTERRUPT ，那么系统调用将不会继续执行并且会返回一个     &lt;code&gt;-EINTR&lt;/code&gt;常量，告诉调用方，这个系统调用被信号中断了，怎么处理你看着办吧。&lt;/p&gt;&lt;/li&gt;   &lt;li&gt;    &lt;p&gt;     &lt;strong&gt;SA_RESTART&lt;/strong&gt;：当系统调用被信号中断后，相应的信号处理函数执行完毕后，如果这里设置了 SA_RESTART 系统调用将会被自动重新启动。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;sigset_t sa_mask：&lt;/code&gt;这个字段主要指定在信号处理函数正在运行的过程中，如果连续产生多个信号，需要屏蔽哪些信号。也就是说当进程收到屏蔽的信号时，正在进行的信号处理函数不会被中断。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;blockquote&gt;  &lt;p&gt;屏蔽并不意味着信号一定丢失，而是暂存，这样可以使相同信号的处理函数，在进程连续接收到多个相同的信号时，可以一个一个的处理。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;最终通过 sigaction 函数会调用到底层的系统调用 rt_sigaction 函数，在
rt_sigaction 中会将上边介绍的用户态 struct sigaction 结构拷贝为内核态的
k_sigaction ，然后调用 do_sigaction 函数。&lt;/p&gt; &lt;p&gt;最后在 do_sigaction 函数中将用户要在进程中捕获的信号以及相应的信号处理函数设置到进程描述符 task_struct 结构里。&lt;/p&gt; &lt;img&gt;&lt;/img&gt;进程中的信号结构.png &lt;p&gt;进程在内核中的数据结构 task_struct 中有一个 struct sighand_struct 结构的属性 sighand ，struct sighand_struct 结构中包含一个 k_sigaction 类型的数组 action[] ，这个数组保存的就是进程中需要捕获的信号以及对应的信号处理函数在内核中的结构体 k_sigaction ，数组下标为进程需要捕获的信号。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;#include &amp;lt;signal.h&amp;gt;   &lt;br /&gt;   &lt;br /&gt;static void sig_handler(int signum) {   &lt;br /&gt;   &lt;br /&gt;    if (signum == SIGTERM) {   &lt;br /&gt;   &lt;br /&gt;        .....执行优雅关闭逻辑....   &lt;br /&gt;   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;int main (Void) {   &lt;br /&gt;   &lt;br /&gt;    struct sigaction sa_usr; //定义sigaction结构体   &lt;br /&gt;    sa_usr.sa_flags = 0;   &lt;br /&gt;    sa_usr.sa_handler = sig_handler;   //设置信号处理函数   &lt;br /&gt;   &lt;br /&gt;    sigaction(SIGTERM, &amp;amp;sa_usr, NULL);//进程捕获信号，注册信号处理函数   &lt;br /&gt;           &lt;br /&gt;        ,,,,,,,,,,,,   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们可以通过如上简单的示例代码，将 SIGTERM 信号及其对应的自定义信号处理函数注册到进程中，当我们执行 kill -15 pid 命令之后，进程就会捕获到 SIGTERM 信号，随后就可以执行优雅关闭步骤了。&lt;/p&gt; &lt;h2&gt;3. JVM 中的 ShutdownHook&lt;/h2&gt; &lt;p&gt;在《2. 内核信号机制》小节中为大家介绍的内容是操作系统内核为我们实现进程的优雅关闭提供的最底层系统级别的支持机制，在内核的强力支持下，那么本文的主题 Java 进程的优雅关闭就很容易实现了。&lt;/p&gt; &lt;p&gt;我们要想实现 Java 进程的优雅关闭功能，只需要在进程启动的时候将优雅关闭的操作封装在一个 Thread 中，随后将这个 Thread 注册到 JVM 的 ShutdownHook 中就好了，当 JVM 进程接收到 kill -15 信号时，就会执行我们注册的 ShutdownHook 关闭钩子，进而执行我们定义的优雅关闭步骤。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;        Runtime.getRuntime().addShutdownHook(new Thread(){   &lt;br /&gt;            @Override   &lt;br /&gt;            public void run() {   &lt;br /&gt;                .....执行优雅关闭步骤.....   &lt;br /&gt;            }   &lt;br /&gt;        });   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;3.1 导致 JVM 退出的几种情况&lt;/h3&gt; &lt;ol&gt;  &lt;li&gt;   &lt;p&gt;JVM 进程中最后一个非守护线程退出。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;在程序代码中主动调用 java.lang.System#exit(int status) 方法，会导致 JVM 进程的退出并触发 ShutdownHook 的调用。参数 int status 如果是非零值，则表示本次关闭是在一个非正常情况下的关闭行为。比如：进程发生 OOM 异常或者其他运行时异常。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt; &lt;pre&gt;  &lt;code&gt;public static void main(String[] args) {   &lt;br /&gt;        try {   &lt;br /&gt;   &lt;br /&gt;           ......进程启动main函数.......   &lt;br /&gt;   &lt;br /&gt;        } catch (RuntimeException e) {   &lt;br /&gt;            logger.error(e.getMessage(), e);   &lt;br /&gt;            // JVM 进程主动关闭触发调用 shutdownHook   &lt;br /&gt;            System.exit(1);   &lt;br /&gt;        }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;ol start="3"&gt;  &lt;li&gt;   &lt;p&gt;当 JVM 进程接收到第二小节《2.内核信号机制》介绍的那些关闭信号时， JVM 进程会被关闭。    &lt;strong&gt;由于 SIGKILL 信号和 SIGSTOP 信号不能够被进程捕获和忽略&lt;/strong&gt;，这两个信号会直接粗暴地关闭 JVM 进程，所以一般我们会发送 SIGTERM 信号，JVM 进程通过捕获 SIGTERM 信号，从而可以执行我们定义的 ShutdownHook 完成优雅关闭的操作。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;Native Method 执行过程中发生错误，比如试图访问一个不存在的内存，这样也会导致 JVM 强制关闭，ShutdownHook 也不会运行。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt; &lt;h3&gt;3.2 使用 ShutdownHook 的注意事项&lt;/h3&gt; &lt;ol&gt;  &lt;li&gt;ShutdownHook 其实本质上是一个已经被初始化但是未启动的 Thread ，这些通过 Runtime.getRuntime().addShutdownHook 方法注册的 ShutdownHooks ，在 JVM 进程关闭的时候会被启动   &lt;strong&gt;并发执行&lt;/strong&gt;，但是并   &lt;strong&gt;不会保证执行顺序&lt;/strong&gt;。&lt;/li&gt;&lt;/ol&gt; &lt;blockquote&gt;  &lt;p&gt;所以在编写 ShutdownHook 中的逻辑时，我们应该确保程序的线程安全性，并尽可能避免死锁。最好是一个 JVM 进程只注册一个 ShutdownHook 。&lt;/p&gt;&lt;/blockquote&gt; &lt;ol start="2"&gt;  &lt;li&gt;如果我们通过   &lt;code&gt;java.lang.Runtime#runFinalizersOnExit(boolean value)&lt;/code&gt;开启了 finalization-on-exit ，那么当所有 ShutdownHook 运行完毕之后，JVM 在关闭之前将会继续调用所有未被调用的 finalizers 方法。默认 finalization-on-exit 选项是关闭的。&lt;/li&gt;&lt;/ol&gt; &lt;blockquote&gt;  &lt;p&gt;   &lt;strong&gt;注意&lt;/strong&gt;：当 JVM 开始关闭并执行上述关闭操作的时候，守护线程是会继续运行的，如果用户使用 java.lang.System#exit(int status) 方法主动发起 JVM 关闭，那么关闭期间非守护线程也是会继续运行的。&lt;/p&gt;&lt;/blockquote&gt; &lt;ol start="3"&gt;  &lt;li&gt;一旦 JVM 进程开始关闭，一般情况下这个过程是不可以被中断的，除非操作系统强制中断或者用户通过调用 java.lang.Runtime#halt(int status) 来强制关闭。&lt;/li&gt;&lt;/ol&gt; &lt;pre&gt;  &lt;code&gt;   public void halt(int status) {   &lt;br /&gt;        SecurityManager sm = System.getSecurityManager();   &lt;br /&gt;        if (sm != null) {   &lt;br /&gt;            sm.checkExit(status);   &lt;br /&gt;        }   &lt;br /&gt;        Shutdown.halt(status);   &lt;br /&gt;    }   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;  &lt;p&gt;java.lang.Runtime#halt(int status) 方法是用来强制关闭正在运行的 JVM 进程的，它会导致我们注册的 ShutdownHook 不会被运行和执行，如果此时 JVM 正在执行 ShutdownHook ，当调用该方法后，JVM 进程将会被强制关闭，并不会等待 ShutdownHook 执行完毕。&lt;/p&gt;&lt;/blockquote&gt; &lt;ol start="4"&gt;  &lt;li&gt;   &lt;p&gt;当 JVM 关闭流程开始的时候，就不能在向其注册 ShutdownHook 或者取消注册之前已经注册好的 ShutdownHook 了，否则将会抛出 IllegalStateException异常。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;ShutdownHook 中的程序应该尽快的完成优雅关闭逻辑，因为当用户调用 System#exit 方法的时候是希望 JVM 在保证业务无损的情况下尽快完成关闭动作。这里并不适合做一些需要长时间运行的任务或者和用户交互的操作。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt; &lt;blockquote&gt;  &lt;p&gt;如果是因为物理机关闭从而导致的 JVM 关闭，那么操作系统只会允许 JVM 限定的时间内尽快的关闭，超过限定时间操作系统将会强制关闭 JVM 。&lt;/p&gt;&lt;/blockquote&gt; &lt;ol start="6"&gt;  &lt;li&gt;ShutdownHook 中可能也会抛出异常，而 ShutdownHook 对于 JVM 来说本质上是一个 Thread ，那么对于 ShutdownHook 中未捕获的异常，JVM 的处理方法和其他普通的线程一样，都是通过调用 ThreadGroup#uncaughtException 方法来处理。此方法的默认实现是将异常的堆栈跟踪打印到 System#err 并终止异常的 ShutdownHook 线程。&lt;/li&gt;&lt;/ol&gt; &lt;blockquote&gt;  &lt;p&gt;   &lt;strong&gt;注意：&lt;/strong&gt;这里只会停止异常的 ShutdownHook ，但不会影响其他 ShutdownHook 线程的执行更不会导致 JVM 退出。&lt;/p&gt;&lt;/blockquote&gt; &lt;ol start="7"&gt;  &lt;li&gt;   &lt;strong&gt;最后也是非常重要的一点是&lt;/strong&gt;，当 JVM 进程接收到 SIGKILL 信号和 SIGSTOP 信号时，是会强制关闭，并不会执行 ShutdownHook 。另外一种导致 JVM 强制关闭的情况就是 Native Method 执行过程中发生错误，比如试图访问一个不存在的内存，这样也会导致 JVM 强制关闭，ShutdownHook 也不会运行。&lt;/li&gt;&lt;/ol&gt; &lt;h3&gt;3.3 ShutdownHook 执行原理&lt;/h3&gt; &lt;p&gt;我们在 JVM 中通过 Runtime.getRuntime().addShutdownHook 添加关闭钩子，当 JVM 接收到 SIGTERM 信号之后，就会调用我们注册的这些 ShutdownHooks 。&lt;/p&gt; &lt;p&gt;本小节介绍的 ShutdownHook 就类似于我们在第二小节《内核信号机制》中介绍的信号处理函数。&lt;/p&gt; &lt;p&gt;大家这里一定会有个疑问，那就是在介绍内核信号机制小节中，我们可以通过系统调用 sigaction 函数向内核注册进程要捕获的信号以及对应的信号处理函数。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;int sigaction(int signum, const struct sigaction *act,   &lt;br /&gt;                     struct sigaction *oldact);   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;但是在本小节介绍的 JVM 中，我们只是通过 Runtime.getRuntime().addShutdownHook 注册了一个关闭钩子。但是并未注册 JVM 进程所需要捕获的信号。那么 JVM 是怎么捕获关闭信号的呢？&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;        Runtime.getRuntime().addShutdownHook(new Thread(){   &lt;br /&gt;            @Override   &lt;br /&gt;            public void run() {   &lt;br /&gt;                .....执行优雅关闭步骤.....   &lt;br /&gt;            }   &lt;br /&gt;        });   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;事实上，JVM 捕获操作系统信号的部分在 JDK 中已经帮我们处理好了，在用户层我们并不需要关注捕获信号的处理，只需要关注信号的处理逻辑即可。&lt;/p&gt; &lt;p&gt;下面我们就来看一下 JDK 是如何帮助我们将要捕获的信号向内核注册的？&lt;/p&gt; &lt;p&gt;当 JVM 第一个线程被初始化之后，随后就会调用 System#initializeSystemClass 函数来初始化 JDK 中的一些系统类，其中就包括注册 JVM 进程需要捕获的信号以及信号处理函数。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public final class System {   &lt;br /&gt;   &lt;br /&gt;    private static void initializeSystemClass() {   &lt;br /&gt;   &lt;br /&gt;           .......省略.......   &lt;br /&gt;   &lt;br /&gt;            // Setup Java signal handlers for HUP, TERM, and INT (where available).   &lt;br /&gt;           Terminator.setup();   &lt;br /&gt;   &lt;br /&gt;           .......省略.......   &lt;br /&gt;   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;从这里可以看出，JDK 在向 JVM 注册需要捕获的内核信号是在 Terminator 类中进行的。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;   &lt;br /&gt;class Terminator {   &lt;br /&gt;    //信号处理函数   &lt;br /&gt;    private static SignalHandler handler = null;   &lt;br /&gt;   &lt;br /&gt;    static void setup() {   &lt;br /&gt;        if (handler != null) return;   &lt;br /&gt;        SignalHandler sh = new SignalHandler() {   &lt;br /&gt;            public void handle(Signal sig) {   &lt;br /&gt;                Shutdown.exit(sig.getNumber() + 0200);   &lt;br /&gt;            }   &lt;br /&gt;        };   &lt;br /&gt;        handler = sh;   &lt;br /&gt;   &lt;br /&gt;        try {   &lt;br /&gt;            Signal.handle(new Signal(&amp;quot;HUP&amp;quot;), sh);   &lt;br /&gt;        } catch (IllegalArgumentException e) {   &lt;br /&gt;        }   &lt;br /&gt;        try {   &lt;br /&gt;            Signal.handle(new Signal(&amp;quot;INT&amp;quot;), sh);   &lt;br /&gt;        } catch (IllegalArgumentException e) {   &lt;br /&gt;        }   &lt;br /&gt;        try {   &lt;br /&gt;            Signal.handle(new Signal(&amp;quot;TERM&amp;quot;), sh);   &lt;br /&gt;        } catch (IllegalArgumentException e) {   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;JDK 向我们提供了  &lt;code&gt;sun.misc.Signal#handle(Signal signal, SignalHandler signalHandler)&lt;/code&gt;函数来实现在 JVM 进程中对内核信号的捕获。底层依赖于我们在第二小节介绍的系统调用 sigaction 。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;int sigaction(int signum, const struct sigaction *act,   &lt;br /&gt;                     struct sigaction *oldact);   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;  &lt;code&gt;sun.misc.Signal#handle&lt;/code&gt;函数的参数含义和系统调用函数  &lt;code&gt;sigaction&lt;/code&gt;中的参数含义是一一对应的：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;code&gt;Signal signal&lt;/code&gt;：表示要捕获的内核信号。从这里我们可以看出 JVM 主要捕获三种信号：SIGHUP(1)，SIGINT(2)，SIGTERM(15)。&lt;/li&gt;&lt;/ul&gt; &lt;blockquote&gt;  &lt;p&gt;除了上述的这三种信号之外，JVM 如果接收到其他信号，会执行系统内核默认的操作，直接关闭进程，并不会触发 ShutdownHook 的执行。&lt;/p&gt;&lt;/blockquote&gt; &lt;ul&gt;  &lt;li&gt;   &lt;code&gt;SignalHandler handler&lt;/code&gt;：信号响应函数。我们看到这里直接调用了 Shutdown#exit 函数。&lt;/li&gt;&lt;/ul&gt; &lt;pre&gt;  &lt;code&gt;    SignalHandler sh = new SignalHandler() {   &lt;br /&gt;            public void handle(Signal sig) {   &lt;br /&gt;                Shutdown.exit(sig.getNumber() + 0200);   &lt;br /&gt;            }   &lt;br /&gt;        };   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们这里应该很容易就会猜测出 ShutdownHook 的调用应该就是在 Shutdown#exit 函数中被触发的。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;class Shutdown {   &lt;br /&gt;   &lt;br /&gt;    static void exit(int status) {   &lt;br /&gt;   &lt;br /&gt;          ........省略.........   &lt;br /&gt;   &lt;br /&gt;          synchronized (Shutdown.class) {   &lt;br /&gt;              // 开始 JVM 关闭流程，执行 ShutdownHooks   &lt;br /&gt;              sequence();   &lt;br /&gt;              // 强制关闭 JVM   &lt;br /&gt;              halt(status);   &lt;br /&gt;          }   &lt;br /&gt;   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;    private static void sequence() {   &lt;br /&gt;        synchronized (lock) {   &lt;br /&gt;            if (state != HOOKS) return;   &lt;br /&gt;        }   &lt;br /&gt;        //触发 ShutdownHooks   &lt;br /&gt;        runHooks();   &lt;br /&gt;        boolean rfoe;   &lt;br /&gt;        synchronized (lock) {   &lt;br /&gt;            state = FINALIZERS;   &lt;br /&gt;            rfoe = runFinalizersOnExit;   &lt;br /&gt;        }   &lt;br /&gt;        //如果 runFinalizersOnExit = true   &lt;br /&gt;        //开始运行所有未被调用过的 Finalizers   &lt;br /&gt;        if (rfoe) runAllFinalizers();   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;Shutdown#sequence 函数中的逻辑就是我们在《3.2 使用ShutdownHook的注意事项》小节中介绍的 JVM 关闭时的运行逻辑：在这里会触发所有 ShutdownHook 的  &lt;strong&gt;并发运行&lt;/strong&gt;。注意这里并不会保证运行顺序。&lt;/p&gt; &lt;p&gt;当所有 ShutdownHook 运行完毕之后，如果我们通过  &lt;code&gt;java.lang.Runtime#runFinalizersOnExit(boolean value)&lt;/code&gt;开启了  &lt;code&gt;finalization-on-exit&lt;/code&gt;选项，JVM 在关闭之前将会继续调用所有未被调用的 finalizers 方法。默认 finalization-on-exit 选项是关闭的。&lt;/p&gt; &lt;h3&gt;3.4 ShutdownHook 的执行&lt;/h3&gt; &lt;img&gt;&lt;/img&gt;shutdownhook的运行.png &lt;p&gt;如上图所示，在 JDK 的 Shutdown 类中，包含了一个 Runnable[] hooks 数组，容量为 10 。JDK 中的 ShutdownHook 是以类型来分类的，数组 hooks 每一个槽中存放的是一种特定类型的 ShutdownHook 。&lt;/p&gt; &lt;p&gt;而我们通常在程序代码中通过 Runtime.getRuntime().addShutdownHook 注册的是  &lt;code&gt;Application hooks&lt;/code&gt;类型的 ShutdownHook ，存放在数组 hooks 中索引为 1 的槽中。&lt;/p&gt; &lt;p&gt;当在 Shutdown#sequence 中触发 runHooks() 函数开始运行 JVM 中所有类型的 ShutdownHooks 时，会在 runHooks() 函数中依次遍历数组 hooks 中的 Runnable ，进而开始运行 Runnable 中封装的 ShutdownHooks 。&lt;/p&gt; &lt;p&gt;当遍历到数组 Hooks 的第二个槽（索引为 1 ）的时候，  &lt;code&gt;Application hooks&lt;/code&gt;类型的 ShutdownHook 得以运行，也就是我们通过 Runtime.getRuntime().addShutdownHook 注册的 ShutdownHook 在这个时候开始运行起来。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;   &lt;br /&gt;    // The system shutdown hooks are registered with a predefined slot.   &lt;br /&gt;    // The list of shutdown hooks is as follows:   &lt;br /&gt;    // (0) Console restore hook   &lt;br /&gt;    // (1) Application hooks   &lt;br /&gt;    // (2) DeleteOnExit hook   &lt;br /&gt;    private static final int MAX_SYSTEM_HOOKS = 10;   &lt;br /&gt;    private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];   &lt;br /&gt;   &lt;br /&gt;    /* Run all registered shutdown hooks   &lt;br /&gt;     */   &lt;br /&gt;    private static void runHooks() {   &lt;br /&gt;        for (int i=0; i &amp;lt; MAX_SYSTEM_HOOKS; i++) {   &lt;br /&gt;            try {   &lt;br /&gt;                Runnable hook;   &lt;br /&gt;                synchronized (lock) {   &lt;br /&gt;                    // acquire the lock to make sure the hook registered during   &lt;br /&gt;                    // shutdown is visible here.   &lt;br /&gt;                    currentRunningHook = i;   &lt;br /&gt;                    hook = hooks[i];   &lt;br /&gt;                }   &lt;br /&gt;                if (hook != null) hook.run();   &lt;br /&gt;            } catch(Throwable t) {   &lt;br /&gt;                if (t instanceof ThreadDeath) {   &lt;br /&gt;                    ThreadDeath td = (ThreadDeath)t;   &lt;br /&gt;                    throw td;   &lt;br /&gt;                }   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;下面我们就来看一下，JDK 是如果通过 Runtime.getRuntime().addShutdownHook 函数将我们自定义的 ShutdownHook 注册到 Shutdown 类中的数组 Hooks 里的。&lt;/p&gt; &lt;h3&gt;3.5 ShutdownHook 的注册&lt;/h3&gt; &lt;pre&gt;  &lt;code&gt;public class Runtime {   &lt;br /&gt;   &lt;br /&gt;    public void addShutdownHook(Thread hook) {   &lt;br /&gt;        SecurityManager sm = System.getSecurityManager();   &lt;br /&gt;        if (sm != null) {   &lt;br /&gt;            sm.checkPermission(new RuntimePermission(&amp;quot;shutdownHooks&amp;quot;));   &lt;br /&gt;        }   &lt;br /&gt;        //注意 这里注册的是 Application 类型的 hooks   &lt;br /&gt;        ApplicationShutdownHooks.add(hook);   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;从 JDK 源码中我们看到在 Runtime 类中的 addShutdownHook 方法里，JDK 会将我们自定义的 ShutdownHook 封装在 ApplicationShutdownHooks 类中，从这类的命名上看，它里边封装的就是我们在上小节《3.4 ShutdownHook 的执行》提到的  &lt;code&gt;Application hooks&lt;/code&gt;类型的 ShutdownHook ，由用户自定义实现。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;class ApplicationShutdownHooks {   &lt;br /&gt;    // 存放用户自定义的 Application 类型的 hooks   &lt;br /&gt;    private static IdentityHashMap&amp;lt;Thread, Thread&amp;gt; hooks;   &lt;br /&gt;   &lt;br /&gt;    static synchronized void add(Thread hook) {   &lt;br /&gt;        if(hooks == null)   &lt;br /&gt;            throw new IllegalStateException(&amp;quot;Shutdown in progress&amp;quot;);   &lt;br /&gt;   &lt;br /&gt;        if (hook.isAlive())   &lt;br /&gt;            throw new IllegalArgumentException(&amp;quot;Hook already running&amp;quot;);   &lt;br /&gt;   &lt;br /&gt;        if (hooks.containsKey(hook))   &lt;br /&gt;            throw new IllegalArgumentException(&amp;quot;Hook previously registered&amp;quot;);   &lt;br /&gt;   &lt;br /&gt;        hooks.put(hook, hook);   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;    static void runHooks() {   &lt;br /&gt;        Collection&amp;lt;Thread&amp;gt; threads;   &lt;br /&gt;        synchronized(ApplicationShutdownHooks.class) {   &lt;br /&gt;            threads = hooks.keySet();   &lt;br /&gt;            hooks = null;   &lt;br /&gt;        }   &lt;br /&gt;        // 顺序启动 shutdownhooks   &lt;br /&gt;        for (Thread hook : threads) {   &lt;br /&gt;            hook.start();   &lt;br /&gt;        }   &lt;br /&gt;        // 并发调用 shutdownhooks ，等待所有 hooks 运行完毕退出   &lt;br /&gt;        for (Thread hook : threads) {   &lt;br /&gt;            try {   &lt;br /&gt;                hook.join();   &lt;br /&gt;            } catch (InterruptedException x) { }   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;ApplicationShutdownHooks 类中也有一个集合  &lt;code&gt;IdentityHashMap&amp;lt;Thread, Thread&amp;gt; hooks&lt;/code&gt;，专门用来存放由用户自定义的 Application hooks 类型的 ShutdownHook 。通过 ApplicationShutdownHooks#add 方法添加进 hooks 集合中。&lt;/p&gt; &lt;p&gt;然后在 runHooks 方法里挨个启动 ShutdownHook 线程，并发执行。  &lt;strong&gt;注意这里的 runHooks 方法是 ApplicationShutdownHooks 类中的&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;在 ApplicationShutdownHooks 类的静态代码块 static{.....} 中会将 runHooks 方法封装成 Runnable 添加进 Shutdown 类中的 hooks 数组中。注意这里 Shutdown#add 方法传递进的索引是 1 。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;class ApplicationShutdownHooks {   &lt;br /&gt;    /* The set of registered hooks */   &lt;br /&gt;    private static IdentityHashMap&amp;lt;Thread, Thread&amp;gt; hooks;   &lt;br /&gt;   &lt;br /&gt;    static {   &lt;br /&gt;        try {   &lt;br /&gt;            Shutdown.add(1 /* shutdown hook invocation order */,   &lt;br /&gt;                false /* not registered if shutdown in progress */,   &lt;br /&gt;                new Runnable() {   &lt;br /&gt;                    public void run() {   &lt;br /&gt;                        runHooks();   &lt;br /&gt;                    }   &lt;br /&gt;                }   &lt;br /&gt;            );   &lt;br /&gt;            hooks = new IdentityHashMap&amp;lt;&amp;gt;();   &lt;br /&gt;        } catch (IllegalStateException e) {   &lt;br /&gt;            // application shutdown hooks cannot be added if   &lt;br /&gt;            // shutdown is in progress.   &lt;br /&gt;            hooks = null;   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;img&gt;&lt;/img&gt;Shutdownhook的执行.png &lt;p&gt;Shutdown#add 方法的逻辑就很简单了：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;class Shutdown {   &lt;br /&gt;   &lt;br /&gt;    private static final int MAX_SYSTEM_HOOKS = 10;   &lt;br /&gt;    private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];   &lt;br /&gt;   &lt;br /&gt;    static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {   &lt;br /&gt;        synchronized (lock) {   &lt;br /&gt;            if (hooks[slot] != null)   &lt;br /&gt;                throw new InternalError(&amp;quot;Shutdown hook at slot &amp;quot; + slot + &amp;quot; already registered&amp;quot;);   &lt;br /&gt;   &lt;br /&gt;            if (!registerShutdownInProgress) {   &lt;br /&gt;                if (state &amp;gt; RUNNING)   &lt;br /&gt;                    throw new IllegalStateException(&amp;quot;Shutdown in progress&amp;quot;);   &lt;br /&gt;            } else {   &lt;br /&gt;                if (state &amp;gt; HOOKS || (state == HOOKS &amp;amp;&amp;amp; slot &amp;lt;= currentRunningHook))   &lt;br /&gt;                    throw new IllegalStateException(&amp;quot;Shutdown in progress&amp;quot;);   &lt;br /&gt;            }   &lt;br /&gt;   &lt;br /&gt;            hooks[slot] = hook;   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;参数 Runnable hook 就是在 ApplicationShutdownHooks 中的静态代码块 static{....} 中将 runHooks 方法封装成的 Runnable。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;参数 int slot 表示将封装好的 Runnable 放入 hooks 数组中的哪个槽中。这里我们注册的是 Application hooks 类型的 ShutdonwHook ，所以这里的索引为 1 。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;参数 registerShutdownInProgress 表示是否允许在 JVM 关闭流程开始之后，继续向 JVM 添加 ShutdownHook 。默认为 false 表示不允许。否则将会抛出 IllegalStateException 异常。这一点笔者在小节《3.2 使用ShutdownHook的注意事项》中强调过。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;以上就是 JVM 如何捕获操作系统内核信号，如何注册 ShutdownHook ，以及何时触发 ShutdownHook 的执行的一个全面介绍。&lt;/p&gt; &lt;img&gt;&lt;/img&gt;shutdownhook完整触发时机.png &lt;blockquote&gt;  &lt;p&gt;读到这里大家应该彻底明白了为什么不能使用 kill -9 pid 命令来关闭进程了吧，现在赶快去检查一下你们公司生产环境的运维脚本吧！！&lt;/p&gt;&lt;/blockquote&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;俗话说的好 talk is cheap! show me the code! ，在介绍了这么多关于优雅关闭的理论方案和原理之后，我想大家现在一定很好奇究竟我们该如何实现这一套优雅关闭的方案呢？&lt;/p&gt; &lt;p&gt;那么接下来笔者就从一些知名框架源码实现角度，为大家详细阐述一下优雅关闭是如何实现的？&lt;/p&gt; &lt;p&gt;                           &lt;img&gt;&lt;/img&gt;&lt;/p&gt;image.png &lt;h2&gt;4. Spring 的优雅关闭机制&lt;/h2&gt; &lt;p&gt;前面两个小节中我们从支持优雅关闭最底层的内核信号机制开始聊起然后到 JVM 进程实现优雅关闭的 ShutdwonHook 原理，经过这一系列的介绍，我们现在对优雅关闭在内核层和 JVM 层的相关机制原理有了一定的了解。&lt;/p&gt; &lt;p&gt;那么在真实 Java 应用中，我们到底该如何基于上述机制实现一套优雅关闭方案呢？本小节我们来从 Spring 源码中获取下答案！！&lt;/p&gt; &lt;p&gt;在介绍 Spring 优雅关闭机制源码实现之前，笔者先来带大家回顾下，在 Spring 的应用上下文关闭的时候，Spring 究竟给我们提供了哪些关闭时的回调机制，从而可以让我们在这些回调中编写 Java 应用的优雅关闭逻辑。&lt;/p&gt; &lt;h3&gt;4.1 发布 ContextClosedEvent 事件&lt;/h3&gt; &lt;p&gt;在 Spring 上下文开始关闭的时候，首先会发布 ContextClosedEvent 事件，注意此时 Spring 容器的 Bean 还没有开始销毁，所以我们可以在该事件回调中执行优雅关闭的操作。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;@Component   &lt;br /&gt;public class ShutdownListener implements ApplicationListener&amp;lt;ContextClosedEvent&amp;gt; {   &lt;br /&gt;       @Override   &lt;br /&gt;       public void onApplicationEvent(ContextClosedEvent event) {   &lt;br /&gt;                  ........优雅关闭逻辑.....   &lt;br /&gt;       }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;4.2 Spring 容器中的 Bean 销毁前回调&lt;/h3&gt; &lt;p&gt;当 Spring 开始销毁容器中管理的 Bean 之前，会回调所有实现 DestructionAwareBeanPostProcessor 接口的 Bean 中的 postProcessBeforeDestruction 方法。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;@Component   &lt;br /&gt;public class DestroyBeanPostProcessor implements DestructionAwareBeanPostProcessor {   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {   &lt;br /&gt;   &lt;br /&gt;             ........Spring容器中的Bean开始销毁前回调.......   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;4.3 回调标注 @PreDestroy 注解的方法&lt;/h3&gt; &lt;pre&gt;  &lt;code&gt;@Component   &lt;br /&gt;public class Shutdown {   &lt;br /&gt;    @PreDestroy   &lt;br /&gt;    public void preDestroy() {   &lt;br /&gt;        ......释放资源.......   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;4.4 回调 DisposableBean 接口中的 destroy 方法&lt;/h3&gt; &lt;pre&gt;  &lt;code&gt;@Component   &lt;br /&gt;public class Shutdown implements DisposableBean{   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public void destroy() throws Exception {   &lt;br /&gt;         ......释放资源......   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;4.5 回调自定义的销毁方法&lt;/h3&gt; &lt;pre&gt;  &lt;code&gt;&amp;lt;bean id=&amp;quot;Shutdown&amp;quot; class=&amp;quot;com.test.netty.Shutdown&amp;quot;  destroy-method=&amp;quot;doDestroy&amp;quot;/&amp;gt;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;  &lt;code&gt;public class Shutdown {   &lt;br /&gt;   &lt;br /&gt;    public void doDestroy() {   &lt;br /&gt;        .....自定义销毁方法....   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;4.6 Spring 优雅关闭机制的实现&lt;/h3&gt; &lt;p&gt;Spring 相关应用程序本质上也是一个 JVM 进程，所以 Spring 框架想要实现优雅关闭机制也必须依托于我们在本文第三小节中介绍的 JVM 的 ShutdownHook 机制。&lt;/p&gt; &lt;p&gt;在 Spring 启动的时候，需要向 JVM 注册 ShutdownHook ，当我们执行  &lt;code&gt;kill - 15 pid&lt;/code&gt;命令时，随后 Spring 会在 ShutdownHook 中触发上述介绍的五种回调。&lt;/p&gt; &lt;p&gt;下面我们来看一下 Spring 中 ShutdownHook 的注册逻辑：&lt;/p&gt; &lt;h4&gt;4.6.1 Spring 中 ShutdownHook 的注册&lt;/h4&gt; &lt;pre&gt;  &lt;code&gt;public abstract class AbstractApplicationContext extends DefaultResourceLoader   &lt;br /&gt;  implements ConfigurableApplicationContext, DisposableBean {   &lt;br /&gt;   &lt;br /&gt; @Override   &lt;br /&gt; public void registerShutdownHook() {   &lt;br /&gt;  if (this.shutdownHook == null) {   &lt;br /&gt;   // No shutdown hook registered yet.   &lt;br /&gt;   this.shutdownHook = new Thread() {   &lt;br /&gt;    @Override   &lt;br /&gt;    public void run() {   &lt;br /&gt;     synchronized (startupShutdownMonitor) {   &lt;br /&gt;      doClose();   &lt;br /&gt;     }   &lt;br /&gt;    }   &lt;br /&gt;   };   &lt;br /&gt;   Runtime.getRuntime().addShutdownHook(this.shutdownHook);   &lt;br /&gt;  }   &lt;br /&gt; }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在 Spring 启动的时候，我们需要调用  &lt;code&gt;AbstractApplicationContext#registerShutdownHook&lt;/code&gt;方法向 JVM 注册 Spring 的 ShutdownHook ，从这段源码中我们看出，Spring 将 doClose() 方法封装在 ShutdownHook 线程中，而 doClose() 方法里边就是 Spring 优雅关闭的逻辑。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;这里需要强调的是，当我们在一个纯 Spring 环境下&lt;/strong&gt;，Spring 框架是不会为我们主动调用 registerShutdownHook 方法去向 JVM 注册 ShutdownHook 的，我们需要手动调用 registerShutdownHook 方法去注册。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class SpringShutdownHook {   &lt;br /&gt;   &lt;br /&gt;    public static void main(String[] args) throws IOException {   &lt;br /&gt;        GenericApplicationContext context = new GenericApplicationContext();   &lt;br /&gt;                      ........   &lt;br /&gt;        // 注册 Shutdown Hook   &lt;br /&gt;        context.registerShutdownHook();   &lt;br /&gt;                      ........   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;  &lt;strong&gt;而在 SpringBoot 环境下&lt;/strong&gt;，SpringBoot 在启动的时候会为我们调用这个方法去主动注册 ShutdownHook 。我们不需要手动注册。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class SpringApplication {   &lt;br /&gt;   &lt;br /&gt; public ConfigurableApplicationContext run(String... args) {   &lt;br /&gt;   &lt;br /&gt;                  ...............省略.................   &lt;br /&gt;   &lt;br /&gt;                  ConfigurableApplicationContext context = null;   &lt;br /&gt;                  context = createApplicationContext();   &lt;br /&gt;                  refreshContext(context);   &lt;br /&gt;   &lt;br /&gt;                  ...............省略.................   &lt;br /&gt; }   &lt;br /&gt;   &lt;br /&gt; private void refreshContext(ConfigurableApplicationContext context) {   &lt;br /&gt;  refresh(context);   &lt;br /&gt;  if (this.registerShutdownHook) {   &lt;br /&gt;   try {   &lt;br /&gt;    context.registerShutdownHook();   &lt;br /&gt;   }   &lt;br /&gt;   catch (AccessControlException ex) {   &lt;br /&gt;    // Not allowed in some environments.   &lt;br /&gt;   }   &lt;br /&gt;  }   &lt;br /&gt; }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h4&gt;4.6.2 Spring 中的优雅关闭逻辑&lt;/h4&gt; &lt;pre&gt;  &lt;code&gt; protected void doClose() {   &lt;br /&gt;  // 更新上下文状态   &lt;br /&gt;  if (this.active.get() &amp;amp;&amp;amp; this.closed.compareAndSet(false, true)) {   &lt;br /&gt;   if (logger.isInfoEnabled()) {   &lt;br /&gt;    logger.info(&amp;quot;Closing &amp;quot; + this);   &lt;br /&gt;   }   &lt;br /&gt;            // 取消 JMX 托管   &lt;br /&gt;   LiveBeansView.unregisterApplicationContext(this);   &lt;br /&gt;   &lt;br /&gt;   try {   &lt;br /&gt;    // 发布 ContextClosedEvent 事件   &lt;br /&gt;    publishEvent(new ContextClosedEvent(this));   &lt;br /&gt;   }   &lt;br /&gt;   catch (Throwable ex) {   &lt;br /&gt;    logger.warn(&amp;quot;Exception thrown from ApplicationListener handling ContextClosedEvent&amp;quot;, ex);   &lt;br /&gt;   }   &lt;br /&gt;   &lt;br /&gt;   // 回调 Lifecycle beans,相关 stop 方法   &lt;br /&gt;   if (this.lifecycleProcessor != null) {   &lt;br /&gt;    try {   &lt;br /&gt;     this.lifecycleProcessor.onClose();   &lt;br /&gt;    }   &lt;br /&gt;    catch (Throwable ex) {   &lt;br /&gt;     logger.warn(&amp;quot;Exception thrown from LifecycleProcessor on context close&amp;quot;, ex);   &lt;br /&gt;    }   &lt;br /&gt;   }   &lt;br /&gt;   &lt;br /&gt;   // 销毁 bean，触发前面介绍的几种回调   &lt;br /&gt;   destroyBeans();   &lt;br /&gt;   &lt;br /&gt;   // Close the state of this context itself.   &lt;br /&gt;   closeBeanFactory();   &lt;br /&gt;   &lt;br /&gt;   // Let subclasses do some final clean-up if they wish...   &lt;br /&gt;   onClose();   &lt;br /&gt;   &lt;br /&gt;   // Switch to inactive.   &lt;br /&gt;   this.active.set(false);   &lt;br /&gt;  }   &lt;br /&gt; }   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在这里我们可以看出最终是在 AbstractApplicationContext#doClose 方法中触发本小节开始介绍的五种回调：&lt;/p&gt; &lt;ol&gt;  &lt;li&gt;发布 ContextClosedEvent 事件。   &lt;strong&gt;注意这里是一个同步事件&lt;/strong&gt;，也就是说 Spring 的 ShutdownHook 线程在这里发布完事件之后会继续同步执行事件的处理，等到事件处理完毕后，才会去执行后面的 destroyBeans() 方法对 IOC 容器中的 Bean 进行销毁。&lt;/li&gt;&lt;/ol&gt; &lt;blockquote&gt;  &lt;p&gt;所以在 ContextClosedEvent 事件监听类中，可以放心地去做优雅关闭相关的操作，因为此时 Spring 容器中的 Bean 还没有被销毁。&lt;/p&gt;&lt;/blockquote&gt; &lt;ol start="2"&gt;  &lt;li&gt;destroyBeans() 方法中依次触发剩下的四种回调。&lt;/li&gt;&lt;/ol&gt; &lt;p&gt;最后结合前边小节中介绍的内容，总结 Spring 的整个优雅关闭流程如下图所示：&lt;/p&gt; &lt;img&gt;&lt;/img&gt;Spring优雅关闭机制.png &lt;h2&gt;5. Dubbo 的优雅关闭&lt;/h2&gt; &lt;blockquote&gt;  &lt;p&gt;本小节优雅关闭部分源码基于 apache dubbo   &lt;strong&gt;2.7.7&lt;/strong&gt;版本，该版本中的优雅关闭是有 Bug 的，下面让我们一起来 Shooting Bug !&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;在前边几个小节的内容中，我们从内核提供的底层技术支持开始聊到了 JVM 的 ShutdonwHook ，然后又从 JVM 聊到了 Spring 框架的优雅关闭机制。&lt;/p&gt; &lt;p&gt;在了解了这些内容之后，本小节我们就来看下 dubbo 中的优雅关闭实现，由于现在几乎所有 Java 应用都会采用 Spring 作为开发框架，所以 dubbo 一般是集成在 Spring 框架中供我们使用的，它的优雅关闭和 Spring 有着紧密的联系。&lt;/p&gt; &lt;h3&gt;5.1 Dubbo 在 Spring 环境下的优雅关闭&lt;/h3&gt; &lt;p&gt;在本文第四小节《4. Spring的优雅关闭机制》的介绍中，我们知道在 Spring 的优雅关闭流程中，Spring 的 ShutdownHook 线程会首先发布 ContextClosedEvent 事件，  &lt;strong&gt;该事件是一个同步事件&lt;/strong&gt;，ShutdownHook 线程发布完该事件紧接着就会同步执行该事件的监听器，当在事件监听器中处理完 ContextClosedEvent 事件之后，在回过头来执行 destroyBeans() 方法并依次触发剩下的四种回调来销毁 IOC 容器中的 Bean 。&lt;/p&gt; &lt;img&gt;&lt;/img&gt;Spring优雅关闭流程.png &lt;p&gt;由于在处理 ContextClosedEvent 事件的时候，Dubbo 所依赖的一些关键 bean 这时还没有被销毁，所以 dubbo 定义了一个 DubboBootstrapApplicationListener 用来监听 ContextClosedEvent 事件，并在 onContextClosedEvent 事件处理方法中调用 dubboBootstrap.stop() 方法开启 dubbo 的优雅关闭流程。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class DubboBootstrapApplicationListener extends OneTimeExecutionApplicationContextEventListener   &lt;br /&gt;        implements Ordered {   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public void onApplicationContextEvent(ApplicationContextEvent event) {   &lt;br /&gt;        // 这里是 Spring 的同步事件，publishEvent 和处理 Event 是在同一个线程中   &lt;br /&gt;        if (event instanceof ContextRefreshedEvent) {   &lt;br /&gt;            onContextRefreshedEvent((ContextRefreshedEvent) event);   &lt;br /&gt;        } else if (event instanceof ContextClosedEvent) {   &lt;br /&gt;            onContextClosedEvent((ContextClosedEvent) event);   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;    private void onContextClosedEvent(ContextClosedEvent event) {   &lt;br /&gt;        // spring 在 shutdownhook 中会先触发 ContextClosedEvent ，然后在销毁 spring beans   &lt;br /&gt;        // 所以这里 dubbo 开始优雅关闭时，依赖的 spring beans 并未销毁   &lt;br /&gt;        dubboBootstrap.stop();   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当服务提供者 ServiceBean 和服务消费者 ReferenceBean 被初始化时,会将 DubboBootstrapApplicationListener 注册到 Spring 容器中。并开始监听 ContextClosedEvent 事件和 ContextRefreshedEvent 事件。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class ServiceClassPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware,   &lt;br /&gt;        ResourceLoaderAware, BeanClassLoaderAware {   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {   &lt;br /&gt;   &lt;br /&gt;        // @since 2.7.5 注册spring启动 关闭事件的listener   &lt;br /&gt;        //在事件回调中中调用启动类 DubboBootStrap的start  stop来启动 关闭dubbo应用   &lt;br /&gt;        registerBeans(registry, DubboBootstrapApplicationListener.class);   &lt;br /&gt;         &lt;br /&gt;                  ........省略.......   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;5.2 Dubbo 优雅关闭流程简介&lt;/h3&gt; &lt;blockquote&gt;  &lt;p&gt;由于本文的主题是介绍优雅关闭的一整条流程主线，所以这里笔者只是简要介绍 Dubbo 优雅关闭的主流程，相关细节部分笔者会在后续的 dubbo 源码解析系列里为大家详细介绍 Dubbo 优雅关闭的细节。为了避免本文发散太多，我们这里还是聚焦于流程主线。&lt;/p&gt;&lt;/blockquote&gt; &lt;pre&gt;  &lt;code&gt;public class DubboBootstrap extends GenericEventListener {   &lt;br /&gt;   &lt;br /&gt;    public DubboBootstrap stop() throws IllegalStateException {   &lt;br /&gt;        destroy();   &lt;br /&gt;        return this;   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;这里的核心逻辑其实就是我们在《1.2 优雅关闭》小节中介绍的两大优雅关闭主题：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;从当前正在关闭的应用实例上切走现有生产流量。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;保证业务无损。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;blockquote&gt;  &lt;p&gt;这里大家只需要了解 Dubbo 优雅关闭的主流程即可，相关细节笔者后续会有一篇专门的文章详细为大家介绍。&lt;/p&gt;&lt;/blockquote&gt; &lt;pre&gt;  &lt;code&gt;    public void destroy() {   &lt;br /&gt;        if (destroyLock.tryLock()) {   &lt;br /&gt;            try {   &lt;br /&gt;                DubboShutdownHook.destroyAll();   &lt;br /&gt;   &lt;br /&gt;                if (started.compareAndSet(true, false)   &lt;br /&gt;                        &amp;amp;&amp;amp; destroyed.compareAndSet(false, true)) {   &lt;br /&gt;   &lt;br /&gt;                    //取消注册   &lt;br /&gt;                    unregisterServiceInstance();   &lt;br /&gt;                    //取消元数据服务   &lt;br /&gt;                    unexportMetadataService();   &lt;br /&gt;                    //停止暴露服务   &lt;br /&gt;                    unexportServices();   &lt;br /&gt;                    //取消订阅服务   &lt;br /&gt;                    unreferServices();   &lt;br /&gt;                    //注销注册中心   &lt;br /&gt;                    destroyRegistries();   &lt;br /&gt;                    //关闭服务   &lt;br /&gt;                    DubboShutdownHook.destroyProtocols();   &lt;br /&gt;                    //销毁注册中心客户端实例   &lt;br /&gt;                    destroyServiceDiscoveries();   &lt;br /&gt;                    //清除应用配置类以及相关应用模型   &lt;br /&gt;                    clear();   &lt;br /&gt;                    //关闭线程池   &lt;br /&gt;                    shutdown();   &lt;br /&gt;                    //释放资源   &lt;br /&gt;                    release();   &lt;br /&gt;                }   &lt;br /&gt;            } finally {   &lt;br /&gt;                destroyLock.unlock();   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;从以上内容可以看出，Dubbo 的优雅关闭依托于 Spring ContextClosedEvent 事件的发布，而 ContextClosedEvent 事件的发布又依托于 Spring ShutdownHook 的注册。&lt;/p&gt; &lt;img&gt;&lt;/img&gt;dubbo spring环境优雅关闭.png &lt;p&gt;从《4.6.1 Spring 中 ShutdownHook 的注册》小节的介绍中我们知道，在 SpringBoot 环境下，SpringBoot 在启动的时候会为我们调用  &lt;code&gt;ApplicationContext#registerShutdownHook&lt;/code&gt;方法去主动注册 ShutdownHook 。我们不需要手动注册。&lt;/p&gt; &lt;p&gt;而在一个纯 Spring 环境下，Spring 框架并不会为我们主动调用 registerShutdownHook 方法去向 JVM 注册 ShutdownHook 的，我们需要手动调用 registerShutdownHook 方法去注册。&lt;/p&gt; &lt;p&gt;所以 Dubbo 这里为了兼容 SpringBoot 环境和纯 Spring 环境下的优雅关闭，引入了  &lt;code&gt;SpringExtensionFactory类&lt;/code&gt;，只要在 Spring 环境下都会调用 registerShutdownHook 去向 JVM 注册 Spring 的 ShutdownHook 。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class SpringExtensionFactory implements ExtensionFactory {   &lt;br /&gt;    private static final Logger logger = LoggerFactory.getLogger(SpringExtensionFactory.class);   &lt;br /&gt;   &lt;br /&gt;    private static final Set&amp;lt;ApplicationContext&amp;gt; CONTEXTS = new ConcurrentHashSet&amp;lt;ApplicationContext&amp;gt;();   &lt;br /&gt;   &lt;br /&gt;    public static void addApplicationContext(ApplicationContext context) {   &lt;br /&gt;        CONTEXTS.add(context);   &lt;br /&gt;        if (context instanceof ConfigurableApplicationContext) {   &lt;br /&gt;            //在spring启动成功之后设置shutdownHook（兼容非SpringBoot环境）   &lt;br /&gt;            ((ConfigurableApplicationContext) context).registerShutdownHook();   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当服务提供者 ServiceBean 和服务消费者 ReferenceBean 在初始化完成之后，会回调  &lt;code&gt;SpringExtensionFactory#addApplicationContext&lt;/code&gt;方法注册 ShutdownHook 。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class ServiceBean&amp;lt;T&amp;gt; extends ServiceConfig&amp;lt;T&amp;gt; implements InitializingBean, DisposableBean,   &lt;br /&gt;        ApplicationContextAware, BeanNameAware, ApplicationEventPublisherAware {   &lt;br /&gt;   &lt;br /&gt;   @Override   &lt;br /&gt;    public void setApplicationContext(ApplicationContext applicationContext) {   &lt;br /&gt;        this.applicationContext = applicationContext;   &lt;br /&gt;        SpringExtensionFactory.addApplicationContext(applicationContext);   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;  &lt;code&gt;public class ReferenceBean&amp;lt;T&amp;gt; extends ReferenceConfig&amp;lt;T&amp;gt; implements FactoryBean,   &lt;br /&gt;        ApplicationContextAware, InitializingBean, DisposableBean {   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public void setApplicationContext(ApplicationContext applicationContext) {   &lt;br /&gt;        this.applicationContext = applicationContext;   &lt;br /&gt;        SpringExtensionFactory.addApplicationContext(applicationContext);   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;以上就是 Dubbo 在 Spring 集成环境下的优雅关闭全流程，下面我们来看下 Dubbo 在非 Spring 环境下的优雅关闭流程。&lt;/p&gt; &lt;h3&gt;5.3 Dubbo 在非 Spring 环境下的优雅关闭&lt;/h3&gt; &lt;p&gt;在上小节的介绍中我们知道 Dubbo 在 Spring 环境下依托 Spring 的 ShutdownHook ，通过监听 ContextClosedEvent 事件，从而触发 Dubbo 的优雅关闭流程。&lt;/p&gt; &lt;p&gt;而到了非 Spring 环境下，Dubbo 就需要定义自己的 ShutdownHook ，从而引入了 DubboShutdownHook ，直接将优雅关闭流程封装在自己的 ShutdownHook 中执行。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class DubboBootstrap extends GenericEventListener {   &lt;br /&gt;   &lt;br /&gt;    private DubboBootstrap() {   &lt;br /&gt;        configManager = ApplicationModel.getConfigManager();   &lt;br /&gt;        environment = ApplicationModel.getEnvironment();   &lt;br /&gt;   &lt;br /&gt;        DubboShutdownHook.getDubboShutdownHook().register();   &lt;br /&gt;        ShutdownHookCallbacks.INSTANCE.addCallback(new ShutdownHookCallback() {   &lt;br /&gt;            @Override   &lt;br /&gt;            public void callback() throws Throwable {   &lt;br /&gt;                DubboBootstrap.this.destroy();   &lt;br /&gt;            }   &lt;br /&gt;        });   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;  &lt;code&gt;public class DubboShutdownHook extends Thread {   &lt;br /&gt;   &lt;br /&gt;   public void register() {   &lt;br /&gt;        if (registered.compareAndSet(false, true)) {   &lt;br /&gt;            DubboShutdownHook dubboShutdownHook = getDubboShutdownHook();   &lt;br /&gt;            Runtime.getRuntime().addShutdownHook(dubboShutdownHook);   &lt;br /&gt;            dispatch(new DubboShutdownHookRegisteredEvent(dubboShutdownHook));   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public void run() {   &lt;br /&gt;        if (logger.isInfoEnabled()) {   &lt;br /&gt;            logger.info(&amp;quot;Run shutdown hook now.&amp;quot;);   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        callback();   &lt;br /&gt;        doDestroy();   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;   private void callback() {   &lt;br /&gt;        callbacks.callback();   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;从源码中我们看到，当我们的 Dubbo 应用程序接收到  &lt;code&gt;kill -15 pid&lt;/code&gt;信号时，JVM 捕获到 SIGTERM(15) 信号之后，就会触发 DubboShutdownHook 线程运行，从而通过 callback() 又回调了上小节中介绍的 DubboBootstrap#destroy 方法（dubbo 的整个优雅关闭逻辑全部封装在这里）。&lt;/p&gt; &lt;img&gt;&lt;/img&gt;dubbo 非Spring环境下优雅关闭流程.png &lt;pre&gt;  &lt;code&gt;public class DubboBootstrap extends GenericEventListener {   &lt;br /&gt;   &lt;br /&gt;    public void destroy() {   &lt;br /&gt;        if (destroyLock.tryLock()) {   &lt;br /&gt;            try {   &lt;br /&gt;                DubboShutdownHook.destroyAll();   &lt;br /&gt;   &lt;br /&gt;                if (started.compareAndSet(true, false)   &lt;br /&gt;                        &amp;amp;&amp;amp; destroyed.compareAndSet(false, true)) {   &lt;br /&gt;   &lt;br /&gt;                    ........取消注册......   &lt;br /&gt;                     &lt;br /&gt;                    ........取消元数据服务........   &lt;br /&gt;                     &lt;br /&gt;                    ........停止暴露服务........   &lt;br /&gt;                    &lt;br /&gt;                    ........取消订阅服务........   &lt;br /&gt;                    &lt;br /&gt;                    ........注销注册中心........   &lt;br /&gt;                    &lt;br /&gt;                    ........关闭服务........   &lt;br /&gt;                     &lt;br /&gt;                    ........销毁注册中心客户端实例........   &lt;br /&gt;                    &lt;br /&gt;                    ........清除应用配置类以及相关应用模型........   &lt;br /&gt;                   &lt;br /&gt;                    ........关闭线程池........   &lt;br /&gt;                    &lt;br /&gt;                    ........释放资源........   &lt;br /&gt;                    &lt;br /&gt;                }   &lt;br /&gt;            } finally {   &lt;br /&gt;                destroyLock.unlock();   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;5.4 啊哈！Bug!&lt;/h3&gt; &lt;p&gt;前边我们在《5.1 Dubbo在Spring环境下的优雅关闭》小节和《5.3 Dubbo在非Spring环境下的优雅关闭》小节中介绍的这两个环境的下的优雅关闭方案，当它们在各自的场景下运行的时候是没有任何问题的。&lt;/p&gt; &lt;p&gt;但是当这两种方案结合在一起运行，就出大问题了~~~&lt;/p&gt; &lt;p&gt;还记得笔者在《3.2 使用 ShutdownHook 的注意事项》小节中特别强调的一点：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;ShutdownHook 其实本质上是一个已经被初始化但是未启动的 Thread ，这些通过   &lt;code&gt;Runtime.getRuntime().addShutdownHook&lt;/code&gt;方法注册的 ShutdownHooks ，在 JVM 进程关闭的时候会被启动   &lt;strong&gt;并发执行，但是并不会保证执行顺序&lt;/strong&gt;。&lt;/li&gt;&lt;/ul&gt; &lt;blockquote&gt;  &lt;p&gt;所以在编写 ShutdownHook 中的逻辑时，我们应该确保程序的线程安全性，并尽可能避免死锁。最好是一个 JVM 进程只注册一个 ShutdownHook 。&lt;/p&gt;&lt;/blockquote&gt; &lt;img&gt;&lt;/img&gt;Dubbo在Spring环境下的优雅关闭Bug.png &lt;p&gt;那么现在 JVM 中我们注册了两个 ShutdownHook 线程，一个 Spring 的 ShutdownHook ，另一个是 Dubbo 的 ShutdonwHook 。那么这会引出什么问题呢？&lt;/p&gt; &lt;p&gt;经过前边的内容介绍我们知道，无论是在 Spring 的 ShutdownHook 中触发的 ContextClosedEvent 事件还是在 Dubbo 的 ShutdownHook 中执行的 CallBack 。最终都会调用到  &lt;code&gt;DubboBootstrap#destroy&lt;/code&gt;方法执行真正的优雅关闭逻辑。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class DubboBootstrap extends GenericEventListener {   &lt;br /&gt;   &lt;br /&gt;    private final Lock destroyLock = new ReentrantLock();   &lt;br /&gt;   &lt;br /&gt;    public void destroy() {   &lt;br /&gt;        if (destroyLock.tryLock()) {   &lt;br /&gt;            try {   &lt;br /&gt;                DubboShutdownHook.destroyAll();   &lt;br /&gt;   &lt;br /&gt;                if (started.compareAndSet(true, false)   &lt;br /&gt;                        &amp;amp;&amp;amp; destroyed.compareAndSet(false, true)) {   &lt;br /&gt;                       &lt;br /&gt;                        .......dubbo应用的优雅关闭.......   &lt;br /&gt;                    &lt;br /&gt;                }   &lt;br /&gt;            } finally {   &lt;br /&gt;                destroyLock.unlock();   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;让我们来设想一个这种的场景：当 Spring 的 ShutdownHook 线程和 Dubbo 的 ShutdownHook 线程同时执行并且在同一个时间点来到 DubboBootstrap#destroy 方法中争夺 destroyLock 。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;Dubbo 的 ShutdownHook 线程获得 destroyLock 进入 destroy() 方法体开始执行优雅关闭逻辑。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;Spring 的 ShutdownHook 线程没有获得 destroyLock，退出 destroy() 方法。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;img&gt;&lt;/img&gt;Dubbo优雅关闭Bug.png &lt;p&gt;在 Spring 的 ShutdownHook 线程退出 destroy() 方法之后紧接着就会执行 destroyBeans() 方法销毁 IOC 容器中的 Bean ，这里边肯定涉及到一些关键业务 Bean 的销毁，比如：数据库连接池，以及 Dubbo 相关的核心 Bean。&lt;/p&gt; &lt;p&gt;于此同时 Dubbo 的 ShutdownHook 线程开始执行优雅关闭逻辑，《1.2 优雅关闭》小节中我们提到，优雅关闭要保证业务无损。所以需要将剩下正在进行中的业务流程继续处理完毕并将业务处理结果响应给客户端。但是这时依赖的一些业务关键 Bean 已经被销毁，比如数据库连接池，这时执行数据库操作就会抛出  &lt;code&gt;CannotGetJdbcConnectionException&lt;/code&gt;。导致优雅关闭失败，对业务造成了影响。&lt;/p&gt; &lt;h3&gt;5.5 Bug 的修复&lt;/h3&gt; &lt;blockquote&gt;  &lt;p&gt;该 Bug 最终在   &lt;strong&gt;apache dubbo 2.7.15&lt;/strong&gt;版本中被修复&lt;/p&gt;&lt;/blockquote&gt; &lt;blockquote&gt;  &lt;p&gt;详情可查看Issue：https://github.com/apache/dubbo/issues/7093&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;经过上小节的分析，我们知道既然这个 Bug 产生的原因是由于 Spring 的 ShutdownHook 线程和 Dubbo 的 ShutdownHook 线程并发执行所导致的。&lt;/p&gt; &lt;p&gt;那么当我们处于 Spring 环境下的时候，就将 Dubbo 的 ShutdownHook 注销掉即可。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class SpringExtensionFactory implements ExtensionFactory {   &lt;br /&gt;    private static final Logger logger = LoggerFactory.getLogger(SpringExtensionFactory.class);   &lt;br /&gt;   &lt;br /&gt;    private static final Set&amp;lt;ApplicationContext&amp;gt; CONTEXTS = new ConcurrentHashSet&amp;lt;ApplicationContext&amp;gt;();   &lt;br /&gt;   &lt;br /&gt;    public static void addApplicationContext(ApplicationContext context) {   &lt;br /&gt;        CONTEXTS.add(context);   &lt;br /&gt;        if (context instanceof ConfigurableApplicationContext) {   &lt;br /&gt;            // 注册 Spring 的 ShutdownHook   &lt;br /&gt;            ((ConfigurableApplicationContext) context).registerShutdownHook();   &lt;br /&gt;            // 在 Spring 环境下将 Dubbo 的 ShutdownHook 取消掉   &lt;br /&gt;            DubboShutdownHook.getDubboShutdownHook().unregister();   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;而在非 Spring 环境下，我们依然保留 Dubbo 的 ShutdownHook 。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class DubboBootstrap {   &lt;br /&gt;   &lt;br /&gt;    private DubboBootstrap() {   &lt;br /&gt;        configManager = ApplicationModel.getConfigManager();   &lt;br /&gt;        environment = ApplicationModel.getEnvironment();   &lt;br /&gt;   &lt;br /&gt;        DubboShutdownHook.getDubboShutdownHook().register();   &lt;br /&gt;        ShutdownHookCallbacks.INSTANCE.addCallback(DubboBootstrap.this::destroy);   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;以上内容就是 Dubbo 的整个优雅关闭主线流程，以及优雅关闭 Bug 产生的原因和修复方案。&lt;/p&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;在 Dubbo 的优雅关闭流程中最终会通过  &lt;code&gt;DubboShutdownHook.destroyProtocols()&lt;/code&gt;关闭底层服务。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class DubboBootstrap extends GenericEventListener {   &lt;br /&gt;   &lt;br /&gt;    private final Lock destroyLock = new ReentrantLock();   &lt;br /&gt;   &lt;br /&gt;    public void destroy() {   &lt;br /&gt;        if (destroyLock.tryLock()) {   &lt;br /&gt;            try {   &lt;br /&gt;                DubboShutdownHook.destroyAll();   &lt;br /&gt;   &lt;br /&gt;                if (started.compareAndSet(true, false)   &lt;br /&gt;                        &amp;amp;&amp;amp; destroyed.compareAndSet(false, true)) {   &lt;br /&gt;                       &lt;br /&gt;                        .......dubbo应用的优雅关闭.......   &lt;br /&gt;                    //关闭服务   &lt;br /&gt;                    DubboShutdownHook.destroyProtocols();   &lt;br /&gt;   &lt;br /&gt;                        .......dubbo应用的优雅关闭.......   &lt;br /&gt;   &lt;br /&gt;                }   &lt;br /&gt;            } finally {   &lt;br /&gt;                destroyLock.unlock();   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在 Dubbo 服务的销毁过程中，会通过调用 server.close 关闭底层的 Netty 服务。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class DubboProtocol extends AbstractProtocol {   &lt;br /&gt;   &lt;br /&gt;   @Override   &lt;br /&gt;    public void destroy() {   &lt;br /&gt;        for (String key : new ArrayList&amp;lt;&amp;gt;(serverMap.keySet())) {   &lt;br /&gt;            ProtocolServer protocolServer = serverMap.remove(key);   &lt;br /&gt;            RemotingServer server = protocolServer.getRemotingServer();   &lt;br /&gt;            server.close(ConfigurationUtils.getServerShutdownTimeout());   &lt;br /&gt;             ...........省略........   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;         ...........省略........   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;最终触发 Netty 的优雅关闭。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public class NettyServer extends AbstractServer implements RemotingServer {   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    protected void doClose() throws Throwable {   &lt;br /&gt;        ..........关闭底层Channel......   &lt;br /&gt;        try {   &lt;br /&gt;            if (bootstrap != null) {   &lt;br /&gt;                // 关闭 Netty 的主从 Reactor 线程组   &lt;br /&gt;                bossGroup.shutdownGracefully();   &lt;br /&gt;                workerGroup.shutdownGracefully();   &lt;br /&gt;            }   &lt;br /&gt;        } catch (Throwable e) {   &lt;br /&gt;            logger.warn(e.getMessage(), e);   &lt;br /&gt;        }   &lt;br /&gt;        .........清理缓存Channel数据.......   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;6. Netty 的优雅关闭&lt;/h2&gt; &lt;p&gt;通过上小节介绍 dubbo 优雅关闭的相关内容，我们很自然的引出了 Netty 的优雅关闭触发时机，那么在本小节中笔者将为大家详细介绍下 Netty 是如何优雅地装..........优雅地谢幕的~~&lt;/p&gt; &lt;p&gt;               &lt;img&gt;&lt;/img&gt;&lt;/p&gt;image.png &lt;p&gt;在之前的系列文章中，我们围绕下图所展示的 Netty 整个核心框架的运转流程介绍了主从 ReactorGroup 的  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247483907&amp;idx=1&amp;sn=084c470a8fe6234c2c9461b5f713ff30&amp;chksm=ce77c444f9004d52e7c6244bee83479070effb0bc59236df071f4d62e91e25f01715fca53696&amp;scene=21#wechat_redirect"&gt;创建&lt;/a&gt;，  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247484005&amp;idx=1&amp;sn=52f51269902a58f40d33208421109bc3&amp;chksm=ce77c422f9004d340e5b385ef6ba24dfba1f802076ace80ad6390e934173a10401e64e13eaeb&amp;scene=21#wechat_redirect"&gt;启动&lt;/a&gt;，  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247484087&amp;idx=1&amp;sn=0c065780e0f05c23c8e6465ede86cba0&amp;chksm=ce77c4f0f9004de63be369a664105708bc5975b52993f4a6df223caed34cc1ef6185a16acd75&amp;scene=21#wechat_redirect"&gt;运行&lt;/a&gt;，  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247484184&amp;idx=1&amp;sn=726877ce28cf6e5d2ac3225fae687f19&amp;chksm=ce77c55ff9004c493b592288819dc4d4664b5949ee97fed977b6558bc517dad0e1f73fab0f46&amp;scene=21#wechat_redirect"&gt;接收网络连接&lt;/a&gt;，  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247484244&amp;idx=1&amp;sn=831060fc38caa201d69f87305de7f86a&amp;chksm=ce77c513f9004c05b48f849ff99997d6d7252453135ae856a029137b88aa70b8e046013d596e&amp;scene=21#wechat_redirect"&gt;接收网络数据&lt;/a&gt;，  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247484532&amp;idx=1&amp;sn=c3a8b37a2eb09509d9914494ef108c68&amp;chksm=ce77c233f9004b25a29f9fdfb179e41646092d09bc89df2147a9fab66df13231e46dd6a5c26d&amp;scene=21#wechat_redirect"&gt;发送网络数据&lt;/a&gt;，以及  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247484823&amp;idx=1&amp;sn=9396fb0f5dbac5e32d0fa1129d385fbc&amp;chksm=ce77c3d0f9004ac678283b7d178835e740eb5e5b80740cc624ae7a307be7a7eb0c7766842878&amp;scene=21#wechat_redirect"&gt;如何在pipeline中处理相关IO事件&lt;/a&gt;的整个源码实现。&lt;/p&gt; &lt;img&gt;&lt;/img&gt;netty中的reactor.png &lt;p&gt;本小节就到了 Netty 优雅谢幕的时刻了，在这谢幕的过程中，Netty 会对它的主从 ReactorGroup ，以及对应 ReactorGroup 中的 Reacto r进行优雅的关闭。下面让我们一起来看下这个优雅关闭的过程~~~&lt;/p&gt; &lt;h3&gt;6.1 ReactorGroup 的优雅谢幕&lt;/h3&gt; &lt;pre&gt;  &lt;code&gt;   &lt;br /&gt;public abstract class AbstractEventExecutorGroup implements EventExecutorGroup {   &lt;br /&gt;   &lt;br /&gt;    static final long DEFAULT_SHUTDOWN_QUIET_PERIOD = 2;   &lt;br /&gt;    static final long DEFAULT_SHUTDOWN_TIMEOUT = 15;   &lt;br /&gt;   &lt;br /&gt;   @Override   &lt;br /&gt;    public Future&amp;lt;?&amp;gt; shutdownGracefully() {   &lt;br /&gt;        return shutdownGracefully(DEFAULT_SHUTDOWN_QUIET_PERIOD, DEFAULT_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS);   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在 Netty 进行优雅关闭的整个过程中，这里涉及到了两个非常重要的控制参数：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;gracefulShutdownQuietPeriod&lt;/code&gt;：优雅关闭静默期，默认为    &lt;code&gt;2s&lt;/code&gt;。这个参数主要来保证 Netty 整个关闭过程中的    &lt;strong&gt;优雅&lt;/strong&gt;。在关闭流程开始后，如果 Reactor 中还有遗留的异步任务需要执行，那么 Netty 就不能关闭，需要把所有异步任务执行完毕才可以。当所有异步任务执行完毕后，Netty 为了实现更加优雅的关闭操作，一定要保障业务无损，这时候就引入了静默期这个概念，如果在这个静默期内，用户没有新的任务向 Reactor 提交那么就开始关闭。如果在这个静默期内，还有用户继续提交异步任务，那么就不能关闭，需要把静默期内用户提交的异步任务执行完毕才可以放心关闭。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;gracefulShutdownTimeout&lt;/code&gt;：优雅关闭超时时间，默认为    &lt;code&gt;15s&lt;/code&gt;。这个参数主要来保证 Netty 整个关闭过程的    &lt;strong&gt;可控&lt;/strong&gt;。我们知道一个生产级的优雅关闭方案既要保证优雅做到业务无损，更重要的是要保证关闭流程的可控，不能无限制的优雅下去。导致长时间无法完成关闭动作。于是 Netty 就引入了这个参数，如果优雅关闭超时，那么无论此时有无异步任务需要执行都要开始关闭了。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;这两个控制参数是非常重要核心的两个参数，我们在后面介绍 Netty 关闭细节的时候还会为大家详细剖析，这里大家先从概念上大概理解一下。&lt;/p&gt; &lt;p&gt;在介绍完这两个重要核心参数之后，我们接下来看下 ReactorGroup 的关闭流程：&lt;/p&gt; &lt;p&gt;我们都知道 Netty 为了保证整个系统的吞吐量以及保证 Reactor 可以线程安全地，有序地处理各个 Channel 上的 IO 事件。基于这个目的 Netty 将其承载的海量连接分摊打散到不同的 Reactor 上处理。&lt;/p&gt; &lt;p&gt;ReactorGroup 中包含多个 Reactor ，每个 Channel 只能注册到一个固定的 Reactor 上，由这个固定的 Reactor 负责处理该 Channel 上整个生命周期的事件。&lt;/p&gt; &lt;p&gt;一个 Reactor 上注册了多个 Channel ，负责处理注册在其上的所有 Channel 的 IO 事件以及异步任务。&lt;/p&gt; &lt;p&gt;ReactorGroup 的结构如下图所示：&lt;/p&gt; &lt;img&gt;&lt;/img&gt;image.png &lt;p&gt;ReactorGroup 的关闭流程本质上其实是 ReactorGroup 中包含的所有 Reactor 的关闭，当 ReactorGroup 中的所有 Reactor 完成关闭后，ReactorGroup 才算是真正的关闭。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;   &lt;br /&gt;public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup {   &lt;br /&gt;   &lt;br /&gt;    // Reactor线程组中的Reactor集合   &lt;br /&gt;    private final EventExecutor[] children;   &lt;br /&gt;   &lt;br /&gt;    // 关闭future   &lt;br /&gt;    private final Promise&amp;lt;?&amp;gt; terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE);   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public Future&amp;lt;?&amp;gt; shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {   &lt;br /&gt;        for (EventExecutor l: children) {   &lt;br /&gt;            l.shutdownGracefully(quietPeriod, timeout, unit);   &lt;br /&gt;        }   &lt;br /&gt;        return terminationFuture();   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public Future&amp;lt;?&amp;gt; terminationFuture() {   &lt;br /&gt;        return terminationFuture;   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;EventExecutor[] children&lt;/code&gt;：数组中存放的是当前 ReactorGroup 中包含的所有 Reactor，类型为 EventExecutor。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;Promise&amp;lt;?&amp;gt; terminationFuture&lt;/code&gt;：ReactorGroup 中的关闭 Future ，用户线程通过这个 terminationFuture 可以知道 ReactorGroup 完成关闭的时机，也可以向 terminationFuture 注册一些 listener 。当 ReactorGroup 完成关闭动作后，会回调用户注册的这些 listener 。大家可以根据各自的业务场景灵活运用。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;在 ReactorGroup 的关闭过程中，会挨个触发它所包含的所有 Reactor 的关闭流程。并返回 terminationFuture 给用户线程。&lt;/p&gt; &lt;p&gt;当 ReactorGroup 中的所有 Reactor 完成关闭之后，这个 terminationFuture 会被设置为 success，这样一来用户线程可以感知到 ReactorGroup 已经完成关闭了。&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;这一点笔者也在   &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247483907&amp;idx=1&amp;sn=084c470a8fe6234c2c9461b5f713ff30&amp;chksm=ce77c444f9004d52e7c6244bee83479070effb0bc59236df071f4d62e91e25f01715fca53696&amp;token=1670680185&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;《Reactor在Netty中的实现（创建篇）》&lt;/a&gt;一文中的第四小节《4. 向Reactor线程组中所有的Reactor注册terminated回调函数》强调过。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;在 ReactorGroup 创建的最后一步，会定义 Reactor 关闭的 terminationListener。在 Reactor 的 terminationListener 中会判断当前 ReactorGroup 中的 Reactor 是否全部关闭，如果已经全部关闭，则会设置 ReactorGroup的 terminationFuture 为 success 。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;    //记录关闭的Reactor个数，当Reactor全部关闭后，ReactorGroup才可以认为关闭成功   &lt;br /&gt;    private final AtomicInteger terminatedChildren = new AtomicInteger();   &lt;br /&gt;    //ReactorGroup的关闭future   &lt;br /&gt;    private final Promise&amp;lt;?&amp;gt; terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE);   &lt;br /&gt;   &lt;br /&gt;    protected MultithreadEventExecutorGroup(int nThreads, Executor executor,   &lt;br /&gt;                                            EventExecutorChooserFactory chooserFactory, Object... args) {   &lt;br /&gt;   &lt;br /&gt;        ........挨个创建Reactor............   &lt;br /&gt;   &lt;br /&gt;        final FutureListener&amp;lt;Object&amp;gt; terminationListener = new FutureListener&amp;lt;Object&amp;gt;() {   &lt;br /&gt;            @Override   &lt;br /&gt;            public void operationComplete(Future&amp;lt;Object&amp;gt; future) throws Exception {   &lt;br /&gt;                if (terminatedChildren.incrementAndGet() == children.length) {   &lt;br /&gt;                    //当所有Reactor关闭后 ReactorGroup才认为是关闭成功   &lt;br /&gt;                    terminationFuture.setSuccess(null);   &lt;br /&gt;                }   &lt;br /&gt;            }   &lt;br /&gt;        };   &lt;br /&gt;   &lt;br /&gt;        for (EventExecutor e: children) {   &lt;br /&gt;            //向每个Reactor注册terminationListener   &lt;br /&gt;            e.terminationFuture().addListener(terminationListener);   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;从以上 ReactorGroup 的关闭流程我们可以看出，ReactorGroup 的关闭逻辑只是挨个去触发它所包含的所有 Reactor 的关闭，Netty 的整个优雅关闭核心其实是在单个 Reactor 的关闭逻辑上。毕竟 Reactor 才是真正驱动 Netty 运转的核心引擎。&lt;/p&gt; &lt;h3&gt;6.2 Reactor 的优雅谢幕&lt;/h3&gt; &lt;img&gt;&lt;/img&gt;Reactor的优雅谢幕流程.png &lt;p&gt;Reactor 的状态特别重要，从  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247484087&amp;idx=1&amp;sn=0c065780e0f05c23c8e6465ede86cba0&amp;chksm=ce77c4f0f9004de63be369a664105708bc5975b52993f4a6df223caed34cc1ef6185a16acd75&amp;scene=21#wechat_redirect"&gt;《一文聊透Netty核心引擎Reactor的运转架构》&lt;/a&gt;一文中我们知道 Reactor 是在一个 for (;;) {....} 死循环中 996 不停地工作。比如轮询 Channel 上的 IO 就绪事件，处理 IO 就绪事件，执行异步任务就是在这个死循环中完成的。&lt;/p&gt; &lt;p&gt;而 Reactor 在每一次循环任务结束之后，都会先去判断一下当前 Reactor 的状态，如果状态变为准备关闭状态 ST_SHUTTING_DOWN 后，Reactor 就会开启优雅关闭流程。&lt;/p&gt; &lt;p&gt;所以在介绍 Reactor 的关闭流程之前，笔者先来为大家捋一捋 Reactor 中的各种状态。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;ST_NOT_STARTED = 1&lt;/code&gt;：Reactor 的初始状态。在 Reactor 刚被创建出来的时候，状态为 ST_NOT_STARTED 。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;ST_STARTED = 2&lt;/code&gt;：Reactor 的启动状态。当向 Reactor 提交第一个异步任务的时候会触发 Reactor 的启动。启动之后状态变为 ST_STARTED 。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;blockquote&gt;  &lt;p&gt;相关细节可在回顾下   &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247484005&amp;idx=1&amp;sn=52f51269902a58f40d33208421109bc3&amp;chksm=ce77c422f9004d340e5b385ef6ba24dfba1f802076ace80ad6390e934173a10401e64e13eaeb&amp;token=1670680185&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;《详细图解Netty Reactor启动全流程》&lt;/a&gt;一文。&lt;/p&gt;&lt;/blockquote&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;ST_SHUTTING_DOWN = 3&lt;/code&gt;：Reactor 准备开始关闭状态。当 Reactor 的 shutdownGracefully 方法被调用的时候，Reactor 的状态就会变为ST_SHUTTING_DOWN。在这个状态下，用户仍然可以向 Reactor 提交任务。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;ST_SHUTDOWN = 4&lt;/code&gt;：Reactor 停止状态。表示 Reactor 的优雅关闭流程已经结束，    &lt;strong&gt;此时用户不能在向 Reactor 提交任务&lt;/strong&gt;，Reactor 会在这个状态下最后一次执行剩余的异步任务。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;ST_TERMINATED = 5&lt;/code&gt;：Reactor 真正的终结状态，该状态表示 Reactor 已经完全关闭了。在这个状态下 Reactor 会设置自己的 terminationFuture 为 Success。进而开始回调上小节末尾提到的 terminationListener 。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;在我们了解了 Reactor 的各种状态之后，下面就该来正式开始介绍 Reactor 的关闭流程了：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {   &lt;br /&gt;   &lt;br /&gt;    //Reactor的状态  初始为未启动状态   &lt;br /&gt;    private volatile int state = ST_NOT_STARTED;   &lt;br /&gt;    &lt;br /&gt;    //Reactor的初始状态，未启动   &lt;br /&gt;    private static final int ST_NOT_STARTED = 1;   &lt;br /&gt;    //Reactor启动后的状态   &lt;br /&gt;    private static final int ST_STARTED = 2;   &lt;br /&gt;    //准备正在进行优雅关闭，此时用户仍然可以提交任务，Reactor仍可以执行任务   &lt;br /&gt;    private static final int ST_SHUTTING_DOWN = 3;   &lt;br /&gt;    //Reactor停止状态，表示优雅关闭结束，此时用户不能在提交任务，Reactor最后一次执行剩余的任务   &lt;br /&gt;    private static final int ST_SHUTDOWN = 4;   &lt;br /&gt;    //Reactor中的任务已被全部执行完毕，且不在接受新的任务，真正的终止状态   &lt;br /&gt;    private static final int ST_TERMINATED = 5;   &lt;br /&gt;   &lt;br /&gt;    //优雅关闭的静默期   &lt;br /&gt;    private volatile long gracefulShutdownQuietPeriod;   &lt;br /&gt;    //优雅关闭超时时间   &lt;br /&gt;    private volatile long gracefulShutdownTimeout;   &lt;br /&gt;   &lt;br /&gt;    //Reactor的关闭Future   &lt;br /&gt;    private final Promise&amp;lt;?&amp;gt; terminationFuture = new DefaultPromise&amp;lt;Void&amp;gt;(GlobalEventExecutor.INSTANCE);   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public Future&amp;lt;?&amp;gt; shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {   &lt;br /&gt;   &lt;br /&gt;        ......省略参数校验.......   &lt;br /&gt;   &lt;br /&gt;        //此时Reactor的状态为ST_STARTED   &lt;br /&gt;        if (isShuttingDown()) {   &lt;br /&gt;            return terminationFuture();   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        boolean inEventLoop = inEventLoop();   &lt;br /&gt;        boolean wakeup;   &lt;br /&gt;        int oldState;   &lt;br /&gt;        for (;;) {   &lt;br /&gt;            if (isShuttingDown()) {   &lt;br /&gt;                return terminationFuture();   &lt;br /&gt;            }   &lt;br /&gt;            int newState;   &lt;br /&gt;            //需要唤醒Reactor去执行关闭流程   &lt;br /&gt;            wakeup = true;   &lt;br /&gt;            oldState = state;   &lt;br /&gt;            if (inEventLoop) {   &lt;br /&gt;                newState = ST_SHUTTING_DOWN;   &lt;br /&gt;            } else {   &lt;br /&gt;                switch (oldState) {   &lt;br /&gt;                    case ST_NOT_STARTED:   &lt;br /&gt;                    case ST_STARTED:   &lt;br /&gt;                        newState = ST_SHUTTING_DOWN;   &lt;br /&gt;                        break;   &lt;br /&gt;                    default:   &lt;br /&gt;                        //Reactor正在关闭或者已经关闭   &lt;br /&gt;                        newState = oldState;   &lt;br /&gt;                        wakeup = false;   &lt;br /&gt;                }   &lt;br /&gt;            }   &lt;br /&gt;            if (STATE_UPDATER.compareAndSet(this, oldState, newState)) {   &lt;br /&gt;                break;   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;        //优雅关闭静默期，在该时间内，用户还是可以向Reactor提交任务并且执行，只要有任务在Reactor中，就不能进行关闭   &lt;br /&gt;        //每隔100ms检测是否有任务提交进来，如果在静默期内没有新的任务提交，那么才会进行关闭 保证关闭行为的优雅   &lt;br /&gt;        gracefulShutdownQuietPeriod = unit.toNanos(quietPeriod);   &lt;br /&gt;        //优雅关闭的最大超时时间，优雅关闭行为不能超过该时间，如果超过的话 不管当前是否还有任务 都要进行关闭   &lt;br /&gt;        //保证关闭行为的可控   &lt;br /&gt;        gracefulShutdownTimeout = unit.toNanos(timeout);   &lt;br /&gt;   &lt;br /&gt;        //这里需要保证Reactor线程是在运行状态，如果已经停止，那么就不在进行后续关闭行为，直接返回terminationFuture   &lt;br /&gt;        if (ensureThreadStarted(oldState)) {   &lt;br /&gt;            return terminationFuture;   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        //将正在监听IO事件的Reactor从Selector上唤醒，表示要关闭了，开始执行关闭流程   &lt;br /&gt;        if (wakeup) {   &lt;br /&gt;            //确保Reactor线程在执行完任务之后 不会在selector上停留   &lt;br /&gt;            taskQueue.offer(WAKEUP_TASK);   &lt;br /&gt;            if (!addTaskWakesUp) {   &lt;br /&gt;                //如果此时Reactor正在Selector上阻塞，则可以确保Reactor被及时唤醒   &lt;br /&gt;                wakeup(inEventLoop);   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        return terminationFuture();   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public Future&amp;lt;?&amp;gt; terminationFuture() {   &lt;br /&gt;        return terminationFuture;   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;首先在开启关闭流程之前，需要调用 isShuttingDown() 判断一下当前 Reactor 是否已经开始关闭流程或者已经完成关闭。如果已经开始关闭了，这里会直接返回 Reactor 的 terminationFuture 。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public boolean isShuttingDown() {   &lt;br /&gt;        return state &amp;gt;= ST_SHUTTING_DOWN;   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;剩下的逻辑就是不停的在一个 for 循环中通过 CAS 不停的尝试将 Reactor 的当前 ST_STARTED 状态改为 ST_SHUTTING_DOWN 正在关闭状态。&lt;/p&gt; &lt;p&gt;如果通过 inEventLoop() 判断出当前执行线程是 Reactor 线程，那么表示当前 Reactor 的状态只会是 ST_STARTED 运行状态，那么就可以直接将 newState 设置为 ST_SHUTTING_DOWN 。因为只有 Reactor 处于 ST_STARTED 状态的时候才会运行到这里。否则在前边就直接返回 terminationFuture了。&lt;/p&gt; &lt;p&gt;如果当前执行线程为用户线程并不是 Reactor 线程的话，那么此时 Reactor 的状态可能是正在关闭状态或者已经关闭状态，用户线程在重复发起 Reactor 的关闭流程。所以这些异常场景的处理会在 switch(oldState){....} 语句中完成。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;            switch (oldState) {   &lt;br /&gt;                    case ST_NOT_STARTED:   &lt;br /&gt;                    case ST_STARTED:   &lt;br /&gt;                        newState = ST_SHUTTING_DOWN;   &lt;br /&gt;                        break;   &lt;br /&gt;                    default:   &lt;br /&gt;                        //Reactor正在关闭或者已经关闭   &lt;br /&gt;                        newState = oldState;   &lt;br /&gt;                        //当前Reactor已经处于关闭流程中，则无需在唤醒Reactor了   &lt;br /&gt;                        wakeup = false;   &lt;br /&gt;                }   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果当前 Reactor 还未发起关闭流程，比如状态为 ST_NOT_STARTED 或者 ST_STARTED ，那么直接可以放心的将 newState 设置为 ST_SHUTTING_DOWN 。&lt;/p&gt; &lt;p&gt;如果当前 Reactor 已经处于关闭流程中或者已经完成关闭，比如状态为 ST_SHUTTING_DOWN ，ST_SHUTDOWN 或者 ST_TERMINATED 。则没有必要在唤醒 Reactor 重复执行关闭流程了 wakeup = false。Reactor 的状态维持当前状态不变。&lt;/p&gt; &lt;p&gt;当 Reactor 的状态确定完毕后，则在 for 循环中不断的通过 CAS 修改 Reactor 的当前状态。此时 oldState = ST_STARTED ，newState = ST_SHUTTING_DOWN 。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;   &lt;br /&gt;          if (STATE_UPDATER.compareAndSet(this, oldState, newState)) {   &lt;br /&gt;                break;   &lt;br /&gt;            }   &lt;br /&gt;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;随后在 Reactor 中设置我们在《6.1 ReactorGroup 的优雅谢幕》小节开始处介绍的控制 Netty 优雅关闭的两个非常重要的核心参数：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;gracefulShutdownQuietPeriod&lt;/code&gt;：优雅关闭静默期，默认为 2s 。当 Reactor 中已经没有异步任务需要在执行时，该静默期开始触发，Netty 在这里会每隔    &lt;code&gt;100ms&lt;/code&gt;检测一下是否有任务提交进来，如果在静默期内没有新的任务提交，那么才会进行关闭，保证关闭行为的优雅。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;    &lt;code&gt;gracefulShutdownTimeout&lt;/code&gt;：优雅关闭超时时间，默认为 15s 。优雅关闭行为不能超过该时间，如果超过的话不管当前是否还有任务都要进行关闭，保证关闭行为的可控。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;流程走到这里，Reactor 就开始准备执行关闭流程了，那么在进行关闭操作之前，我们需要确保 Reactor 线程此时应该是运行状态，如果此时 Reactor 线程还未开始运行那么就需要让它运行起来执行关闭操作。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;   &lt;br /&gt;        //这里需要保证Reactor线程是在运行状态，如果已经停止，   &lt;br /&gt;        //那么就不在进行后续关闭行为，直接返回terminationFuture   &lt;br /&gt;        if (ensureThreadStarted(oldState)) {   &lt;br /&gt;            return terminationFuture;   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;pre&gt;  &lt;code&gt;   &lt;br /&gt;    private boolean ensureThreadStarted(int oldState) {   &lt;br /&gt;        if (oldState == ST_NOT_STARTED) {   &lt;br /&gt;            try {   &lt;br /&gt;                doStartThread();   &lt;br /&gt;            } catch (Throwable cause) {   &lt;br /&gt;                STATE_UPDATER.set(this, ST_TERMINATED);   &lt;br /&gt;                terminationFuture.tryFailure(cause);   &lt;br /&gt;   &lt;br /&gt;                if (!(cause instanceof Exception)) {   &lt;br /&gt;                    // Also rethrow as it may be an OOME for example   &lt;br /&gt;                    PlatformDependent.throwException(cause);   &lt;br /&gt;                }   &lt;br /&gt;                return true;   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;        return false;   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果此时 Reactor 线程刚刚执行完异步任务或者正在 Selector 上阻塞，那么我们需要确保 Reactor 线程被及时的唤醒，从而可以直接进入关闭流程。wakeup == true。&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;这里的 addTaskWakesUp 默认为 false 。表示并不是只有 addTask 方法才能唤醒 Reactor 线程 还有其他方法可以唤醒 Reactor 线程，比如 SingleThreadEventExecutor#execute 方法还有本小节介绍的 SingleThreadEventExecutor#shutdownGracefully 方法都会唤醒 Reactor 线程。&lt;/p&gt;&lt;/blockquote&gt; &lt;blockquote&gt;  &lt;p&gt;关于 addTaskWakesUp 字段的详细含义和作用，大家可以回顾下   &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247484087&amp;idx=1&amp;sn=0c065780e0f05c23c8e6465ede86cba0&amp;chksm=ce77c4f0f9004de63be369a664105708bc5975b52993f4a6df223caed34cc1ef6185a16acd75&amp;token=1670680185&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;《一文聊透 Netty 核心引擎 Reactor 的运转架构》&lt;/a&gt;一文中的《1.2.2 Reactor 开始轮询 IO 就绪事件》小节。&lt;/p&gt;&lt;/blockquote&gt; &lt;pre&gt;  &lt;code&gt;   &lt;br /&gt;     //将正在监听IO事件的Reactor从Selector上唤醒，表示要关闭了，开始执行关闭流程   &lt;br /&gt;        if (wakeup) {   &lt;br /&gt;            //确保Reactor线程在执行完任务之后 不会在selector上停留   &lt;br /&gt;            taskQueue.offer(WAKEUP_TASK);   &lt;br /&gt;            if (!addTaskWakesUp) {   &lt;br /&gt;                //如果此时Reactor正在Selector上阻塞，则可以确保Reactor被及时唤醒   &lt;br /&gt;                wakeup(inEventLoop);   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;通过    &lt;code&gt;taskQueue.offer(WAKEUP_TASK)&lt;/code&gt;向 Reactor 中添加 WAKEUP_TASK，可以确保 Reactor 在执行完异步任务之后不会在 Selector 上做停留，直接执行关闭操作。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;如果此时 Reactor 线程正在 Selector 上阻塞，那么直接调用 wakeup(inEventLoop) 唤醒 Reactor 线程，直接来到关闭流程。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;pre&gt;  &lt;code&gt;public final class NioEventLoop extends SingleThreadEventLoop {   &lt;br /&gt;    @Override   &lt;br /&gt;    protected void wakeup(boolean inEventLoop) {   &lt;br /&gt;        if (!inEventLoop &amp;amp;&amp;amp; nextWakeupNanos.getAndSet(AWAKE) != AWAKE) {   &lt;br /&gt;            selector.wakeup();   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h3&gt;6.3 Reactor 线程的优雅关闭&lt;/h3&gt; &lt;p&gt;我们先来通过一张 Reactor 优雅关闭整体流程图来从总体上俯撼一下关闭流程：&lt;/p&gt; &lt;img&gt;&lt;/img&gt;Reactor线程优雅关闭流程.png &lt;p&gt;通过  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247484087&amp;idx=1&amp;sn=0c065780e0f05c23c8e6465ede86cba0&amp;chksm=ce77c4f0f9004de63be369a664105708bc5975b52993f4a6df223caed34cc1ef6185a16acd75&amp;scene=21#wechat_redirect"&gt;《一文聊透Netty核心引擎Reactor的运转架构》&lt;/a&gt;一文的介绍，我们知道 Reacto r是在一个 for 循环中 996 不停地处理 IO 事件以及执行异步任务。如下面笔者提取的 Reactor 运行框架所示：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public final class NioEventLoop extends SingleThreadEventLoop {   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    protected void run() {   &lt;br /&gt;        for (;;) {   &lt;br /&gt;            try {   &lt;br /&gt;                  .......1.监听Channel上的IO事件.......   &lt;br /&gt;                  .......2.处理Channel上的IO事件.......   &lt;br /&gt;                  .......3.执行异步任务..........   &lt;br /&gt;            } finally {   &lt;br /&gt;                try {   &lt;br /&gt;                    if (isShuttingDown()) {   &lt;br /&gt;                        //关闭Reactor上注册的所有Channel,停止处理IO事件，触发unActive以及unRegister事件   &lt;br /&gt;                        closeAll();   &lt;br /&gt;                        //注销掉所有Channel停止处理IO事件之后，剩下的就需要执行Reactor中剩余的异步任务了   &lt;br /&gt;                        if (confirmShutdown()) {   &lt;br /&gt;                            return;   &lt;br /&gt;                        }   &lt;br /&gt;                    }   &lt;br /&gt;                } catch (Error e) {   &lt;br /&gt;                    throw (Error) e;   &lt;br /&gt;                } catch (Throwable t) {   &lt;br /&gt;                    handleLoopException(t);   &lt;br /&gt;                }   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在 Reactor 在每次 for 循环的末尾 finally{....} 语句块中都会通过 isShuttingDown() 方法去检查当前 Reactor 的状态是否是关闭状态，如果是关闭状态则开始正式进入 Reactor 的优雅关闭流程。&lt;/p&gt; &lt;p&gt;我们在本文前边《1.2 优雅关闭》小节中在讨论优雅关闭方案的时候提到，我们要着重从以下两个方面来实施优雅关闭：&lt;/p&gt; &lt;ol&gt;  &lt;li&gt;   &lt;p&gt;首先需要切走程序承担的现有流量。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;保证现有剩余的任务可以执行完毕，保证业务无损。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt; &lt;p&gt;Netty 这里实现的优雅关闭同样也遵从这两个要点。&lt;/p&gt; &lt;ol&gt;  &lt;li&gt;   &lt;p&gt;在优雅关闭流程开始之前首先会调用 closeAll() 方法，将 Reactor 上注册的所有 Channel 全部关闭掉，切掉现有流量。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;随后会调用 confirmShutdown() 方法，将剩余的异步任务执行完毕。在该方法中只要有异步任务需要执行，就不能关闭，保证业务无损。该方法返回值为 true 时表示可以进行关闭。返回 false 时表示不能马上关闭。&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt; &lt;h3&gt;6.3.1 切走流量&lt;/h3&gt; &lt;pre&gt;  &lt;code&gt;    private void closeAll() {   &lt;br /&gt;        //这里的目的是清理selector中的一些无效key   &lt;br /&gt;        selectAgain();   &lt;br /&gt;        //获取Selector上注册的所有Channel   &lt;br /&gt;        Set&amp;lt;SelectionKey&amp;gt; keys = selector.keys();   &lt;br /&gt;        Collection&amp;lt;AbstractNioChannel&amp;gt; channels = new ArrayList&amp;lt;AbstractNioChannel&amp;gt;(keys.size());   &lt;br /&gt;        for (SelectionKey k: keys) {   &lt;br /&gt;            //获取NioSocketChannel   &lt;br /&gt;            Object a = k.attachment();   &lt;br /&gt;            if (a instanceof AbstractNioChannel) {   &lt;br /&gt;                channels.add((AbstractNioChannel) a);   &lt;br /&gt;            } else {   &lt;br /&gt;                .........省略......   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        for (AbstractNioChannel ch: channels) {   &lt;br /&gt;            //关闭Reactor上注册的所有Channel，并在pipeline中触发unActive事件和unRegister事件   &lt;br /&gt;            ch.unsafe().close(ch.unsafe().voidPromise());   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;首先会通过 selectAgain() 最后一次在 Selector 上执行一次非阻塞轮询操作，目的是清除 Selector 上的一些无效 Key 。&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;关于无效 Key 的清除，详细细节大家可以回看下   &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247484087&amp;idx=1&amp;sn=0c065780e0f05c23c8e6465ede86cba0&amp;chksm=ce77c4f0f9004de63be369a664105708bc5975b52993f4a6df223caed34cc1ef6185a16acd75&amp;token=1670680185&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;《一文聊透Netty核心引擎Reactor的运转架构》&lt;/a&gt;一文中的《3.1.3 从Selector中移除失效的SelectionKey》小节。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;随后通过 selector.keys() 获取在 Selector 上注册的所有 SelectionKey 。进而获取到 Netty 中的 NioSocketChannel 。SelectionKey 与 NioSocketChannel 的对应关系如下图所示：&lt;/p&gt; &lt;img&gt;&lt;/img&gt;channel与SelectionKey对应关系.png &lt;p&gt;最后将注册在 Reactor 上的这些 NioSocketChannel 挨个进行关闭。&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;Channel 的关闭流程可以回看下笔者的这篇文章   &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247485060&amp;idx=1&amp;sn=736360af6eb3a4db496de2d6665ebd3c&amp;chksm=ce77c0c3f90049d5e44692c2cf837e8d85bb28758b243d505c43c48ce703da1edadfc19360b1&amp;token=1978355368&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;《且看 Netty 如何应对 TCP 连接的正常关闭，异常关闭，半关闭场景》&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt; &lt;h3&gt;6.3.2 保证业务无损&lt;/h3&gt; &lt;p&gt;该方法中的逻辑是保证 Reactor 进行优雅关闭的核心，Netty 这里为了保证业务无损，采取的是只要有异步任务 Task 或者 ShutdwonHooks 需要执行，就不能关闭，需要等待所有 tasks 或者 ShutdownHooks 执行完毕，才会考虑关闭的事情。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;    protected boolean confirmShutdown() {   &lt;br /&gt;        if (!isShuttingDown()) {   &lt;br /&gt;            return false;   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        if (!inEventLoop()) {   &lt;br /&gt;            throw new IllegalStateException(&amp;quot;must be invoked from an event loop&amp;quot;);   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        //取消掉所有的定时任务   &lt;br /&gt;        cancelScheduledTasks();   &lt;br /&gt;   &lt;br /&gt;        if (gracefulShutdownStartTime == 0) {   &lt;br /&gt;            //获取优雅关闭开始时间，相对时间   &lt;br /&gt;            gracefulShutdownStartTime = ScheduledFutureTask.nanoTime();   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        //这里判断只要有task任务需要执行就不能关闭   &lt;br /&gt;        if (runAllTasks() || runShutdownHooks()) {   &lt;br /&gt;            if (isShutdown()) {   &lt;br /&gt;                // Executor shut down - no new tasks anymore.   &lt;br /&gt;                return true;   &lt;br /&gt;            }   &lt;br /&gt;   &lt;br /&gt;            /**   &lt;br /&gt;             * gracefulShutdownQuietPeriod表示在这段时间内，用户还是可以继续提交异步任务的，Reactor在这段时间内   &lt;br /&gt;             * 是会保证这些任务被执行到的。   &lt;br /&gt;             *   &lt;br /&gt;             * gracefulShutdownQuietPeriod = 0 表示 没有这段静默时期，当前Reactor中的任务执行完毕后，无需等待静默期，执行关闭   &lt;br /&gt;             * */   &lt;br /&gt;            if (gracefulShutdownQuietPeriod == 0) {   &lt;br /&gt;                return true;   &lt;br /&gt;            }   &lt;br /&gt;            //避免Reactor在Selector上阻塞，因为此时已经不会再去处理IO事件了，专心处理关闭流程   &lt;br /&gt;            taskQueue.offer(WAKEUP_TASK);   &lt;br /&gt;            return false;   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        //此时Reactor中已经没有任务可执行了，是时候考虑关闭的事情了   &lt;br /&gt;        final long nanoTime = ScheduledFutureTask.nanoTime();   &lt;br /&gt;   &lt;br /&gt;        //当Reactor中所有的任务执行完毕后，判断是否超过gracefulShutdownTimeout   &lt;br /&gt;        //如果超过了 则直接关闭   &lt;br /&gt;        if (isShutdown() || nanoTime - gracefulShutdownStartTime &amp;gt; gracefulShutdownTimeout) {   &lt;br /&gt;            return true;   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        //即使现在没有任务也还是不能进行关闭，需要等待一个静默期，在静默期内如果没有新的任务提交，才会进行关闭   &lt;br /&gt;        //如果在静默期内还有任务继续提交，那么静默期将会重新开始计算，进入一轮新的静默期检测   &lt;br /&gt;        if (nanoTime - lastExecutionTime &amp;lt;= gracefulShutdownQuietPeriod) {   &lt;br /&gt;            taskQueue.offer(WAKEUP_TASK);   &lt;br /&gt;            try {   &lt;br /&gt;                //gracefulShutdownQuietPeriod内每隔100ms检测一下 是否有任务需要执行   &lt;br /&gt;                Thread.sleep(100);   &lt;br /&gt;            } catch (InterruptedException e) {   &lt;br /&gt;                // Ignore   &lt;br /&gt;            }   &lt;br /&gt;   &lt;br /&gt;            return false;   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        // 在整个gracefulShutdownQuietPeriod期间内没有任务需要执行或者静默期结束 则无需等待gracefulShutdownTimeout超时，直接关闭   &lt;br /&gt;        return true;   &lt;br /&gt;    }   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在关闭流程开始之前，Netty 首先会调用 cancelScheduledTasks() 方法将 Reactor 中剩余需要执行的定时任务全部取消掉。&lt;/p&gt; &lt;p&gt;记录优雅关闭开始时间 gracefulShutdownStartTime ，这是为了后续判断优雅关闭流程是否超时。&lt;/p&gt; &lt;p&gt;调用 runAllTasks() 方法将 Reactor 中 TaskQueue 里剩余的异步任务全部取出执行。&lt;/p&gt; &lt;img&gt;&lt;/img&gt;运行剩余tasks和hooks.png &lt;p&gt;调用 runShutdownHooks() 方法将用户注册在 Reactor 上的 ShutdownHook 取出执行。&lt;/p&gt; &lt;p&gt;我们可以在用户线程中通过如下方式向 Reactor 中注册 ShutdownHooks ：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;        NioEventLoop reactor = (NioEventLoop) ctx.channel().eventLoop();   &lt;br /&gt;        reactor.addShutdownHook(new Runnable() {   &lt;br /&gt;            @Override   &lt;br /&gt;            public void run() {   &lt;br /&gt;                .....关闭逻辑....   &lt;br /&gt;            }   &lt;br /&gt;        });   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;在 Reactor 进行关闭的时候，会取出用户注册的这些 ShutdownHooks 进行运行。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {   &lt;br /&gt;   &lt;br /&gt;   //可以向Reactor添加shutdownHook，当Reactor关闭的时候会被调用   &lt;br /&gt;   private final Set&amp;lt;Runnable&amp;gt; shutdownHooks = new LinkedHashSet&amp;lt;Runnable&amp;gt;();   &lt;br /&gt;   &lt;br /&gt;   private boolean runShutdownHooks() {   &lt;br /&gt;        boolean ran = false;   &lt;br /&gt;        while (!shutdownHooks.isEmpty()) {   &lt;br /&gt;            List&amp;lt;Runnable&amp;gt; copy = new ArrayList&amp;lt;Runnable&amp;gt;(shutdownHooks);   &lt;br /&gt;            shutdownHooks.clear();   &lt;br /&gt;            for (Runnable task: copy) {   &lt;br /&gt;                try {   &lt;br /&gt;                    //Reactor线程挨个顺序同步执行   &lt;br /&gt;                    task.run();   &lt;br /&gt;                } catch (Throwable t) {   &lt;br /&gt;                    logger.warn(&amp;quot;Shutdown hook raised an exception.&amp;quot;, t);   &lt;br /&gt;                } finally {   &lt;br /&gt;                    ran = true;   &lt;br /&gt;                }   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        if (ran) {   &lt;br /&gt;            lastExecutionTime = ScheduledFutureTask.nanoTime();   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        return ran;   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;blockquote&gt;  &lt;p&gt;需要注意的是这里的 ShutdownHooks 是 Netty 提供的一种机制并不是我们在《3. JVM 中的 ShutdownHook》小节中介绍的 JVM 中的 ShutdownHooks 。&lt;/p&gt;&lt;/blockquote&gt; &lt;blockquote&gt;  &lt;p&gt;JVM 中的 ShutdownHooks 是一个 Thread ，JVM 在关闭之前会   &lt;strong&gt;并发无序&lt;/strong&gt;地运行。而 Netty 中的 ShutdownHooks 是一个 Runnable ，Reactor 在关闭之前，会由 Reactor 线程   &lt;strong&gt;同步有序&lt;/strong&gt;地执行。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;  &lt;strong&gt;这里需要注意的是只要有 tasks 和 hooks 需要执行 Netty 就会一直执行下去直到这些任务全部执行完为止&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;当 Reactor 没有任何任务需要执行时，这时就会判断当前关闭流程所用时间是否超过了我们前边设定的优雅关闭最大超时时间 gracefulShutdownTimeout 。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;nanoTime - gracefulShutdownStartTime &amp;gt; gracefulShutdownTimeout   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果关闭流程因为前边这些任务的执行导致已经超时，那么就直接关闭 Reactor ，退出 Reactor 的工作循环。&lt;/p&gt; &lt;p&gt;如果没有超时，那么这时就会触发前边介绍的优雅关闭的静默期 gracefulShutdownQuietPeriod 。&lt;/p&gt; &lt;p&gt;在静默期中 Reactor 线程会每隔 100ms 检查一下是否有用户提交任务请求，如果有的话，就需要保证将用户提交的这些任务执行完毕。然后静默期将会重新开始计算，进入一轮新的静默期检测。&lt;/p&gt; &lt;p&gt;如果在整个静默期内，没有任何任务提交，则无需等待 gracefulShutdownTimeout 超时，直接关闭 Reactor ，退出 Reactor 的工作循环。&lt;/p&gt; &lt;p&gt;从以上过程我们可以看出 Netty 的优雅关闭至少需要等待一个静默期的时间。还有一点是 Netty 优雅关闭的时间可能会超出 gracefulShutdownTimeout ，因为 Netty 需要保证遗留剩余的任务被执行完毕。当所有任务执行完毕之后，才会去检测是否超时。&lt;/p&gt; &lt;h2&gt;6.4 Reactor 的最终关闭流程&lt;/h2&gt; &lt;p&gt;当在静默期内没有任何任务提交或者关闭流程超时时，上小节中介绍的 confirmShutdown() 就会返回 true 。随即 Reactor 线程就会退出工作循环。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public final class NioEventLoop extends SingleThreadEventLoop {   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    protected void run() {   &lt;br /&gt;        for (;;) {   &lt;br /&gt;            try {   &lt;br /&gt;                  .......1.监听Channel上的IO事件.......   &lt;br /&gt;                  .......2.处理Channel上的IO事件.......   &lt;br /&gt;                  .......3.执行异步任务..........   &lt;br /&gt;            } finally {   &lt;br /&gt;                try {   &lt;br /&gt;                    if (isShuttingDown()) {   &lt;br /&gt;                        //关闭Reactor上注册的所有Channel,停止处理IO事件，触发unActive以及unRegister事件   &lt;br /&gt;                        closeAll();   &lt;br /&gt;                        //注销掉所有Channel停止处理IO事件之后，剩下的就需要执行Reactor中剩余的异步任务了   &lt;br /&gt;                        if (confirmShutdown()) {   &lt;br /&gt;                            return;   &lt;br /&gt;                        }   &lt;br /&gt;                    }   &lt;br /&gt;                } catch (Error e) {   &lt;br /&gt;                    throw (Error) e;   &lt;br /&gt;                } catch (Throwable t) {   &lt;br /&gt;                    handleLoopException(t);   &lt;br /&gt;                }   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;我们在  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247484005&amp;idx=1&amp;sn=52f51269902a58f40d33208421109bc3&amp;chksm=ce77c422f9004d340e5b385ef6ba24dfba1f802076ace80ad6390e934173a10401e64e13eaeb&amp;token=1670680185&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;《详细图解 Netty Reactor 启动全流程》&lt;/a&gt;一文中的《1.3.3 Reactor 线程的启动》小节中的介绍中提到，Reactor 线程的启动是通过第一个异步任务被提交到 Reactor 中的时候被触发的。在向 Reactor 提交任务的方法  &lt;code&gt;SingleThreadEventExecutor#execute(java.lang.Runnable, boolean)&lt;/code&gt;中会触发下面 doStartThread() 方法的调用，在这里会调用前边提到的 Reactor 工作循环 run() 方法。&lt;/p&gt; &lt;p&gt;在 doStartThread() 方法的 finally{...} 语句块中会完成 Reactor 的最终关闭流程，也就是 Reactor 在退出 run 方法中的 for 循环之后的后续收尾流程。&lt;/p&gt; &lt;p&gt;最终 Reactor 的优雅关闭完整流程如下图所示：&lt;/p&gt; &lt;img&gt;&lt;/img&gt;Reactor优雅关闭全流程.png &lt;pre&gt;  &lt;code&gt;public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {   &lt;br /&gt;   &lt;br /&gt;    private void doStartThread() {   &lt;br /&gt;        assert thread == null;   &lt;br /&gt;        executor.execute(new Runnable() {   &lt;br /&gt;            @Override   &lt;br /&gt;            public void run() {   &lt;br /&gt;   &lt;br /&gt;                ..........省略.........   &lt;br /&gt;   &lt;br /&gt;                try {   &lt;br /&gt;                    //Reactor线程开始轮询处理IO事件，执行异步任务   &lt;br /&gt;                    SingleThreadEventExecutor.this.run();   &lt;br /&gt;                    //后面的逻辑为用户调用shutdownGracefully关闭Reactor退出循环 走到这里   &lt;br /&gt;                    success = true;   &lt;br /&gt;                } catch (Throwable t) {   &lt;br /&gt;                    logger.warn(&amp;quot;Unexpected exception from an event executor: &amp;quot;, t);   &lt;br /&gt;                } finally {   &lt;br /&gt;                    //走到这里表示在静默期内已经没有用户在向Reactor提交任务了，或者达到优雅关闭超时时间，开始对Reactor进行关闭   &lt;br /&gt;                    //如果当前Reactor不是关闭状态则将Reactor的状态设置为ST_SHUTTING_DOWN   &lt;br /&gt;                    for (;;) {   &lt;br /&gt;                        int oldState = state;   &lt;br /&gt;                        if (oldState &amp;gt;= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(   &lt;br /&gt;                                SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {   &lt;br /&gt;                            break;   &lt;br /&gt;                        }   &lt;br /&gt;                    }   &lt;br /&gt;   &lt;br /&gt;                    try {   &lt;br /&gt;                        for (;;) {   &lt;br /&gt;                            //此时Reactor线程虽然已经退出，而此时Reactor的状态为shuttingdown，但任务队列还在   &lt;br /&gt;                            //用户在此时依然可以提交任务，这里是确保用户在最后的这一刻提交的任务可以得到执行。   &lt;br /&gt;                            if (confirmShutdown()) {   &lt;br /&gt;                                break;   &lt;br /&gt;                            }   &lt;br /&gt;                        }   &lt;br /&gt;   &lt;br /&gt;                        for (;;) {   &lt;br /&gt;                            // 当Reactor的状态被更新为SHUTDOWN后，用户提交的任务将会被拒绝   &lt;br /&gt;                            int oldState = state;   &lt;br /&gt;                            if (oldState &amp;gt;= ST_SHUTDOWN || STATE_UPDATER.compareAndSet(   &lt;br /&gt;                                    SingleThreadEventExecutor.this, oldState, ST_SHUTDOWN)) {   &lt;br /&gt;                                break;   &lt;br /&gt;                            }   &lt;br /&gt;                        }   &lt;br /&gt;   &lt;br /&gt;                        // 这里Reactor的状态已经变为SHUTDOWN了，不会在接受用户提交的新任务了   &lt;br /&gt;                        // 但为了防止用户在状态变为SHUTDOWN之前，也就是Reactor在SHUTTINGDOWN的时候 提交了任务   &lt;br /&gt;                        // 所以此时Reactor中可能还会有任务，需要将剩余的任务执行完毕   &lt;br /&gt;                        confirmShutdown();   &lt;br /&gt;                    } finally {   &lt;br /&gt;                        try {   &lt;br /&gt;                            //SHUTDOWN状态下，在将全部的剩余任务执行完毕后，则将Selector关闭   &lt;br /&gt;                            cleanup();   &lt;br /&gt;                        } finally {   &lt;br /&gt;                            // 清理Reactor线程中的threadLocal缓存，并通知相应future。   &lt;br /&gt;                            FastThreadLocal.removeAll();   &lt;br /&gt;   &lt;br /&gt;                            //ST_TERMINATED状态为Reactor真正的终止状态   &lt;br /&gt;                            STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);   &lt;br /&gt;                               &lt;br /&gt;                            //使得awaitTermination方法返回   &lt;br /&gt;                            threadLock.countDown();   &lt;br /&gt;   &lt;br /&gt;                            //统计一下当前reactor任务队列中还有多少未执行的任务，打出日志   &lt;br /&gt;                            int numUserTasks = drainTasks();   &lt;br /&gt;                            if (numUserTasks &amp;gt; 0 &amp;amp;&amp;amp; logger.isWarnEnabled()) {   &lt;br /&gt;                                logger.warn(&amp;quot;An event executor terminated with &amp;quot; +   &lt;br /&gt;                                        &amp;quot;non-empty task queue (&amp;quot; + numUserTasks + &amp;apos;)&amp;apos;);   &lt;br /&gt;                            }   &lt;br /&gt;   &lt;br /&gt;                            /**   &lt;br /&gt;                             * 通知Reactor的terminationFuture成功，在创建Reactor的时候会向其terminationFuture添加Listener   &lt;br /&gt;                             * 在listener中增加terminatedChildren个数，当所有Reactor关闭后 ReactorGroup关闭成功   &lt;br /&gt;                             * */   &lt;br /&gt;                            terminationFuture.setSuccess(null);   &lt;br /&gt;                        }   &lt;br /&gt;                    }   &lt;br /&gt;                }   &lt;br /&gt;            }   &lt;br /&gt;        });   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;流程走到 doStartThread 方法中的 finally{...} 语句块中的时候，这个时候表示在优雅关闭的静默期内，已经没有任务继续向 Reactor 提交了。或者关闭耗时已经超过了设定的优雅关闭最大超时时间。&lt;/p&gt; &lt;p&gt;现在正式来到了 Reactor 的关闭流程。在流程开始之前需要确保当前 Reactor 的状态为 ST_SHUTTING_DOWN 正在关闭状态。&lt;/p&gt; &lt;p&gt;注意此刻用户线程依然可以向 Reactor 提交任务。当 Reactor 的状态变为 ST_SHUTDOWN 或者 ST_TERMINATED 时，用户向 Reactor 提交的任务就会被拒绝，但是此时 Reactor 的状态为 ST_SHUTTING_DOWN ，依然可以接受用户提交过来的任务。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {   &lt;br /&gt;  @Override   &lt;br /&gt;  public boolean isShutdown() {   &lt;br /&gt;        return state &amp;gt;= ST_SHUTDOWN;   &lt;br /&gt;  }   &lt;br /&gt;   &lt;br /&gt;  private void execute(Runnable task, boolean immediate) {   &lt;br /&gt;        boolean inEventLoop = inEventLoop();   &lt;br /&gt;        addTask(task);   &lt;br /&gt;        if (!inEventLoop) {   &lt;br /&gt;            startThread();   &lt;br /&gt;            //当Reactor的状态为ST_SHUTDOWN时，拒绝用户提交的异步任务，但是在优雅关闭ST_SHUTTING_DOWN状态时还是可以接受用户提交的任务的   &lt;br /&gt;            if (isShutdown()) {   &lt;br /&gt;                boolean reject = false;   &lt;br /&gt;                try {   &lt;br /&gt;                    if (removeTask(task)) {   &lt;br /&gt;                        reject = true;   &lt;br /&gt;                    }   &lt;br /&gt;                } catch (UnsupportedOperationException e) {   &lt;br /&gt;                }   &lt;br /&gt;                if (reject) {   &lt;br /&gt;                    reject();   &lt;br /&gt;                }   &lt;br /&gt;            }   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        .........省略........   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;所以 Reactor 从工作循环 run 方法中退出随后流程一路走到这里来的这段时间，用户仍然有可能向 Reactor 提交任务，为了确保关闭流程的优雅，这里会在 for 循环中不停的执行 confirmShutdown() 方法直到所有的任务全部执行完毕。&lt;/p&gt; &lt;p&gt;随后会将 Reactor 的状态改为 ST_SHUTDOWN 状态，此时用户就不能在向 Reactor 提交任务了。如果此时在提交任务就会收到 RejectedExecutionException 异常。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;大家这里可能会有疑问，Netty 在 Reactor 的状态变为 ST_SHUTDOWN 之后，又一次调用了 confirmShutdown() 方法，这是为什么呢？&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;其实这样做的目的是为了防止 Reactor 状态在变为 SHUTDOWN 之前，在这个极限的时间里，用户又向 Reactor 提交了任务，所以还需要最后一次调用 confirmShutdown() 将在这个极限时间内提交的任务执行完毕。&lt;/p&gt; &lt;p&gt;以上逻辑步骤就是真正优雅关闭的精髓所在，确保任务全部执行完毕，保证业务无损。&lt;/p&gt; &lt;p&gt;在我们优雅处理流程介绍完了之后，下面就是关闭 Reactor 的流程了：&lt;/p&gt; &lt;p&gt;Reactor 会在 SHUTDOWN 状态下，将 Selector 进行关闭。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;    @Override   &lt;br /&gt;    protected void cleanup() {   &lt;br /&gt;        try {   &lt;br /&gt;            selector.close();   &lt;br /&gt;        } catch (IOException e) {   &lt;br /&gt;            logger.warn(&amp;quot;Failed to close a selector.&amp;quot;, e);   &lt;br /&gt;        }   &lt;br /&gt;    }   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;清理 Reactor 线程中遗留的所有 ThreadLocal 缓存。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;FastThreadLocal.removeAll();   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;将 Reactor 的状态由 SHUTDOWN 改为 ST_TERMINATED 状态。  &lt;strong&gt;此时 Reactor 就算真正的关闭了&lt;/strong&gt;。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt; STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;用户线程可能会调用 Reactor 的 awaitTermination 方法阻塞等待 Reactor 的关闭，当 Reactor 关闭之后会调用 threadLock.countDown() 使得用户线程从 awaitTermination 方法返回。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public abstract class SingleThreadEventExecutor extends AbstractScheduledEventExecutor implements OrderedEventExecutor {   &lt;br /&gt;   &lt;br /&gt;    private final CountDownLatch threadLock = new CountDownLatch(1);   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {   &lt;br /&gt;           &lt;br /&gt;         ........省略.......   &lt;br /&gt;   &lt;br /&gt;        //等待Reactor关闭   &lt;br /&gt;        threadLock.await(timeout, unit);   &lt;br /&gt;        return isTerminated();   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;    @Override   &lt;br /&gt;    public boolean isTerminated() {   &lt;br /&gt;        return state == ST_TERMINATED;   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当这一切处理完毕之后，最后就会设置 Reactor 的 terminationFuture 为 success 。此时注册在 Reactor 的 terminationFuture 上的 listener 就会被回调。&lt;/p&gt; &lt;p&gt;这里还记得我们在  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg2MzU3Mjc3Ng==&amp;mid=2247483907&amp;idx=1&amp;sn=084c470a8fe6234c2c9461b5f713ff30&amp;chksm=ce77c444f9004d52e7c6244bee83479070effb0bc59236df071f4d62e91e25f01715fca53696&amp;token=1470157323&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;《Reactor 在 Netty 中的实现(创建篇)》&lt;/a&gt;一文中介绍的，在 ReactorGroup 中的所有 Reactor 被挨个全部创建成功之后，会向所有 Reactor 的 terminationFuture 注册一个 terminationListener 。&lt;/p&gt; &lt;p&gt;在 terminationListener 中检测当前 ReactorGroup 中的所有 Reactor 是否全部完成关闭，如果已经全部关闭，则设置 ReactorGroup 的 terminationFuture 为Success。此刻 ReactorGroup 关闭流程结束，Netty 正式优雅谢幕完毕~~&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;   &lt;br /&gt;public abstract class MultithreadEventExecutorGroup extends AbstractEventExecutorGroup {   &lt;br /&gt;   &lt;br /&gt;    //Reactor线程组中的Reactor集合   &lt;br /&gt;    private final EventExecutor[] children;   &lt;br /&gt;    //记录关闭的Reactor个数，当Reactor全部关闭后，才可以认为关闭成功   &lt;br /&gt;    private final AtomicInteger terminatedChildren = new AtomicInteger();   &lt;br /&gt;    //ReactorGroup关闭future   &lt;br /&gt;    private final Promise&amp;lt;?&amp;gt; terminationFuture = new DefaultPromise(GlobalEventExecutor.INSTANCE);   &lt;br /&gt;   &lt;br /&gt;    protected MultithreadEventExecutorGroup(int nThreads, Executor executor,   &lt;br /&gt;                                            EventExecutorChooserFactory chooserFactory, Object... args) {   &lt;br /&gt;         &lt;br /&gt;        ........挨个创建Reactor........   &lt;br /&gt;   &lt;br /&gt;        final FutureListener&amp;lt;Object&amp;gt; terminationListener = new FutureListener&amp;lt;Object&amp;gt;() {   &lt;br /&gt;            @Override   &lt;br /&gt;            public void operationComplete(Future&amp;lt;Object&amp;gt; future) throws Exception {   &lt;br /&gt;                if (terminatedChildren.incrementAndGet() == children.length) {   &lt;br /&gt;                    //当所有Reactor关闭后 才认为是关闭成功   &lt;br /&gt;                    terminationFuture.setSuccess(null);   &lt;br /&gt;                }   &lt;br /&gt;            }   &lt;br /&gt;        };   &lt;br /&gt;   &lt;br /&gt;        for (EventExecutor e: children) {   &lt;br /&gt;            e.terminationFuture().addListener(terminationListener);   &lt;br /&gt;        }   &lt;br /&gt;   &lt;br /&gt;        ........省略........   &lt;br /&gt;    }   &lt;br /&gt;   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;hr&gt;&lt;/hr&gt; &lt;p&gt;到现在为止，Netty 的整个优雅关闭流程，笔者就为大家详细介绍完了，下图为整个优雅关闭的完整流程图，大家可以对照下面这副总体流程图在回顾下我们前面介绍的源码逻辑。&lt;/p&gt; &lt;img&gt;&lt;/img&gt;Reactor优雅关闭总流程.png &lt;h3&gt;6.5 Reactor 的状态变更流转&lt;/h3&gt; &lt;p&gt;在本文的最后，笔者再来带着大家回顾下 Reactor 的状态变更流程。&lt;/p&gt; &lt;img&gt;&lt;/img&gt;Reactor的状态变更.png &lt;ul&gt;  &lt;li&gt;   &lt;p&gt;在 Reactor 被创建出来之后状态为 ST_NOT_STARTED。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;随着第一个异步任务的提交 Reactor 开始启动随后状态为 ST_STARTED 。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;当调用 shutdownGracefully 方法之后，Reactor 的状态变为 ST_SHUTTING_DOWN 。表示正在进行优雅关闭。此时用户仍可向 Reactor 提交异步任务。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;当 Reactor 中遗留的任务全部执行完毕之后，Reactor 的状态变为 ST_SHUTDOWN 。此时如果用户继续向 Reactor 提交异步任务，会被拒绝，并收到 RejectedExecutionException 异常。&lt;/p&gt;&lt;/li&gt;  &lt;li&gt;   &lt;p&gt;当 Selector 完成关闭，并清理掉 Reactor 线程中所有的 TheadLocal 缓存之后，Reactor 的状态变为 ST_TERMINATED 。&lt;/p&gt;&lt;/li&gt;&lt;/ul&gt; &lt;h2&gt;总结&lt;/h2&gt; &lt;p&gt;到这里关于优雅关闭的前世今生笔者就位大家全部交代完毕了，信息量比较大，需要好好消化一下，很佩服大家能够一口气看到这里。&lt;/p&gt; &lt;p&gt;本文我们从进程优雅启停方案开始聊起，以优雅关闭的实现方案为起点，先是介绍了优雅关闭的底层基石-内核的信号量机制，从内核又聊到了 JVM 的 ShutdownHook 原理以及执行过程，最后通过三个知名的开源框架为案例，分别从 Spring 的优雅关闭机制聊到了 Dubbo 的优雅关闭，最后通过 Dubbo 的优雅关闭引出了 Netty 优雅关闭的详细实现方案，前后呼应。&lt;/p&gt; &lt;p&gt;好了，本文的内容就到这里了，大家辛苦了，相信大家认真看完之后一定会收获很大，我们下篇文章见~~~&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>dev</category>
      <guid isPermaLink="true">https://itindex.net/detail/62377-java-%E6%8A%80%E6%9C%AF-%E4%B8%AD%E9%97%B4%E4%BB%B6</guid>
      <pubDate>Mon, 22 Aug 2022 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>聊聊接口设计的36个小技巧</title>
      <link>https://itindex.net/detail/62273-%E6%8E%A5%E5%8F%A3%E8%AE%BE%E8%AE%A1-%E6%8A%80%E5%B7%A7</link>
      <description>&lt;h2&gt;l前言&lt;/h2&gt; &lt;p&gt;大家好，我是苏三。作为后端开发，不管是什么语言，  &lt;code&gt;Java&lt;/code&gt;、  &lt;code&gt;Go&lt;/code&gt;还是  &lt;code&gt;C++&lt;/code&gt;，其背后的后端思想都是类似的。后面打算出一个后端思想的技术专栏，主要包括后端的一些设计、或者后端规范相关的，希望对大家日常工作有帮助哈。&lt;/p&gt; &lt;p&gt;我们做后端开发工程师，主要工作就是：  &lt;strong&gt;如何把一个接口设计好&lt;/strong&gt;。所以，今天就给大家介绍，设计好接口的36个锦囊。本文就是后端思想专栏的第一篇哈。&lt;/p&gt; &lt;p&gt;  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;h2&gt;1. 接口参数校验  &lt;br /&gt;&lt;/h2&gt; &lt;p&gt;入参出参校验是每个程序员必备的基本素养。你设计的接口，必须先校验参数。比如入参是否允许为空，入参长度是否符合你的预期长度。这个要养成习惯哈，日常开发中，很多低级bug都是不校验参数导致的。&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;比如你的数据库表字段设置为   &lt;code&gt;varchar(16)&lt;/code&gt;,对方传了一个32位的字符串过来，如果你不校验参数，   &lt;strong&gt;插入数据库直接异常了&lt;/strong&gt;。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;出参也是，比如你定义的接口报文，参数是不为空的，但是你的接口返回参数，没有做校验，因为程序某些原因，直返回别人一个  &lt;code&gt;null&lt;/code&gt;值。。。&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;h2&gt;2. 修改老接口时，注意接口的兼容性&lt;/h2&gt; &lt;p&gt;很多bug都是因为修改了对外旧接口，但是却  &lt;strong&gt;不做兼容&lt;/strong&gt;导致的。关键这个问题多数是比较严重的，可能直接导致系统发版失败的。新手程序员很容易犯这个错误哦~&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;p&gt;所以，如果你的需求是在原来接口上修改，尤其这个接口是对外提供服务的话，一定要考虑接口兼容。举个例子吧，比如dubbo接口，原本是只接收A，B参数，现在你加了一个参数C，就可以考虑这样处理：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;//老接口   &lt;br /&gt;void oldService(A,B){   &lt;br /&gt;  //兼容新接口，传个null代替C   &lt;br /&gt;  newService(A,B,null);   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;//新接口，暂时不能删掉老接口，需要做兼容。   &lt;br /&gt;void newService(A,B,C){   &lt;br /&gt;  ...   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;3.  设计接口时，充分考虑接口的可扩展性&lt;/h2&gt; &lt;p&gt;要根据实际业务场景设计接口，充分考虑接口的可扩展性。&lt;/p&gt; &lt;p&gt;比如你接到一个需求：是用户添加或者修改员工时，需要刷脸。那你是反手提供一个员工管理的提交刷脸信息接口？还是先思考：提交刷脸是不是通用流程呢？比如转账或者一键贴现需要接入刷脸的话，你是否需要重新实现一个接口呢？还是当前按业务类型划分模块，复用这个接口就好，保留接口的可扩展性。&lt;/p&gt; &lt;p&gt;如果按模块划分的话，未来如果其他场景比如一键贴现接入刷脸的话，不用再搞一套新的接口，只需要新增枚举，然后复用刷脸通过流程接口，实现一键贴现刷脸的差异化即可。&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;h2&gt;4.接口考虑是否需要防重处理&lt;/h2&gt; &lt;p&gt;如果前端重复请求，你的逻辑如何处理？是不是考虑接口去重处理。&lt;/p&gt; &lt;p&gt;当然，如果是查询类的请求，其实不用防重。如果是更新修改类的话，尤其金融转账类的，就要过滤重复请求了。简单点，你可以使用Redis防重复请求，同样的请求方，一定时间间隔内的相同请求，考虑是否过滤。当然，转账类接口，并发不高的话，  &lt;strong&gt;推荐使用数据库防重表&lt;/strong&gt;，以  &lt;strong&gt;唯一流水号作为主键或者唯一索引&lt;/strong&gt;。&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;h2&gt;5. 重点接口，考虑线程池隔离。&lt;/h2&gt; &lt;p&gt;一些登陆、转账交易、下单等重要接口，考虑线程池隔离哈。如果你所有业务都共用一个线程池，有些业务出bug导致线程池阻塞打满的话，那就杯具了，  &lt;strong&gt;所有业务都影响了&lt;/strong&gt;。因此进行线程池隔离，重要业务分配多一点的核心线程，就更好保护重要业务。&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;h2&gt;6. 调用第三方接口要考虑异常和超时处理&lt;/h2&gt; &lt;p&gt;如果你调用第三方接口，或者分布式远程服务的的话，需要考虑：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;异常处理&lt;/li&gt;&lt;/ul&gt; &lt;blockquote&gt;  &lt;p&gt;比如，你调别人的接口，如果异常了，怎么处理，是重试还是当做失败还是告警处理。&lt;/p&gt;&lt;/blockquote&gt; &lt;ul&gt;  &lt;li&gt;接口超时&lt;/li&gt;&lt;/ul&gt; &lt;blockquote&gt;  &lt;p&gt;没法预估对方接口一般多久返回，一般设置个超时断开时间，以保护你的接口。   &lt;strong&gt;之前见过一个生产问题&lt;/strong&gt;，就是http调用不设置超时时间，最后响应方进程假死，请求一直占着线程不释放，拖垮线程池。&lt;/p&gt;&lt;/blockquote&gt; &lt;ul&gt;  &lt;li&gt;重试次数&lt;/li&gt;&lt;/ul&gt; &lt;blockquote&gt;  &lt;p&gt;你的接口调失败，需不需要重试？重试几次？需要站在业务上角度思考这个问题&lt;/p&gt;&lt;/blockquote&gt; &lt;img&gt;&lt;/img&gt; &lt;h2&gt;7. 接口实现考虑熔断和降级&lt;/h2&gt; &lt;p&gt;当前互联网系统一般都是分布式部署的。而分布式系统中经常会出现某个基础服务不可用，最终导致整个系统不可用的情况, 这种现象被称为  &lt;strong&gt;服务雪崩效应&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;比如分布式调用链路  &lt;code&gt;A-&amp;gt;B-&amp;gt;C....&lt;/code&gt;，下图所示：&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;blockquote&gt;  &lt;p&gt;如果服务C出现问题，比如是   &lt;strong&gt;因为慢SQL导致调用缓慢&lt;/strong&gt;，那将导致B也会延迟，从而A也会延迟。堵住的A请求会消耗占用系统的线程、IO等资源。当请求A的服务越来越多，占用计算机的资源也越来越多，最终会导致系统瓶颈出现，造成其他的请求同样不可用，最后导致业务系统崩溃。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;为了应对服务雪崩, 常见的做法是  &lt;strong&gt;熔断和降级&lt;/strong&gt;。最简单是加开关控制，当下游系统出问题时，开关降级，不再调用下游系统。还可以选用开源组件  &lt;code&gt;Hystrix&lt;/code&gt;。&lt;/p&gt; &lt;h2&gt;8. 日志打印好，接口的关键代码，要有日志保驾护航。&lt;/h2&gt; &lt;p&gt;关键业务代码无论身处何地，都应该有足够的日志保驾护航。比如：你实现转账业务，转个几百万，然后转失败了，接着客户投诉，然后你还没有打印到日志，想想那种水深火热的困境下，你却毫无办法。。。&lt;/p&gt; &lt;p&gt;那么，你的转账业务都需要哪些日志信息呢？至少，方法调用前，入参需要打印需要吧，接口调用后，需要捕获一下异常吧，同时打印异常相关日志吧，如下：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public void transfer(TransferDTO transferDTO){   &lt;br /&gt;    log.info(&amp;quot;invoke tranfer begin&amp;quot;);   &lt;br /&gt;    //打印入参   &lt;br /&gt;    log.info(&amp;quot;invoke tranfer,paramters:{}&amp;quot;,transferDTO);   &lt;br /&gt;    try {   &lt;br /&gt;      res=  transferService.transfer(transferDTO);   &lt;br /&gt;    }catch(Exception e){   &lt;br /&gt;     log.error(&amp;quot;transfer fail,account：{}&amp;quot;,   &lt;br /&gt;     transferDTO.getAccount（）)   &lt;br /&gt;     log.error(&amp;quot;transfer fail,exception:{}&amp;quot;,e);   &lt;br /&gt;    }   &lt;br /&gt;    log.info(&amp;quot;invoke tranfer end&amp;quot;);   &lt;br /&gt;    }   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;之前写过一篇打印日志的15个建议，大家可以看看哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247494838&amp;idx=1&amp;sn=cdb15fd346bddf3f8c1c99f0efbd67d8&amp;chksm=cf22339ff855ba891616c79d4f4855e228e34a9fb45088d7acbe421ad511b8d090a90f5b019f&amp;token=162724582&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;工作总结！日志打印的15个建议&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;9. 接口的功能定义要具备单一性&lt;/h2&gt; &lt;p&gt;单一性是指接口做的事情比较单一、专一。比如一个登陆接口，它做的事情就只是校验账户名密码，然后返回登陆成功以及  &lt;code&gt;userId&lt;/code&gt;即可。  &lt;strong&gt;但是如果你为了减少接口交互，把一些注册、一些配置查询等全放到登陆接口，就不太妥。&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;其实这也是微服务一些思想，接口的功能单一、明确。比如订单服务、积分、商品信息相关的接口都是划分开的。将来拆分微服务的话，是不是就比较简便啦。&lt;/p&gt; &lt;h2&gt;10.接口有些场景，使用异步更合理&lt;/h2&gt; &lt;p&gt;举个简单的例子，比如你实现一个用户注册的接口。用户注册成功时，发个邮件或者短信去通知用户。这个邮件或者发短信，就更适合异步处理。因为总不能一个通知类的失败，导致注册失败吧。&lt;/p&gt; &lt;p&gt;至于做异步的方式，简单的就是  &lt;strong&gt;用线程池&lt;/strong&gt;。还可以使用消息队列，就是用户注册成功后，生产者产生一个注册成功的消息，消费者拉到注册成功的消息，就发送通知。&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;p&gt;不是所有的接口都适合设计为同步接口。比如你要做一个转账的功能，如果你是单笔的转账，你是可以把接口设计同步。用户发起转账时，客户端在静静等待转账结果就好。如果你是批量转账，一个批次一千笔，甚至一万笔的，你则可以把接口设计为异步。就是用户发起批量转账时，持久化成功就先返回受理成功。然后用户隔十分钟或者十五分钟等再来查转账结果就好。又或者，批量转账成功后，再回调上游系统。&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;h2&gt;11. 优化接口耗时，远程串行考虑改并行调用&lt;/h2&gt; &lt;p&gt;假设我们设计一个APP首页的接口，它需要查用户信息、需要查banner信息、需要查弹窗信息等等。那你是一个一个接口串行调，还是并行调用呢？&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;p&gt;如果是串行一个一个查，比如查用户信息200ms，查banner信息100ms、查弹窗信息50ms，那一共就耗时  &lt;code&gt;350ms&lt;/code&gt;了，如果还查其他信息，那耗时就更大了。这种场景是可以改为并行调用的。也就是说查用户信息、查banner信息、查弹窗信息，可以同时发起。&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;p&gt;在Java中有个异步编程利器：  &lt;code&gt;CompletableFuture&lt;/code&gt;，就可以很好实现这个功能。有兴趣的小伙伴可以看我之前这个文章哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247490456&amp;idx=1&amp;sn=95836324db57673a4d7aea4fb233c0d2&amp;chksm=cf21c4b1f8564da72dc7b39279362bcf965b1374540f3b339413d138599f7de59a5f977e3b0e&amp;token=1260947715&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;CompletableFuture详解&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;12. 接口合并或者说考虑批量处理思想&lt;/h2&gt; &lt;p&gt;数据库操作或或者是远程调用时，能批量操作就不要for循环调用。  &lt;img&gt;&lt;/img&gt;&lt;/p&gt; &lt;p&gt;一个简单例子，我们平时一个列表明细数据插入数据库时，不要在for循环一条一条插入，建议一个批次几百条，进行批量插入。同理远程调用也类似想法，比如你查询营销标签是否命中，可以一个标签一个标签去查，也可以批量标签去查，那批量进行，效率就更高嘛。&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;//反例   &lt;br /&gt;for(int i=0;i&amp;lt;n;i++){   &lt;br /&gt;  remoteSingleQuery(param)   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;//正例   &lt;br /&gt;remoteBatchQuery(param);   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;小伙伴们是否了解过  &lt;code&gt;kafka&lt;/code&gt;为什么这么快呢？其实其中一点原因，就是kafka  &lt;strong&gt;使用批量消息&lt;/strong&gt;提升服务端处理能力。&lt;/p&gt; &lt;h2&gt;13. 接口实现过程中，恰当使用缓存&lt;/h2&gt; &lt;p&gt;哪些场景适合使用缓存？  &lt;strong&gt;读多写少且数据时效要求越低的场景&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;缓存用得好，可以承载更多的请求，提升查询效率，减少数据库的压力。&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;比如一些平时变动很小或者说几乎不会变的商品信息，可以放到缓存，请求过来时，先查询缓存，如果没有再查数据库，并且把数据库的数据更新到缓存。但是，使用缓存增加了需要考虑这些点：缓存和数据库一致性如何保证、集群、缓存击穿、缓存雪崩、缓存穿透等问题。&lt;/p&gt;&lt;/blockquote&gt; &lt;ul&gt;  &lt;li&gt;保证数据库和缓存一致性：   &lt;strong&gt;缓存延时双删、删除缓存重试机制、读取biglog异步删除缓存&lt;/strong&gt;&lt;/li&gt;  &lt;li&gt;缓存击穿：设置数据永不过期&lt;/li&gt;  &lt;li&gt;缓存雪崩：Redis集群高可用、均匀设置过期时间&lt;/li&gt;  &lt;li&gt;缓存穿透：接口层校验、查询为空设置个默认空值标记、布隆过滤器。&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;一般用  &lt;code&gt;Redis&lt;/code&gt;分布式缓存，当然有些时候也可以考虑使用本地缓存，如  &lt;code&gt;Guava Cache、Caffeine&lt;/code&gt;等。使用本地缓存有些缺点，就是无法进行大数据存储，并且应用进程的重启，缓存会失效。&lt;/p&gt; &lt;h2&gt;14. 接口考虑热点数据隔离性&lt;/h2&gt; &lt;p&gt;瞬时间的高并发，可能会打垮你的系统。可以做一些热点数据的隔离。比如  &lt;strong&gt;业务隔离、系统隔离、用户隔离、数据隔离&lt;/strong&gt;等。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;业务隔离性，比如12306的分时段售票，将热点数据分散处理，降低系统负载压力。&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;h2&gt;15. 可变参数配置化，比如红包皮肤切换等&lt;/h2&gt; &lt;p&gt;假如产品经理提了个红包需求，圣诞节的时候，红包皮肤为圣诞节相关的，春节的时候，为春节红包皮肤等。&lt;/p&gt; &lt;p&gt;如果在代码写死控制，可有类似以下代码：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;if(duringChristmas){   &lt;br /&gt;   img = redPacketChristmasSkin;   &lt;br /&gt;}else if(duringSpringFestival){   &lt;br /&gt;   img =  redSpringFestivalSkin;   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;如果到了元宵节的时候，运营小姐姐突然又有想法，红包皮肤换成灯笼相关的，这时候，是不是要去修改代码了，重新发布了？&lt;/p&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&gt;16.接口考虑幂等性&lt;/h2&gt; &lt;p&gt;接口是需要考虑幂等性的，尤其抢红包、转账这些重要接口。最直观的业务场景，就是  &lt;strong&gt;用户连着点击两次&lt;/strong&gt;，你的接口有没有  &lt;strong&gt;hold住&lt;/strong&gt;。或者消息队列出现重复消费的情况，你的业务逻辑怎么控制？&lt;/p&gt; &lt;p&gt;回忆下，  &lt;strong&gt;什么是幂等？&lt;/strong&gt;&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;计算机科学中，幂等表示一次和多次请求某一个资源应该具有同样的副作用，或者说，多次请求所产生的影响与一次请求执行的影响效果相同。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;大家别搞混哈，  &lt;strong&gt;防重和幂等设计其实是有区别的&lt;/strong&gt;。防重主要为了避免产生重复数据，把重复请求拦截下来即可。而幂等设计除了拦截已经处理的请求，还要求每次相同的请求都返回一样的效果。不过呢，很多时候，它们的处理流程、方案是类似的哈。&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;p&gt;接口幂等实现方案主要有8种：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;select+insert+主键/唯一索引冲突&lt;/li&gt;  &lt;li&gt;直接insert + 主键/唯一索引冲突&lt;/li&gt;  &lt;li&gt;状态机幂等&lt;/li&gt;  &lt;li&gt;抽取防重表&lt;/li&gt;  &lt;li&gt;token令牌&lt;/li&gt;  &lt;li&gt;悲观锁&lt;/li&gt;  &lt;li&gt;乐观锁&lt;/li&gt;  &lt;li&gt;分布式锁&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;大家可以看我这篇文章哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247497427&amp;idx=1&amp;sn=2ed160c9917ad989eee1ac60d6122855&amp;chksm=cf2229faf855a0ecf5eb34c7335acdf6420426490ee99fc2b602d54ff4ffcecfdab24eeab0a3&amp;token=1260947715&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;聊聊幂等设计&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;17. 读写分离，优先考虑读从库，注意主从延迟问题&lt;/h2&gt; &lt;p&gt;我们的数据库都是集群部署的，有主库也有从库，当前一般都是读写分离的。比如你写入数据，肯定是写入主库，但是对于读取实时性要求不高的数据，则优先考虑读从库，因为可以分担主库的压力。&lt;/p&gt; &lt;p&gt;如果读取从库的话，需要考虑主从延迟的问题。&lt;/p&gt; &lt;h2&gt;18.接口注意返回的数据量，如果数据量大需要分页&lt;/h2&gt; &lt;p&gt;一个接口返回报文，不应该包含过多的数据量。过多的数据量不仅处理复杂，并且数据量传输的压力也非常大。因此数量实在是比较大，可以分页返回，如果是功能不相关的报文，那应该考虑接口拆分。&lt;/p&gt; &lt;h2&gt;19. 好的接口实现，离不开SQL优化&lt;/h2&gt; &lt;p&gt;我们做后端的，写好一个接口，离不开SQL优化。&lt;/p&gt; &lt;p&gt;SQL优化从这几个维度思考：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;explain 分析SQL查询计划（重点关注type、extra、filtered字段）&lt;/li&gt;  &lt;li&gt;show profile分析，了解SQL执行的线程的状态以及消耗的时间&lt;/li&gt;  &lt;li&gt;索引优化 （覆盖索引、最左前缀原则、隐式转换、order by以及group by的优化、join优化）&lt;/li&gt;  &lt;li&gt;大分页问题优化（延迟关联、记录上一页最大ID）&lt;/li&gt;  &lt;li&gt;数据量太大（   &lt;strong&gt;分库分表&lt;/strong&gt;、同步到es，用es查询）&lt;/li&gt;&lt;/ul&gt; &lt;h2&gt;20.代码锁的粒度控制好&lt;/h2&gt; &lt;p&gt;什么是加锁粒度呢？&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;其实就是就是你要锁住的范围是多大。比如你在家上卫生间，你只要锁住卫生间就可以了吧，不需要将整个家都锁起来不让家人进门吧，卫生间就是你的加锁粒度。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;我们写代码时，如果不涉及到共享资源，就没有必要锁住的。这就好像你上卫生间，不用把整个家都锁住，锁住卫生间门就可以了。&lt;/p&gt; &lt;p&gt;比如，在业务代码中，有一个ArrayList因为涉及到多线程操作，所以需要加锁操作，假设刚好又有一段比较耗时的操作（代码中的  &lt;code&gt;slowNotShare&lt;/code&gt;方法）不涉及线程安全问题，你会如何加锁呢？&lt;/p&gt; &lt;p&gt;反例：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;//不涉及共享资源的慢方法   &lt;br /&gt;private void slowNotShare() {   &lt;br /&gt;    try {   &lt;br /&gt;        TimeUnit.MILLISECONDS.sleep(100);   &lt;br /&gt;    } catch (InterruptedException e) {   &lt;br /&gt;    }   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;//错误的加锁方法   &lt;br /&gt;public int wrong() {   &lt;br /&gt;    long beginTime = System.currentTimeMillis();   &lt;br /&gt;    IntStream.rangeClosed(1, 10000).parallel().forEach(i -&amp;gt; {   &lt;br /&gt;        //加锁粒度太粗了，slowNotShare其实不涉及共享资源   &lt;br /&gt;        synchronized (this) {   &lt;br /&gt;            slowNotShare();   &lt;br /&gt;            data.add(i);   &lt;br /&gt;        }   &lt;br /&gt;    });   &lt;br /&gt;    log.info(&amp;quot;cosume time:{}&amp;quot;, System.currentTimeMillis() - beginTime);   &lt;br /&gt;    return data.size();   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;正例：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public int right() {   &lt;br /&gt;    long beginTime = System.currentTimeMillis();   &lt;br /&gt;    IntStream.rangeClosed(1, 10000).parallel().forEach(i -&amp;gt; {   &lt;br /&gt;        slowNotShare();//可以不加锁   &lt;br /&gt;        //只对List这部分加锁   &lt;br /&gt;        synchronized (data) {   &lt;br /&gt;            data.add(i);   &lt;br /&gt;        }   &lt;br /&gt;    });   &lt;br /&gt;    log.info(&amp;quot;cosume time:{}&amp;quot;, System.currentTimeMillis() - beginTime);   &lt;br /&gt;    return data.size();   &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;h2&gt;21.接口状态和错误需要统一明确&lt;/h2&gt; &lt;p&gt;提供必要的接口调用状态信息。比如你的一个转账接口调用是成功、失败、处理中还是受理成功等，需要明确告诉客户端。如果接口失败，那么具体失败的原因是什么。这些必要的信息都必须要告诉给客户端，因此需要定义明确的错误码和对应的描述。同时，尽量对报错信息封装一下，不要把后端的异常信息完全抛出到客户端。&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;h2&gt;22.接口要考虑异常处理&lt;/h2&gt; &lt;p&gt;实现一个好的接口，离不开优雅的异常处理。对于异常处理，提十个小建议吧：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;尽量不要使用   &lt;code&gt;e.printStackTrace()&lt;/code&gt;,而是使用   &lt;code&gt;log&lt;/code&gt;打印。因为   &lt;code&gt;e.printStackTrace()&lt;/code&gt;语句可能会导致内存占满。&lt;/li&gt;  &lt;li&gt;   &lt;code&gt;catch&lt;/code&gt;住异常时，建议打印出具体的   &lt;code&gt;exception&lt;/code&gt;，利于更好定位问题&lt;/li&gt;  &lt;li&gt;不要用一个   &lt;code&gt;Exception&lt;/code&gt;捕捉所有可能的异常&lt;/li&gt;  &lt;li&gt;记得使用   &lt;code&gt;finally&lt;/code&gt;关闭流资源或者直接使用   &lt;code&gt;try-with-resource&lt;/code&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;code&gt;Throwable cause&lt;/code&gt;&lt;/li&gt;  &lt;li&gt;运行时异常   &lt;code&gt;RuntimeException&lt;/code&gt;，不应该通过   &lt;code&gt;catch&lt;/code&gt;的方式来处理，而是先预检查，比如：   &lt;code&gt;NullPointerException&lt;/code&gt;处理&lt;/li&gt;  &lt;li&gt;注意异常匹配的顺序，优先捕获具体的异常&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;小伙伴们有兴趣可以看下我之前写的这篇文章哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247488009&amp;idx=1&amp;sn=7c27849c67476143660e3ea0dcdfae3d&amp;scene=21#wechat_redirect"&gt;Java 异常处理的十个建议&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;23. 优化程序逻辑&lt;/h2&gt; &lt;p&gt;优化程序逻辑这块还是挺重要的，也就是说，你实现的业务代码，  &lt;strong&gt;如果是比较复杂的话，建议把注释写清楚&lt;/strong&gt;。还有，代码逻辑尽量清晰，代码尽量高效。&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;比如，你要使用用户信息的属性，你根据session已经获取到   &lt;code&gt;userId&lt;/code&gt;了，然后就把用户信息从数据库查询出来，使用完后，后面可能又要用到用户信息的属性，有些小伙伴没想太多，反手就把   &lt;code&gt;userId&lt;/code&gt;再传进去，再查一次数据库。。。我在项目中，见过这种代码。。。直接把用户对象传下来不好嘛。。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;反例伪代码：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public Response test(Session session){   &lt;br /&gt;    UserInfo user = UserDao.queryByUserId(session.getUserId());   &lt;br /&gt;       &lt;br /&gt;    if(user==null){   &lt;br /&gt;       reutrn new Response();   &lt;br /&gt;    }   &lt;br /&gt;       &lt;br /&gt;    return do(session.getUserId());   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;public Response do(String UserId){   &lt;br /&gt;  //多查了一次数据库   &lt;br /&gt;  UserInfo user = UserDao.queryByUserId(session.getUserId());   &lt;br /&gt;  ......   &lt;br /&gt;  return new Response();    &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;正例：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;public Response test(Session session){   &lt;br /&gt;    UserInfo user = UserDao.queryByUserId(session.getUserId());   &lt;br /&gt;       &lt;br /&gt;    if(user==null){   &lt;br /&gt;       reutrn new Response();   &lt;br /&gt;    }   &lt;br /&gt;       &lt;br /&gt;    return do(session.getUserId());   &lt;br /&gt;}   &lt;br /&gt;   &lt;br /&gt;//直接传UserInfo对象过来即可，不用再多查一次数据库   &lt;br /&gt;public Response do(UserInfo user){   &lt;br /&gt;  ......   &lt;br /&gt;  return new Response();    &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;当然，这只是一些很小的一个例子，还有很多类似的例子，需要大家开发过程中，多点思考的哈。&lt;/p&gt; &lt;h2&gt;24. 接口实现过程中，注意大文件、大事务、大对象&lt;/h2&gt; &lt;ul&gt;  &lt;li&gt;读取大文件时，不要   &lt;code&gt;Files.readAllBytes&lt;/code&gt;直接读取到内存，这样会OOM的，建议使用   &lt;code&gt;BufferedReader&lt;/code&gt;一行一行来。&lt;/li&gt;  &lt;li&gt;大事务可能导致死锁、回滚时间长、主从延迟等问题，开发中尽量避免大事务。&lt;/li&gt;  &lt;li&gt;注意一些大对象的使用，因为大对象是直接进入老年代的，可能会触发fullGC&lt;/li&gt;&lt;/ul&gt; &lt;h2&gt;25. 你的接口，需要考虑限流&lt;/h2&gt; &lt;p&gt;如果你的系统每秒扛住的请求是1000，如果一秒钟来了十万请求呢？换个角度就是说，高并发的时候，流量洪峰来了，超过系统的承载能力，怎么办呢？&lt;/p&gt; &lt;p&gt;如果不采取措施，所有的请求打过来，系统CPU、内存、Load负载飚的很高，最后请求处理不过来，所有的请求无法正常响应。&lt;/p&gt; &lt;p&gt;针对这种场景，我们可以采用限流方案。就是为了保护系统，多余的请求，直接丢弃。&lt;/p&gt; &lt;p&gt;限流定义：&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;在计算机网络中，限流就是控制网络接口发送或接收请求的速率，它可防止DoS攻击和限制Web爬虫。限流，也称流量控制。是指系统在面临高并发，或者大流量请求的情况下，限制新的请求对系统的访问，从而保证系统的稳定性。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;可以使用Guava的  &lt;code&gt;RateLimiter&lt;/code&gt;单机版限流，也可以使用  &lt;code&gt;Redis&lt;/code&gt;分布式限流，还可以使用阿里开源组件  &lt;code&gt;sentinel&lt;/code&gt;限流&lt;/p&gt; &lt;p&gt;大家可以看下我之前这篇文章哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247490393&amp;idx=1&amp;sn=98189caa486406f8fa94d84ba0667604&amp;chksm=cf21c470f8564d665ce04ccb9dc7502633246da87a0541b07ba4ac99423b28ce544cdd6c036b&amp;token=162724582&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;4种经典限流算法讲解&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;26.代码实现时，注意运行时异常（比如空指针、下标越界等）&lt;/h2&gt; &lt;p&gt;日常开发中，我们需要采取措施  &lt;strong&gt;规避数组边界溢出，被零整除，空指针&lt;/strong&gt;等运行时错误。类似代码比较常见：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;String name = list.get(1).getName(); //list可能越界，因为不一定有2个元素哈   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;p&gt;应该采取措施，预防一下数组边界溢出。正例如下：&lt;/p&gt; &lt;pre&gt;  &lt;code&gt;if(CollectionsUtil.isNotEmpty(list)&amp;amp;&amp;amp; list.size()&amp;gt;1){   &lt;br /&gt;  String name = list.get(1).getName();    &lt;br /&gt;}   &lt;br /&gt;&lt;/code&gt;&lt;/pre&gt; &lt;img&gt;&lt;/img&gt; &lt;h2&gt;27.保证接口安全性&lt;/h2&gt; &lt;p&gt;如果你的API接口是对外提供的，需要保证接口的安全性。保证接口的安全性有  &lt;strong&gt;token机制和接口签名&lt;/strong&gt;。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;token机制身份验证&lt;/strong&gt;方案还比较简单的，就是&lt;/p&gt; &lt;img&gt;&lt;/img&gt; &lt;ol&gt;  &lt;li&gt;客户端发起请求，申请获取token。&lt;/li&gt;  &lt;li&gt;服务端生成全局唯一的token，保存到redis中（一般会设置一个过期时间），然后返回给客户端。&lt;/li&gt;  &lt;li&gt;客户端带着token，发起请求。&lt;/li&gt;  &lt;li&gt;服务端去redis确认token是否存在，一般用 redis.del(token)的方式，如果存在会删除成功，即处理业务逻辑，如果删除失败不处理业务逻辑，直接返回结果。&lt;/li&gt;&lt;/ol&gt; &lt;p&gt;  &lt;strong&gt;接口签名&lt;/strong&gt;的方式，就是把接口请求相关信息（请求报文，包括请求时间戳、版本号、appid等），客户端私钥加签，然后服务端用公钥验签，验证通过才认为是合法的、没有被篡改过的请求。&lt;/p&gt; &lt;p&gt;有关于加签验签的，大家可以看下我这篇文章哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247488022&amp;idx=1&amp;sn=70484a48173d36006c8db1dfb74ab64d&amp;chksm=cf21cd3ff8564429a1205f6c1d78757faae543111c8461d16c71aaee092fe3e0fed870cc5e0e&amp;token=162724582&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;程序员必备基础：加签验签&lt;/a&gt;&lt;/p&gt; &lt;p&gt;除了  &lt;strong&gt;加签验签和token机制，接口报文一般是要加密的&lt;/strong&gt;。当然，用https协议是会对报文加密的。如果是我们服务层的话，如何加解密呢？&lt;/p&gt; &lt;blockquote&gt;  &lt;p&gt;可以参考HTTPS的原理，就是服务端把公钥给客户端，然后客户端生成对称密钥，接着客户端用服务端的公钥加密对称密钥，再发到服务端，服务端用自己的私钥解密，得到客户端的对称密钥。这时候就可以愉快传输报文啦，客户端用   &lt;strong&gt;对称密钥加密请求报文&lt;/strong&gt;，   &lt;strong&gt;服务端用对应的对称密钥解密报文&lt;/strong&gt;。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;有时候，接口的安全性，还包括  &lt;strong&gt;手机号、身份证等信息的脱敏&lt;/strong&gt;。就是说，  &lt;strong&gt;用户的隐私数据，不能随便暴露&lt;/strong&gt;。&lt;/p&gt; &lt;h2&gt;28.分布式事务，如何保证&lt;/h2&gt; &lt;blockquote&gt;  &lt;p&gt;分布式事务：就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单来说，分布式事务指的就是分布式系统中的事务，它的存在就是为了保证不同数据库节点的数据一致性。&lt;/p&gt;&lt;/blockquote&gt; &lt;p&gt;分布式事务的几种解决方案：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;2PC(二阶段提交)方案、3PC&lt;/li&gt;  &lt;li&gt;TCC（Try、Confirm、Cancel）&lt;/li&gt;  &lt;li&gt;本地消息表&lt;/li&gt;  &lt;li&gt;最大努力通知&lt;/li&gt;  &lt;li&gt;seata&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;大家可以看下这篇文章哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247498358&amp;idx=1&amp;sn=aa6c7ceb61b73267d68d1b4fb7ccc2ed&amp;scene=21#wechat_redirect"&gt;看一遍就理解：分布式事务详解&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;29. 事务失效的一些经典场景&lt;/h2&gt; &lt;p&gt;我们的接口开发过程中，经常需要使用到事务。所以需要避开事务失效的一些经典场景。&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;方法的访问权限必须是public，其他private等权限，事务失效&lt;/li&gt;  &lt;li&gt;方法被定义成了final的，这样会导致事务失效。&lt;/li&gt;  &lt;li&gt;在同一个类中的方法直接内部调用，会导致事务失效。&lt;/li&gt;  &lt;li&gt;一个方法如果没交给spring管理，就不会生成spring事务。&lt;/li&gt;  &lt;li&gt;多线程调用，两个方法不在同一个线程中，获取到的数据库连接不一样的。&lt;/li&gt;  &lt;li&gt;表的存储引擎不支持事务&lt;/li&gt;  &lt;li&gt;如果自己try...catch误吞了异常，事务失效。&lt;/li&gt;  &lt;li&gt;错误的传播特性&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;推荐大家看下这篇文章：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247494570&amp;idx=2&amp;sn=17357bcd328b2d1d83f4a72c47daac1b&amp;chksm=cf223483f855bd95351a778d5f48ddd37917ce2790ebbbcd1d6ee4f27f7f4b147f0d41101dcc&amp;token=2044040586&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;聊聊spring事务失效的12种场景，太坑了&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;30. 掌握常用的设计模式&lt;/h2&gt; &lt;p&gt;把代码写好，还是需要熟练常用的设计模式，比如策略模式、工厂模式、模板方法模式、观察者模式等等。设计模式，是代码设计经验的总结。使用设计模式可以可重用代码、让代码更容易被他人理解、保证代码可靠性。&lt;/p&gt; &lt;p&gt;我之前写过一篇总结工作中常用设计模式的文章，写得挺不错的，大家可以看下：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247495616&amp;idx=1&amp;sn=e74c733d26351eab22646e44ea74d233&amp;chksm=cf2230e9f855b9ffe1ddb9fe15f72a273d5de02ed91cc97f3066d4162af027299718e2bf748e&amp;token=1260947715&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;实战！工作中常用到哪些设计模式&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;31. 写代码时，考虑线性安全问题&lt;/h2&gt; &lt;p&gt;在  &lt;strong&gt;高并发&lt;/strong&gt;情况下，  &lt;code&gt;HashMap&lt;/code&gt;可能会出现死循环。因为它是非线性安全的，可以考虑使用  &lt;code&gt;ConcurrentHashMap&lt;/code&gt;。所以这个也尽量养成习惯，不要上来反手就是一个  &lt;code&gt;new HashMap()&lt;/code&gt;;&lt;/p&gt; &lt;blockquote&gt;  &lt;ul&gt;   &lt;li&gt;Hashmap、Arraylist、LinkedList、TreeMap等都是线性不安全的；&lt;/li&gt;   &lt;li&gt;Vector、Hashtable、ConcurrentHashMap等都是线性安全的&lt;/li&gt;&lt;/ul&gt;&lt;/blockquote&gt; &lt;img&gt;&lt;/img&gt; &lt;h2&gt;32.接口定义清晰易懂，命名规范。&lt;/h2&gt; &lt;p&gt;我们写代码，不仅仅是为了实现当前的功能，也要有利于后面的维护。说到维护，代码不仅仅是写给自己看的，也是给别人看的。所以接口定义要清晰易懂，命名规范。&lt;/p&gt; &lt;h2&gt;33. 接口的版本控制&lt;/h2&gt; &lt;p&gt;接口要做好版本控制。就是说，请求基础报文，应该包含  &lt;code&gt;version&lt;/code&gt;接口版本号字段，方便未来做接口兼容。其实这个点也算接口扩展性的一个体现点吧。&lt;/p&gt; &lt;p&gt;比如客户端APP某个功能优化了，新老版本会共存，这时候我们的  &lt;code&gt;version&lt;/code&gt;版本号就派上用场了，对  &lt;code&gt;version&lt;/code&gt;做升级，做好版本控制。&lt;/p&gt; &lt;h2&gt;34. 注意代码规范问题&lt;/h2&gt; &lt;p&gt;注意一些常见的代码坏味道：&lt;/p&gt; &lt;ul&gt;  &lt;li&gt;大量重复代码（抽共用方法，设计模式）&lt;/li&gt;  &lt;li&gt;方法参数过多（可封装成一个DTO对象）&lt;/li&gt;  &lt;li&gt;方法过长（抽小函数）&lt;/li&gt;  &lt;li&gt;判断条件太多（优化if...else）&lt;/li&gt;  &lt;li&gt;不处理没用的代码&lt;/li&gt;  &lt;li&gt;不注重代码格式&lt;/li&gt;  &lt;li&gt;避免过度设计&lt;/li&gt;&lt;/ul&gt; &lt;p&gt;代码的坏味道，这里我都写到啦：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247490148&amp;idx=1&amp;sn=00a181bf74313f751b3ea15ebc303545&amp;chksm=cf21c54df8564c5bc5b4600fce46619f175f7ae557956f449629c470a08e20580feef4ea8d53&amp;token=162724582&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;25种代码坏味道总结+优化示例&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;35.保证接口正确性，其实就是保证更少的bug&lt;/h2&gt; &lt;p&gt;保证接口的正确性，换个角度讲，就是保证更少的bug，甚至是没有bug。所以接口开发完后，一般需要开发  &lt;strong&gt;自测一下&lt;/strong&gt;。然后的话，接口的正确还体现在，多线程并发的时候，  &lt;strong&gt;保证数据的正确性&lt;/strong&gt;,等等。比如你做一笔转账交易，扣减余额的时候，可以通过CAS乐观锁的方式保证余额扣减正确吧。&lt;/p&gt; &lt;p&gt;如果你是实现秒杀接口，得防止超卖问题吧。你可以使用Redis分布式锁防止超卖问题。使用Redis分布式锁，有几个注意要点，大家可以看下我之前这篇文章哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247488142&amp;idx=1&amp;sn=79a304efae7a814b6f71bbbc53810c0c&amp;chksm=cf21cda7f85644b11ff80323defb90193bc1780b45c1c6081f00da85d665fd9eb32cc934b5cf&amp;token=162724582&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;七种方案！探讨Redis分布式锁的正确使用姿势&lt;/a&gt;&lt;/p&gt; &lt;h2&gt;36.学会沟通，跟前端沟通，跟产品沟通&lt;/h2&gt; &lt;p&gt;我把这一点放到最后，学会沟通是非常非常重要的。比如你开发定义接口时，  &lt;strong&gt;一定不能上来就自己埋头把接口定义完了&lt;/strong&gt;，  &lt;strong&gt;需要跟客户端先对齐接口&lt;/strong&gt;。遇到一些难点时，跟技术leader对齐方案。实现需求的过程中，有什么问题，及时跟产品沟通。&lt;/p&gt; &lt;p&gt;总之就是，开发接口过程中，一定要沟通好~&lt;/p&gt; &lt;h2&gt;最后(求关注，别白嫖我)&lt;/h2&gt; &lt;p&gt;如果这篇文章对您有所帮助，或者有所启发的话，求一键三连：点赞、转发、在看，您的支持是我坚持写作最大的动力。&lt;/p&gt;&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>dev</category>
      <guid isPermaLink="true">https://itindex.net/detail/62273-%E6%8E%A5%E5%8F%A3%E8%AE%BE%E8%AE%A1-%E6%8A%80%E5%B7%A7</guid>
      <pubDate>Wed, 25 May 2022 00:00:00 CST</pubDate>
    </item>
    <item>
      <title>支付设计白皮书：支付系统的总架构</title>
      <link>https://itindex.net/detail/62272-%E8%AE%BE%E8%AE%A1-%E7%99%BD%E7%9A%AE%E4%B9%A6-%E7%B3%BB%E7%BB%9F</link>
      <description>&lt;p&gt;“持续创作，加速成长！这是我参与「掘金日新计划 · 6 月更文挑战」的第1天，  &lt;a href="https://juejin.cn/post/7099702781094674468"&gt;点击查看活动详情&lt;/a&gt;”&lt;/p&gt;
 &lt;h1&gt;前言&lt;/h1&gt;
 &lt;blockquote&gt;
  &lt;p&gt;文本已收录至我的GitHub仓库，欢迎Star：https://github.com/bin392328206/six-finger   &lt;br /&gt;
   &lt;strong&gt;种一棵树最好的时间是十年前，其次是现在&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h2&gt;絮叨&lt;/h2&gt;
 &lt;p&gt;大家好，我是小六六，三天打鱼，两天晒网，小六六接触进入到支付这个行业也快一年了，从今天开始就开始输出自己的对支付相关的知识的梳理来和大家一起来学习支付，支付是一个非常大并且应用广泛的一个行业，它是万事万物的基础！  &lt;code&gt;我觉得任何产品的最后一公里肯定是支付了。有人说：“支付很简单。”，有人说：“支付很难。” &lt;/code&gt;如果你对支付感兴趣，建议关注我哦！大家一起学习！&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://juejin.cn/post/7101522332883091463"&gt;支付设计白皮书：支付系统的概念与中国互联网支付清算体系&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;中国互联网支付总架构&lt;/h2&gt;
 &lt;p&gt;今天这篇文章就是想带大家来了解下一个从点到点，从端到端，从始到终的支付链路，最近三只松鼠的坚果不是挺火的嘛，那六六就以从京东买三只松鼠为例，带大家从整个宏观的角度来看看中国的互联网支付！&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#25903;&amp;#20184;&amp;#24635;&amp;#38142;&amp;#36335;.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bebf806e833f462d84fe1081cb99768b~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&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;h2&gt;来看看京东支付的架构&lt;/h2&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/34ef8ce66b024246a57e621aa8c383b1~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;其实这几秒钟整个支付的链条跋山涉水，翻山越岭经历千险，&lt;/p&gt;
 &lt;h2&gt;支付架构解析&lt;/h2&gt;
 &lt;p&gt;我们看上面的架构图，对于一个服务平台的支付架构，一般有图中的相关系统组成：直面用户的收银台，记录业务的订单系统，推动交易的交易系统，对支付指令进行处理的支付系统，支付指令传送通道的支付通道子系统。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b9894ac15dd74fac989e3bbc4790bec4~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;另外支付成功后还有一条线清结算线：支付成功以后交易将数据提交清算中心完成数据的清分计算，然后提交账务系统完成记账；再通知会计核心完成内部账的记录；最后通知资金平台对交易向商家进行货款的结算……&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6d4f05ca70ac4974a9a745c24c70a04d~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;这样对于一个服务平台来说，一个支付的骨架就出来了！&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/38f4a06e48f94f04ab5e16a4df16913d~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;其实很多第三方支付公司都是这么玩的 你比如说国内的京东支付，微信支付，海外的Paypal,Strip checkout等等&lt;/p&gt;
 &lt;h2&gt;支付系统架构&lt;/h2&gt;
 &lt;p&gt;支付系统的主要职责是处理业务系统发起的所有交易请求，包含收银台、交易系统、支付核心等模块，根据各模块不同的功能职责，可以将支付系统分为业务层和支付层两部分。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;业务层负责为业务系统提供收付款的操作界面以及处理业务系统提交的交易请求；&lt;/li&gt;
  &lt;li&gt;支付层负责通过支付渠道实时处理完成资金的收付款、记录参与交易的账户间资金流转情况并按照预定规则对账户所属资金进行拆分与合并。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/705d9dcf208e41f6843bbc00b1e6124f~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;收银台&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;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;/ul&gt;
 &lt;h3&gt;交易核心&lt;/h3&gt;
 &lt;p&gt;交易系统本身是作为支付系统外部处理业务逻辑的外围系统。由于支付核心系统本身并非面向业务端且业务逻辑的多变性与复杂性，支付系统为了兼顾稳定并能够为业务端提供灵活支持，因此需要在支付系统外层搭建面向业务端处理交易逻辑的交易系统。交易系统处理业务端的各种交易类型后，将业务信息转化为支付系统可识别的支付订单并导入。&lt;/p&gt;
 &lt;p&gt;以担保交易为例，C 端用户在天猫购买一件商品，成功支付后商家进行发货，用户确认收货后平台将货款结算给商家。此处设计到「担保交易支付」以及「确认收货」环节，与支付系统内部的支付与结算步骤一一对应：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;用户付款成功后对应交易的付款成功状态；&lt;/li&gt;
  &lt;li&gt;用户确认收货后对应交易的成功状态。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;从支付和收货缓解可以看出，担保收单交易就是讲支付系统的支付基础能力包装后对外支持业务的一款产品。&lt;/p&gt;
 &lt;h3&gt;  &lt;strong&gt;会员系统&lt;/strong&gt;&lt;/h3&gt;
 &lt;p&gt;会员系统是完整的支付平台内极其重要的基础模块之一，负责管理支付系统内部的交易主体。会员系统保存了客户在支付系统内部账号的实体信息，为客户建立了统一的、以会员 ID 为标识的会员基本信息、关系信息（会员和账户、会员和操作人、会员与银行卡）视图。&lt;/p&gt;
 &lt;p&gt;一般情况，会员在支付系统内部分为个人会员和企业会员（默认企业会员有商户权限），以电商平台为例，C 端用户为个人会员，B 端商户为企业会员：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;通常，企业会员会配置一定的业务参数，比如结算周期、接口权限、支付方式配置等（开通商户权限的情况下）；&lt;/li&gt;
  &lt;li&gt;在大多数互联网公司，支付系统仅需要对接支付渠道的模块，在没有独立平台化的情况下，不太会出现需要独立的账户体系。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;  &lt;strong&gt;支付核心&lt;/strong&gt;&lt;/h3&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;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;/ul&gt;
 &lt;h3&gt;  &lt;strong&gt;账务核心&lt;/strong&gt;&lt;/h3&gt;
 &lt;p&gt;账务核心的功能为，根据前端业务系统的要求设计相匹配的账户类型、管理各类账户、记录账户资金变动等，同时，按照公司内部的财会规范提供反映各账户间交易资金变化情况的会计数据；并且负责将自身记录账务流水与支付渠道结算资金和结算流水进行核对，对对账结果中出现的差错交易进行差错处理。&lt;/p&gt;
 &lt;h3&gt;清算核心&lt;/h3&gt;
 &lt;p&gt;清算核心负责维护客户参与交易时的清分、结算规则，并按照已配置的规则完成交易资金的清分与结算操作。&lt;/p&gt;
 &lt;h2&gt;结束&lt;/h2&gt;
 &lt;p&gt;由此可见如果你要做一个第三方支付公司的，大大小小估计得建设几十个系统呢？所以来说，支付并不简单，后面六六会和大家一起来学习各个系统！&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62272-%E8%AE%BE%E8%AE%A1-%E7%99%BD%E7%9A%AE%E4%B9%A6-%E7%B3%BB%E7%BB%9F</guid>
      <pubDate>Wed, 25 May 2022 12:15:23 CST</pubDate>
    </item>
    <item>
      <title>设计好接口的36个锦囊</title>
      <link>https://itindex.net/detail/62260-%E8%AE%BE%E8%AE%A1-%E6%8E%A5%E5%8F%A3</link>
      <description>&lt;h2&gt;前言&lt;/h2&gt;
 &lt;p&gt;大家好，我是捡田螺的小男孩。作为后端开发，不管是什么语言，  &lt;code&gt;Java&lt;/code&gt;、  &lt;code&gt;Go&lt;/code&gt;还是  &lt;code&gt;C++&lt;/code&gt;，其背后的后端思想都是类似的。后面打算出一个后端思想的技术专栏，主要包括后端的一些设计、或者后端规范相关的，希望对大家日常工作有帮助哈。&lt;/p&gt;
 &lt;p&gt;我们做后端开发工程师，主要工作就是：  &lt;strong&gt;如何把一个接口设计好&lt;/strong&gt;。所以，今天就给大家介绍，设计好接口的36个锦囊。本文就是后端思想专栏的第一篇哈。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1bfc8123cee34de2ad82b736121165d2~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;公众号：捡田螺的小男孩&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;1. 接口参数校验&lt;/h2&gt;
 &lt;p&gt;入参出参校验是每个程序员必备的基本素养。你设计的接口，必须先校验参数。比如入参是否允许为空，入参长度是否符合你的预期长度。这个要养成习惯哈，日常开发中，很多低级bug都是不校验参数导致的。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;比如你的数据库表字段设置为   &lt;code&gt;varchar(16)&lt;/code&gt;,对方传了一个32位的字符串过来，如果你不校验参数，   &lt;strong&gt;插入数据库直接异常了&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;出参也是，比如你定义的接口报文，参数是不为空的，但是你的接口返回参数，没有做校验，因为程序某些原因，直返回别人一个  &lt;code&gt;null&lt;/code&gt;值。。。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bfd3392f3ce6408daa1940cc185f0d5f~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;2. 修改老接口时，注意接口的兼容性&lt;/h2&gt;
 &lt;p&gt;很多bug都是因为修改了对外旧接口，但是却  &lt;strong&gt;不做兼容&lt;/strong&gt;导致的。关键这个问题多数是比较严重的，可能直接导致系统发版失败的。新手程序员很容易犯这个错误哦~&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/701ac23b5dd04149b277c4001721fb87~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;所以，如果你的需求是在原来接口上修改，尤其这个接口是对外提供服务的话，一定要考虑接口兼容。举个例子吧，比如dubbo接口，原本是只接收A，B参数，现在你加了一个参数C，就可以考虑这样处理：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;//老接口
void oldService(A,B){
  //兼容新接口，传个null代替C
  newService(A,B,null);
}

//新接口，暂时不能删掉老接口，需要做兼容。
void newService(A,B,C){
  ...
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;3.  设计接口时，充分考虑接口的可扩展性&lt;/h2&gt;
 &lt;p&gt;要根据实际业务场景设计接口，充分考虑接口的可扩展性。&lt;/p&gt;
 &lt;p&gt;比如你接到一个需求：是用户添加或者修改员工时，需要刷脸。那你是反手提供一个员工管理的提交刷脸信息接口？还是先思考：提交刷脸是不是通用流程呢？比如转账或者一键贴现需要接入刷脸的话，你是否需要重新实现一个接口呢？还是当前按业务类型划分模块，复用这个接口就好，保留接口的可扩展性。&lt;/p&gt;
 &lt;p&gt;如果按模块划分的话，未来如果其他场景比如一键贴现接入刷脸的话，不用再搞一套新的接口，只需要新增枚举，然后复用刷脸通过流程接口，实现一键贴现刷脸的差异化即可。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cd3ee52ecaa34de384bb529cbb358889~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;4.接口考虑是否需要防重处理&lt;/h2&gt;
 &lt;p&gt;如果前端重复请求，你的逻辑如何处理？是不是考虑接口去重处理。&lt;/p&gt;
 &lt;p&gt;当然，如果是查询类的请求，其实不用防重。如果是更新修改类的话，尤其金融转账类的，就要过滤重复请求了。简单点，你可以使用Redis防重复请求，同样的请求方，一定时间间隔内的相同请求，考虑是否过滤。当然，转账类接口，并发不高的话，  &lt;strong&gt;推荐使用数据库防重表&lt;/strong&gt;，以  &lt;strong&gt;唯一流水号作为主键或者唯一索引&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/059928f565ba4d27a17c54f451b0235d~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;5. 重点接口，考虑线程池隔离。&lt;/h2&gt;
 &lt;p&gt;一些登陆、转账交易、下单等重要接口，考虑线程池隔离哈。如果你所有业务都共用一个线程池，有些业务出bug导致线程池阻塞打满的话，那就杯具了，  &lt;strong&gt;所有业务都影响了&lt;/strong&gt;。因此进行线程池隔离，重要业务分配多一点的核心线程，就更好保护重要业务。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1d30804afc044026b4eb7bad23689c42~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;6. 调用第三方接口要考虑异常和超时处理&lt;/h2&gt;
 &lt;p&gt;如果你调用第三方接口，或者分布式远程服务的的话，需要考虑：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;异常处理&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;
  &lt;p&gt;比如，你调别人的接口，如果异常了，怎么处理，是重试还是当做失败还是告警处理。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;ul&gt;
  &lt;li&gt;接口超时&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;
  &lt;p&gt;没法预估对方接口一般多久返回，一般设置个超时断开时间，以保护你的接口。   &lt;strong&gt;之前见过一个生产问题&lt;/strong&gt;，就是http调用不设置超时时间，最后响应方进程假死，请求一直占着线程不释放，拖垮线程池。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;ul&gt;
  &lt;li&gt;重试次数&lt;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;
  &lt;p&gt;你的接口调失败，需不需要重试？重试几次？需要站在业务上角度思考这个问题&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/25ec61c10c324ada9252745fa4017ad6~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;7. 接口实现考虑熔断和降级&lt;/h2&gt;
 &lt;p&gt;当前互联网系统一般都是分布式部署的。而分布式系统中经常会出现某个基础服务不可用，最终导致整个系统不可用的情况, 这种现象被称为  &lt;strong&gt;服务雪崩效应&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;比如分布式调用链路  &lt;code&gt;A-&amp;gt;B-&amp;gt;C....&lt;/code&gt;，下图所示：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/65240791c94c44b6aab143178eeb790c~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;如果服务C出现问题，比如是   &lt;strong&gt;因为慢SQL导致调用缓慢&lt;/strong&gt;，那将导致B也会延迟，从而A也会延迟。堵住的A请求会消耗占用系统的线程、IO等资源。 当请求A的服务越来越多，占用计算机的资源也越来越多，最终会导致系统瓶颈出现，造成其他的请求同样不可用，最后导致业务系统崩溃。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;为了应对服务雪崩, 常见的做法是  &lt;strong&gt;熔断和降级&lt;/strong&gt;。最简单是加开关控制，当下游系统出问题时，开关降级，不再调用下游系统。还可以选用开源组件  &lt;code&gt;Hystrix&lt;/code&gt;。&lt;/p&gt;
 &lt;h2&gt;8. 日志打印好，接口的关键代码，要有日志保驾护航。&lt;/h2&gt;
 &lt;p&gt;关键业务代码无论身处何地，都应该有足够的日志保驾护航。
比如：你实现转账业务，转个几百万，然后转失败了，接着客户投诉，然后你还没有打印到日志，想想那种水深火热的困境下，你却毫无办法。。。&lt;/p&gt;
 &lt;p&gt;那么，你的转账业务都需要那些日志信息呢？至少，方法调用前，入参需要打印需要吧，接口调用后，需要捕获一下异常吧，同时打印异常相关日志吧，如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;public void transfer(TransferDTO transferDTO){
    log.info(&amp;quot;invoke tranfer begin&amp;quot;);
    //打印入参
    log.info(&amp;quot;invoke tranfer,paramters:{}&amp;quot;,transferDTO);
    try {
      res=  transferService.transfer(transferDTO);
    }catch(Exception e){
     log.error(&amp;quot;transfer fail,account：{}&amp;quot;,
     transferDTO.getAccount（）)
     log.error(&amp;quot;transfer fail,exception:{}&amp;quot;,e);
    }
    log.info(&amp;quot;invoke tranfer end&amp;quot;);
    }
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;之前写过一篇打印日志的15个建议，大家可以看看哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247494838&amp;idx=1&amp;sn=cdb15fd346bddf3f8c1c99f0efbd67d8&amp;chksm=cf22339ff855ba891616c79d4f4855e228e34a9fb45088d7acbe421ad511b8d090a90f5b019f&amp;token=162724582&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;工作总结！日志打印的15个建议&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;9. 接口的功能定义要具备单一性&lt;/h2&gt;
 &lt;p&gt;单一性是指接口做的事情比较单一、专一。比如一个登陆接口，它做的事情就只是校验账户名密码，然后返回登陆成功以及  &lt;code&gt;userId&lt;/code&gt;即可。  &lt;strong&gt;但是如果你为了减少接口交互，把一些注册、一些配置查询等全放到登陆接口，就不太妥。&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;其实这也是微服务一些思想，接口的功能单一、明确。比如订单服务、积分、商品信息相关的接口都是划分开的。将来拆分微服务的话，是不是就比较简便啦。&lt;/p&gt;
 &lt;h2&gt;10.接口有些场景，使用异步更合理&lt;/h2&gt;
 &lt;p&gt;举个简单的例子，比如你实现一个用户注册的接口。用户注册成功时，发个邮件或者短信去通知用户。这个邮件或者发短信，就更适合异步处理。因为总不能一个通知类的失败，导致注册失败吧。&lt;/p&gt;
 &lt;p&gt;至于做异步的方式，简单的就是  &lt;strong&gt;用线程池&lt;/strong&gt;。还可以使用消息队列，就是用户注册成功后，生产者产生一个注册成功的消息，消费者拉到注册成功的消息，就发送通知。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/585b098a67b349d495e6e8579ea85e4c~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;不是所有的接口都适合设计为同步接口。比如你要做一个转账的功能，如果你是单笔的转账，你是可以把接口设计同步。用户发起转账时，客户端在静静等待转账结果就好。如果你是批量转账，一个批次一千笔，甚至一万笔的，你则可以把接口设计为异步。就是用户发起批量转账时，持久化成功就先返回受理成功。然后用户隔十分钟或者十五分钟等再来查转账结果就好。又或者，批量转账成功后，再回调上游系统。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1ae74868492344c4bcbab9b480904c47~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;11. 优化接口耗时，远程串行考虑改并行调用&lt;/h2&gt;
 &lt;p&gt;假设我们设计一个APP首页的接口，它需要查用户信息、需要查banner信息、需要查弹窗信息等等。那你是一个一个接口串行调，还是并行调用呢？&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d118e2b09e1f4fc6a1003fd44a43e4c7~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;如果是串行一个一个查，比如查用户信息200ms，查banner信息100ms、查弹窗信息50ms，那一共就耗时  &lt;code&gt;350ms&lt;/code&gt;了，如果还查其他信息，那耗时就更大了。这种场景是可以改为并行调用的。也就是说查用户信息、查banner信息、查弹窗信息，可以同时发起。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/83561366219b48a2a85a6bb0419f82a3~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;在Java中有个异步编程利器：  &lt;code&gt;CompletableFuture&lt;/code&gt;，就可以很好实现这个功能。有兴趣的小伙伴可以看我之前这个文章哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247490456&amp;idx=1&amp;sn=95836324db57673a4d7aea4fb233c0d2&amp;chksm=cf21c4b1f8564da72dc7b39279362bcf965b1374540f3b339413d138599f7de59a5f977e3b0e&amp;token=1260947715&amp;lang=zh_CN#rd"&gt;CompletableFuture详解&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;12. 接口合并或者说考虑批量处理思想&lt;/h2&gt;
 &lt;p&gt;数据库操作或或者是远程调用时，能批量操作就不要for循环调用。
  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/855cd5cf57d047be909dbc41ddacc021~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;一个简单例子，我们平时一个列表明细数据插入数据库时，不要在for循环一条一条插入，建议一个批次几百条，进行批量插入。同理远程调用也类似想法，比如你查询营销标签是否命中，可以一个标签一个标签去查，也可以批量标签去查，那批量进行，效率就更高嘛。&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;//反例
for(int i=0;i&amp;lt;n;i++){
  remoteSingleQuery(param)
}

//正例
remoteBatchQuery(param);
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;小伙伴们是否了解过  &lt;code&gt;kafka&lt;/code&gt;为什么这么快呢？其实其中一点原因，就是kafka  &lt;strong&gt;使用批量消息&lt;/strong&gt;提升服务端处理能力。&lt;/p&gt;
 &lt;h2&gt;13. 接口实现过程中，恰当使用缓存&lt;/h2&gt;
 &lt;p&gt;哪些场景适合使用缓存？   &lt;strong&gt;读多写少且数据时效要求越低的场景&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;缓存用得好，可以承载更多的请求，提升查询效率，减少数据库的压力。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;比如一些平时变动很小或者说几乎不会变的商品信息，可以放到缓存，请求过来时，先查询缓存，如果没有再查数据库，并且把数据库的数据更新到缓存。但是，使用缓存增加了需要考虑这些点：缓存和数据库一致性如何保证、集群、缓存击穿、缓存雪奔、缓存穿透等问题。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;ul&gt;
  &lt;li&gt;保证数据库和缓存一致性：   &lt;strong&gt;缓存延时双删、删除缓存重试机制、读取biglog异步删除缓存&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;缓存击穿：设置数据永不过期&lt;/li&gt;
  &lt;li&gt;缓存雪奔：Redis集群高可用、均匀设置过期时间&lt;/li&gt;
  &lt;li&gt;缓存穿透：接口层校验、查询为空设置个默认空值标记、布隆过滤器。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;一般用  &lt;code&gt;Redis&lt;/code&gt;分布式缓存，当然有些时候也可以考虑使用本地缓存，如  &lt;code&gt;Guava Cache、Caffeine&lt;/code&gt;等。使用本地缓存有些缺点，就是无法进行大数据存储，并且应用进程的重启，缓存会失效。&lt;/p&gt;
 &lt;h2&gt;14. 接口考虑热点数据隔离性&lt;/h2&gt;
 &lt;p&gt;瞬时间的高并发，可能会打垮你的系统。可以做一些热点数据的隔离。比如  &lt;strong&gt;业务隔离、系统隔离、用户隔离、数据隔离&lt;/strong&gt;等。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;业务隔离性，比如12306的分时段售票，将热点数据分散处理，降低系统负载压力。&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;h2&gt;15. 可变参数配置化，比如红包皮肤切换等&lt;/h2&gt;
 &lt;p&gt;假如产品经理提了个红包需求，圣诞节的时候，红包皮肤为圣诞节相关的，春节的时候，为春节红包皮肤等。&lt;/p&gt;
 &lt;p&gt;如果在代码写死控制，可有类似以下代码：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;if(duringChristmas){
   img = redPacketChristmasSkin;
}else if(duringSpringFestival){
   img =  redSpringFestivalSkin;
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;如果到了元宵节的时候，运营小姐姐突然又有想法，红包皮肤换成灯笼相关的，这时候，是不是要去修改代码了，重新发布了？&lt;/p&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&gt;16.接口考虑幂等性&lt;/h2&gt;
 &lt;p&gt;接口是需要考虑幂等性的，尤其抢红包、转账这些重要接口。最直观的业务场景，就是  &lt;strong&gt;用户连着点击两次&lt;/strong&gt;，你的接口有没有  &lt;strong&gt;hold住&lt;/strong&gt;。或者消息队列出现重复消费的情况，你的业务逻辑怎么控制？&lt;/p&gt;
 &lt;p&gt;回忆下，  &lt;strong&gt;什么是幂等？&lt;/strong&gt;&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;计算机科学中，幂等表示一次和多次请求某一个资源应该具有同样的副作用，或者说，多次请求所产生的影响与一次请求执行的影响效果相同。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;大家别搞混哈，  &lt;strong&gt;防重和幂等设计其实是有区别的&lt;/strong&gt;。防重主要为了避免产生重复数据，把重复请求拦截下来即可。而幂等设计除了拦截已经处理的请求，还要求每次相同的请求都返回一样的效果。不过呢，很多时候，它们的处理流程、方案是类似的哈。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f8c3d0d5a653455198ba3259ef221387~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;接口幂等实现方案主要有8种：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;select+insert+主键/唯一索引冲突&lt;/li&gt;
  &lt;li&gt;直接insert + 主键/唯一索引冲突&lt;/li&gt;
  &lt;li&gt;状态机幂等&lt;/li&gt;
  &lt;li&gt;抽取防重表&lt;/li&gt;
  &lt;li&gt;token令牌&lt;/li&gt;
  &lt;li&gt;悲观锁&lt;/li&gt;
  &lt;li&gt;乐观锁&lt;/li&gt;
  &lt;li&gt;分布式锁&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;大家可以看我这篇文章哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247497427&amp;idx=1&amp;sn=2ed160c9917ad989eee1ac60d6122855&amp;chksm=cf2229faf855a0ecf5eb34c7335acdf6420426490ee99fc2b602d54ff4ffcecfdab24eeab0a3&amp;token=1260947715&amp;lang=zh_CN#rd"&gt;聊聊幂等设计&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;17. 读写分离，优先考虑读从库，注意主从延迟问题&lt;/h2&gt;
 &lt;p&gt;我们的数据库都是集群部署的，有主库也有从库，当前一般都是读写分离的。比如你写入数据，肯定是写入主库，但是对于读取实时性要求不高的数据，则优先考虑读从库，因为可以分担主库的压力。&lt;/p&gt;
 &lt;p&gt;如果读取从库的话，需要考虑主从延迟的问题。&lt;/p&gt;
 &lt;h2&gt;18.接口注意返回的数据量，如果数据量大需要分页&lt;/h2&gt;
 &lt;p&gt;一个接口返回报文，不应该包含过多的数据量。过多的数据量不仅处理复杂，并且数据量传输的压力也非常大。因此数量实在是比较大，可以分页返回，如果是功能不相关的报文，那应该考虑接口拆分。&lt;/p&gt;
 &lt;h2&gt;19. 好的接口实现，离不开SQL优化&lt;/h2&gt;
 &lt;p&gt;我们做后端的，写好一个接口，离不开SQL优化。&lt;/p&gt;
 &lt;p&gt;SQL优化从这几个维度思考：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;explain 分析SQL查询计划（重点关注type、extra、filtered字段）&lt;/li&gt;
  &lt;li&gt;show profile分析，了解SQL执行的线程的状态以及消耗的时间&lt;/li&gt;
  &lt;li&gt;索引优化 （覆盖索引、最左前缀原则、隐式转换、order by以及group by的优化、join优化）&lt;/li&gt;
  &lt;li&gt;大分页问题优化（延迟关联、记录上一页最大ID）&lt;/li&gt;
  &lt;li&gt;数据量太大（   &lt;strong&gt;分库分表&lt;/strong&gt;、同步到es，用es查询）&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;20.代码锁的粒度控制好&lt;/h2&gt;
 &lt;p&gt;什么是加锁粒度呢？&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;其实就是就是你要锁住的范围是多大。比如你在家上卫生间，你只要锁住卫生间就可以了吧，不需要将整个家都锁起来不让家人进门吧，卫生间就是你的加锁粒度。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;我们写代码时，如果不涉及到共享资源，就没有必要锁住的。这就好像你上卫生间，不用把整个家都锁住，锁住卫生间门就可以了。&lt;/p&gt;
 &lt;p&gt;比如，在业务代码中，有一个ArrayList因为涉及到多线程操作，所以需要加锁操作，假设刚好又有一段比较耗时的操作（代码中的  &lt;code&gt;slowNotShare&lt;/code&gt;方法）不涉及线程安全问题，你会如何加锁呢？&lt;/p&gt;
 &lt;p&gt;反例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;//不涉及共享资源的慢方法
private void slowNotShare() {
    try {
        TimeUnit.MILLISECONDS.sleep(100);
    } catch (InterruptedException e) {
    }
}

//错误的加锁方法
public int wrong() {
    long beginTime = System.currentTimeMillis();
    IntStream.rangeClosed(1, 10000).parallel().forEach(i -&amp;gt; {
        //加锁粒度太粗了，slowNotShare其实不涉及共享资源
        synchronized (this) {
            slowNotShare();
            data.add(i);
        }
    });
    log.info(&amp;quot;cosume time:{}&amp;quot;, System.currentTimeMillis() - beginTime);
    return data.size();
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;正例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;public int right() {
    long beginTime = System.currentTimeMillis();
    IntStream.rangeClosed(1, 10000).parallel().forEach(i -&amp;gt; {
        slowNotShare();//可以不加锁
        //只对List这部分加锁
        synchronized (data) {
            data.add(i);
        }
    });
    log.info(&amp;quot;cosume time:{}&amp;quot;, System.currentTimeMillis() - beginTime);
    return data.size();
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;h2&gt;21.接口状态和错误需要统一明确&lt;/h2&gt;
 &lt;p&gt;提供必要的接口调用状态信息。比如你的一个转账接口调用是成功、失败、处理中还是受理成功等，需要明确告诉客户端。如果接口失败，那么具体失败的原因是什么。这些必要的信息都必须要告诉给客户端，因此需要定义明确的错误码和对应的描述。同时，尽量对报错信息封装一下，不要把后端的异常信息完全抛出到客户端。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/20a1080126274c04aa31802178c01bb0~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;22.接口要考虑异常处理&lt;/h2&gt;
 &lt;p&gt;实现一个好的接口，离不开优雅的异常处理。对于异常处理，提十个小建议吧：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;尽量不要使用   &lt;code&gt;e.printStackTrace()&lt;/code&gt;,而是使用   &lt;code&gt;log&lt;/code&gt;打印。因为   &lt;code&gt;e.printStackTrace()&lt;/code&gt;语句可能会导致内存占满。&lt;/li&gt;
  &lt;li&gt;   &lt;code&gt;catch&lt;/code&gt;住异常时，建议打印出具体的   &lt;code&gt;exception&lt;/code&gt;，利于更好定位问题&lt;/li&gt;
  &lt;li&gt;不要用一个   &lt;code&gt;Exception&lt;/code&gt;捕捉所有可能的异常&lt;/li&gt;
  &lt;li&gt;记得使用   &lt;code&gt;finally&lt;/code&gt;关闭流资源或者直接使用   &lt;code&gt;try-with-resource&lt;/code&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;code&gt;Throwable cause&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;运行时异常   &lt;code&gt;RuntimeException&lt;/code&gt; ，不应该通过   &lt;code&gt;catch&lt;/code&gt;的方式来处理，而是先预检查，比如：   &lt;code&gt;NullPointerException&lt;/code&gt;处理&lt;/li&gt;
  &lt;li&gt;注意异常匹配的顺序，优先捕获具体的异常&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;小伙伴们有兴趣可以看下我之前写的这篇文章哈：  &lt;a href="https://mp.weixin.qq.com/s/3mqY77c8iXWvJFzkVQi9Og"&gt;Java 异常处理的十个建议&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;23. 优化程序逻辑&lt;/h2&gt;
 &lt;p&gt;优化程序逻辑这块还是挺重要的，也就是说，你实现的业务代码，  &lt;strong&gt;如果是比较复杂的话，建议把注释写清楚&lt;/strong&gt;。还有，代码逻辑尽量清晰，代码尽量高效。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;比如，你要使用用户信息的属性，你根据session已经获取到   &lt;code&gt;userId&lt;/code&gt;了，然后就把用户信息从数据库查询出来，使用完后，后面可能又要用到用户信息的属性，有些小伙伴没想太多，反手就把   &lt;code&gt;userId&lt;/code&gt;再传进去，再查一次数据库。。。我在项目中，见过这种代码。。。直接把用户对象传下来不好嘛。。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;反例伪代码：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;public Response test(Session session){
    UserInfo user = UserDao.queryByUserId(session.getUserId());
    
    if(user==null){
       reutrn new Response();
    }
    
    return do(session.getUserId());
}

public Response do(String UserId){
  //多查了一次数据库
  UserInfo user = UserDao.queryByUserId(session.getUserId());
  ......
  return new Response(); 
}

&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;正例：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;public Response test(Session session){
    UserInfo user = UserDao.queryByUserId(session.getUserId());
    
    if(user==null){
       reutrn new Response();
    }
    
    return do(session.getUserId());
}

//直接传UserInfo对象过来即可，不用再多查一次数据库
public Response do(UserInfo user){
  ......
  return new Response(); 
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;当然，这只是一些很小的一个例子，还有很多类似的例子，需要大家开发过程中，多点思考的哈。&lt;/p&gt;
 &lt;h2&gt;24. 接口实现过程汇中，注意大文件、大事务、大对象&lt;/h2&gt;
 &lt;ul&gt;
  &lt;li&gt;读取大文件时，不要   &lt;code&gt;Files.readAllBytes&lt;/code&gt;直接读取到内存，这样会OOM的，建议使用   &lt;code&gt;BufferedReader&lt;/code&gt;一行一行来。&lt;/li&gt;
  &lt;li&gt;大事务可能导致死锁、回滚时间长、主从延迟等问题，开发中尽量避免大事务。&lt;/li&gt;
  &lt;li&gt;注意一些大对象的使用，因为大对象是直接进入老年代的，会触发fullGC&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;25. 你的接口，需要考虑限流&lt;/h2&gt;
 &lt;p&gt;如果你的系统每秒扛住的请求是1000，如果一秒钟来了十万请求呢？换个角度就是说，高并发的时候，流量洪峰来了，超过系统的承载能力，怎么办呢？&lt;/p&gt;
 &lt;p&gt;如果不采取措施，所有的请求打过来，系统CPU、内存、Load负载飚的很高，最后请求处理不过来，所有的请求无法正常响应。&lt;/p&gt;
 &lt;p&gt;针对这种场景，我们可以采用限流方案。就是为了保护系统，多余的请求，直接丢弃。&lt;/p&gt;
 &lt;p&gt;限流定义：&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;在计算机网络中，限流就是控制网络接口发送或接收请求的速率，它可防止DoS攻击和限制Web爬虫。限流，也称流量控制。是指系统在面临高并发，或者大流量请求的情况下，限制新的请求对系统的访问，从而保证系统的稳定性。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;可以使用Guava的  &lt;code&gt;RateLimiter&lt;/code&gt;单机版限流，也可以使用  &lt;code&gt;Redis&lt;/code&gt;分布式限流，还可以使用阿里开源组件  &lt;code&gt;sentinel&lt;/code&gt;限流&lt;/p&gt;
 &lt;p&gt;大家可以看下我之前这篇文章哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247490393&amp;idx=1&amp;sn=98189caa486406f8fa94d84ba0667604&amp;chksm=cf21c470f8564d665ce04ccb9dc7502633246da87a0541b07ba4ac99423b28ce544cdd6c036b&amp;token=162724582&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;4种经典限流算法讲解&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;26.代码实现时，注意运行时异常（比如空指针、下标越界等）&lt;/h2&gt;
 &lt;p&gt;日常开发中，我们需要采取措施  &lt;strong&gt;规避数组边界溢出，被零整除，空指针&lt;/strong&gt;等运行时错误。类似代码比较常见：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;String name = list.get(1).getName(); //list可能越界，因为不一定有2个元素哈
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;应该采取措施，预防一下数组边界溢出。正例如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;if(CollectionsUtil.isNotEmpty(list)&amp;amp;&amp;amp; list.size()&amp;gt;1){
  String name = list.get(1).getName(); 
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/10199365140845ea8f7b29a07fbaf3cc~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;27.保证接口安全性&lt;/h2&gt;
 &lt;p&gt;如果你的API接口是对外提供的，需要保证接口的安全性。保证接口的安全性有  &lt;strong&gt;token机制和接口签名&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;token机制身份验证&lt;/strong&gt;方案还比较简单的，就是&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9b468f89cdaf4040b84e432182903fd9~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;客户端发起请求，申请获取token。&lt;/li&gt;
  &lt;li&gt;服务端生成全局唯一的token，保存到redis中（一般会设置一个过期时间），然后返回给客户端。&lt;/li&gt;
  &lt;li&gt;客户端带着token，发起请求。&lt;/li&gt;
  &lt;li&gt;服务端去redis确认token是否存在，一般用 redis.del(token)的方式，如果存在会删除成功，即处理业务逻辑，如果删除失败不处理业务逻辑，直接返回结果。&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;  &lt;strong&gt;接口签名&lt;/strong&gt;的方式，就是把接口请求相关信息（请求报文，包括请求时间戳、版本号、appid等），客户端私钥加签，然后服务端用公钥验签，验证通过才认为是合法的、没有被篡改过的请求。&lt;/p&gt;
 &lt;p&gt;有关于加签验签的，大家可以看下我这篇文章哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247488022&amp;idx=1&amp;sn=70484a48173d36006c8db1dfb74ab64d&amp;chksm=cf21cd3ff8564429a1205f6c1d78757faae543111c8461d16c71aaee092fe3e0fed870cc5e0e&amp;token=162724582&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;程序员必备基础：加签验签&lt;/a&gt;&lt;/p&gt;
 &lt;p&gt;处了  &lt;strong&gt;加签验签和token机制，接口报文一般是要加密的&lt;/strong&gt;。当然，用https协议是会对报文加密的。如果是我们服务层的话，如何加解密呢？&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;可以参考HTTPS的原理，就是服务端把公钥给客户端，然后客户端生成对称密钥，接着客户端用服务端的公钥加密对称密钥，再发到服务端，服务端用自己的私钥解密，得到客户端的对称密钥。这时候就可以愉快传输报文啦，客户端用   &lt;strong&gt;对称密钥加密请求报文&lt;/strong&gt;，   &lt;strong&gt;服务端用对应的对称密钥解密报文&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;有时候，接口的安全性，还包括  &lt;strong&gt;手机号、身份证等信息的脱敏&lt;/strong&gt;。就是说，  &lt;strong&gt;用户的隐私数据，不能随便暴露&lt;/strong&gt;。&lt;/p&gt;
 &lt;h2&gt;28.分布式事务，如何保证&lt;/h2&gt;
 &lt;blockquote&gt;
  &lt;p&gt;分布式事务：就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单来说，分布式事务指的就是分布式系统中的事务，它的存在就是为了保证不同数据库节点的数据一致性。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;分布式事务的几种解决方案：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;2PC(二阶段提交)方案、3PC&lt;/li&gt;
  &lt;li&gt;TCC（Try、Confirm、Cancel）&lt;/li&gt;
  &lt;li&gt;本地消息表&lt;/li&gt;
  &lt;li&gt;最大努力通知&lt;/li&gt;
  &lt;li&gt;seata&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;大家可以看下这篇文章哈：  &lt;a href="https://mp.weixin.qq.com/s/3r9MfIz2RAtdFhYzwwZxjA"&gt;看一遍就理解：分布式事务详解&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;29. 事务失效的一些经典场景&lt;/h2&gt;
 &lt;p&gt;我们的接口开发过程中，经常需要使用到事务。所以需要避开事务失效的一些经典场景。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;方法的访问权限必须是public，其他private等权限，事务失效&lt;/li&gt;
  &lt;li&gt;方法被定义成了final的，这样会导致事务失效。&lt;/li&gt;
  &lt;li&gt;在同一个类中的方法直接内部调用，会导致事务失效。&lt;/li&gt;
  &lt;li&gt;一个方法如果没交给spring管理，就不会生成spring事务。&lt;/li&gt;
  &lt;li&gt;多线程调用，两个方法不在同一个线程中，获取到的数据库连接不一样的。&lt;/li&gt;
  &lt;li&gt;表的存储引擎不支持事务&lt;/li&gt;
  &lt;li&gt;如果自己try...catch误吞了异常，事务失效。&lt;/li&gt;
  &lt;li&gt;错误的传播特性&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;推荐大家看下这篇文章：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247494570&amp;idx=2&amp;sn=17357bcd328b2d1d83f4a72c47daac1b&amp;chksm=cf223483f855bd95351a778d5f48ddd37917ce2790ebbbcd1d6ee4f27f7f4b147f0d41101dcc&amp;token=2044040586&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;聊聊spring事务失效的12种场景，太坑了&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;30. 掌握常用的设计模式&lt;/h2&gt;
 &lt;p&gt;把代码写好，还是需要熟练常用的设计模式，比如策略模式、工厂模式、模板方法模式、观察者模式等等。设计模式，是代码设计经验的总结。使用设计模式可以可重用代码、让代码更容易被他人理解、保证代码可靠性。&lt;/p&gt;
 &lt;p&gt;我之前写过一篇总结工作中常用设计模式的文章，写得挺不错的，大家可以看下：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247495616&amp;idx=1&amp;sn=e74c733d26351eab22646e44ea74d233&amp;chksm=cf2230e9f855b9ffe1ddb9fe15f72a273d5de02ed91cc97f3066d4162af027299718e2bf748e&amp;token=1260947715&amp;lang=zh_CN#rd"&gt;实战！工作中常用到哪些设计模式&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;31. 写代码时，考虑线性安全问题&lt;/h2&gt;
 &lt;p&gt;在  &lt;strong&gt;高并发&lt;/strong&gt;情况下，  &lt;code&gt;HashMap&lt;/code&gt;可能会出现死循环。因为它是非线性安全的，可以考虑使用  &lt;code&gt;ConcurrentHashMap&lt;/code&gt;。所以这个也尽量养成习惯，不要上来反手就是一个  &lt;code&gt;new HashMap()&lt;/code&gt;;&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;ul&gt;
   &lt;li&gt;Hashmap、Arraylist、LinkedList、TreeMap等都是线性不安全的；&lt;/li&gt;
   &lt;li&gt;Vector、Hashtable、ConcurrentHashMap等都是线性安全的&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1ba0cab945874264a8d8e87b7d7c4a1b~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;32.接口定义清晰易懂，命名规范。&lt;/h2&gt;
 &lt;p&gt;我们写代码，不仅仅是为了实现当前的功能，也要有利于后面的维护。说到维护，代码不仅仅是写给自己看的，也是给别人看的。所以接口定义要清晰易懂，命名规范。&lt;/p&gt;
 &lt;h2&gt;33. 接口的版本控制&lt;/h2&gt;
 &lt;p&gt;接口要做好版本控制。就是说，请求基础报文，应该包含  &lt;code&gt;version&lt;/code&gt;接口版本号字段，方便未来做接口兼容。其实这个点也算接口扩展性的一个体现点吧。&lt;/p&gt;
 &lt;p&gt;比如客户端APP某个功能优化了，新老版本会共存，这时候我们的  &lt;code&gt;version&lt;/code&gt;版本号就派上用场了，对  &lt;code&gt;version&lt;/code&gt;做升级，做好版本控制。&lt;/p&gt;
 &lt;h2&gt;34. 注意代码规范问题&lt;/h2&gt;
 &lt;p&gt;注意一些常见的代码坏味道：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;大量重复代码（抽公用方法，设计模式）&lt;/li&gt;
  &lt;li&gt;方法参数过多（可封装成一个DTO对象）&lt;/li&gt;
  &lt;li&gt;方法过长（抽小函数）&lt;/li&gt;
  &lt;li&gt;判断条件太多（优化if...else）&lt;/li&gt;
  &lt;li&gt;不处理没用的代码&lt;/li&gt;
  &lt;li&gt;不注重代码格式&lt;/li&gt;
  &lt;li&gt;避免过度设计&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;代码的坏味道，这里我都写到啦：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247490148&amp;idx=1&amp;sn=00a181bf74313f751b3ea15ebc303545&amp;chksm=cf21c54df8564c5bc5b4600fce46619f175f7ae557956f449629c470a08e20580feef4ea8d53&amp;token=162724582&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;25种代码坏味道总结+优化示例&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;35.保证接口正确性，其实就是保证更少的bug&lt;/h2&gt;
 &lt;p&gt;保证接口的正确性，换个角度讲，就是保证更少的bug，甚至是没有bug。所以接口开发完后，一般需要开发  &lt;strong&gt;自测一下&lt;/strong&gt;。然后的话，接口的正确还体现在，多线程并发的时候，  &lt;strong&gt;保证数据的正确性&lt;/strong&gt;,等等。比如你做一笔转账交易，扣减余额的时候，可以通过CAS乐观锁的方式保证余额扣减正确吧。&lt;/p&gt;
 &lt;p&gt;如果你是实现秒杀接口，得防止超卖问题吧。你可以使用Redis分布式锁防止超卖问题。使用Redis分布式锁，有几个注意要点，大家可以看下我之前这篇文章哈：  &lt;a href="https://mp.weixin.qq.com/s?__biz=Mzg3NzU5NTIwNg==&amp;mid=2247488142&amp;idx=1&amp;sn=79a304efae7a814b6f71bbbc53810c0c&amp;chksm=cf21cda7f85644b11ff80323defb90193bc1780b45c1c6081f00da85d665fd9eb32cc934b5cf&amp;token=162724582&amp;lang=zh_CN&amp;scene=21#wechat_redirect"&gt;七种方案！探讨Redis分布式锁的正确使用姿势&lt;/a&gt;&lt;/p&gt;
 &lt;h2&gt;36.学会沟通，跟前端沟通，跟产品沟通&lt;/h2&gt;
 &lt;p&gt;我把这一点放到最后，学会沟通是非常非常重要的。比如你开发定义接口时，  &lt;strong&gt;一定不能上来就自己埋头把接口定义完了&lt;/strong&gt;，  &lt;strong&gt;需要跟客户端先对齐接口&lt;/strong&gt;。遇到一些难点时，跟技术leader对齐方案。实现需求的过程中，有什么问题，及时跟产品沟通。&lt;/p&gt;
 &lt;p&gt;总之就是，开发接口过程中，一定要沟通好~&lt;/p&gt;
 &lt;h2&gt;最后(求关注，别白嫖我)&lt;/h2&gt;
 &lt;p&gt;如果这篇文章对您有所帮助，或者有所启发的话，欢迎关注我的公众号：捡田螺的小男孩&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62260-%E8%AE%BE%E8%AE%A1-%E6%8E%A5%E5%8F%A3</guid>
      <pubDate>Mon, 09 May 2022 00:49:39 CST</pubDate>
    </item>
    <item>
      <title>关于 To B &amp; To C 账号体系的设计问题</title>
      <link>https://itindex.net/detail/62235-to-to-%E4%BD%93%E7%B3%BB</link>
      <description>&lt;h2&gt;  &lt;strong&gt;钉钉账号是个人所有的么？&lt;/strong&gt;&lt;/h2&gt;



 &lt;p&gt;答案是「否」，钉钉账号表面上是你自己的账号，但在企业的逻辑当中，这是「企业授权给你使用的」，本质上，这些都是企业的资产，企业是有权处置这些资源的。你所说的、所看的、所做的，都是企业提供的资产，企业可以根据自己的需求来调整这些资源的所在位置。&lt;/p&gt;



 &lt;h2&gt;  &lt;strong&gt;为什么钉钉和我们使用的微信、QQ 不同？&lt;/strong&gt;&lt;/h2&gt;



 &lt;p&gt;这是钉钉的不同的一点，在钉钉当中，任何一个账号，都是有对应的租户的（除了你的个人租户）。租户才是资源的所有者，我们只不过是使用者。 而我们使用的微信、QQ 其实也不是我们的，在微信的用户协议当中写明了，我们所拥有的不过是微信账号的使用权。只不过，因为腾讯离我们很远，我们可以认为不会对我们执行任何操作（当然，实际上如果你发一些不和谐的内容，也会被设置为内容自见），但在钉钉中，租户的管理员可能就是我们身边的同事，在这种情况会更有「隐私」和企业所有的对比。&lt;/p&gt;



 &lt;blockquote&gt;  &lt;p&gt;7.1.2 微信帐号的所有权归腾讯公司所有，用户完成申请注册手续后，仅获得微信帐号的使用权，且该使用权仅属于初始申请注册人。同时，初始申请注册人不得赠与、借用、租用、转让或售卖微信帐号或者以其他方式许可非初始申请注册人使用微信帐号。非初始申请注册人不得通过受赠、继承、承租、受让或者其他任何方式使用微信帐号。&lt;/p&gt;&lt;/blockquote&gt;



 &lt;p&gt;综上所述，  &lt;strong&gt;你的账号，并不属于你。&lt;/strong&gt;&lt;/p&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category>随笔 产品经理 思考</category>
      <guid isPermaLink="true">https://itindex.net/detail/62235-to-to-%E4%BD%93%E7%B3%BB</guid>
      <pubDate>Sun, 01 May 2022 06:00:00 CST</pubDate>
    </item>
    <item>
      <title>vivo 短视频推荐去重服务的设计实践</title>
      <link>https://itindex.net/detail/62193-vivo-%E8%A7%86%E9%A2%91-%E6%9C%8D%E5%8A%A1</link>
      <description>&lt;h1&gt;一、概述&lt;/h1&gt;
 &lt;h2&gt;1.1 业务背景&lt;/h2&gt;
 &lt;p&gt;vivo短视频在视频推荐时需要对用户已经看过的视频进行过滤去重，避免给用户重复推荐同一个视频影响体验。在一次推荐请求处理流程中，会基于用户兴趣进行视频召回，大约召回2000~10000条不等的视频，然后进行视频去重，过滤用户已经看过的视频，仅保留用户未观看过的视频进行排序，选取得分高的视频下发给用户。&lt;/p&gt;
 &lt;h2&gt;1.2 当前现状&lt;/h2&gt;
 &lt;p&gt;当前推荐去重基于Redis Zset实现，服务端将播放埋点上报的视频和下发给客户端的视频分别以不同的Key写入Redis ZSet，推荐算法在视频召回后直接读取Redis里对应用户的播放和下发记录（整个ZSet），基于内存中的Set结构实现去重，即判断当前召回视频是否已存在下发或播放视频Set中，大致的流程如图1所示。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3425e4b98d7469099517248138491b0~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（图1：短视频去重当前现状）&lt;/p&gt;
 &lt;p&gt;视频去重本身是基于用户实际观看过的视频进行过滤，但考虑到实际观看的视频是通过客户端埋点上报，存在一定的时延，因此服务端会保存用户最近100条下发记录用于去重，这样就保证了即使客户端埋点还未上报上来，也不会给用户推荐了已经看过的视频（即重复推荐）。而下发给用户的视频并不一定会被曝光，因此仅保存100条，使得未被用户观看的视频在100条下发记录之后仍然可以继续推荐。&lt;/p&gt;
 &lt;p&gt;当前方案主要问题是占用Redis内存非常大，因为视频ID是以原始字符串形式存在Redis Zset中，为了控制内存占用并且保证读写性能，我们对每个用户的播放记录最大长度进行了限制，当前限制单用户最大存储长度为10000，但这会影响重度用户产品体验。&lt;/p&gt;
 &lt;h1&gt;二、方案调研&lt;/h1&gt;
 &lt;h2&gt;2.1 主流方案&lt;/h2&gt;
 &lt;p&gt;  &lt;strong&gt;第一，存储形式&lt;/strong&gt;。视频去重场景是典型的只需要判断是否存在即可，因此并不需要把原始的视频ID存储下来，目前比较常用的方案是使用布隆过滤器存储视频的多个Hash值，可降低存储空间数倍甚至十几倍。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;第二，存储介质&lt;/strong&gt;。如果要支持存储90天（三个月）播放记录，而不是当前粗暴地限制最大存储10000条，那么需要的Redis存储容量非常大。比如，按照5000万用户，平均单用户90天播放10000条视频，每个视频ID占内存25B，共计需要12.5TB。视频去重最终会读取到内存中完成，可以考虑牺牲一些读取性能换取更大的存储空间。而且，当前使用的Redis未进行持久化，如果出现Redis故障会造成数据丢失，且很难恢复（因数据量大，恢复时间会很长）。&lt;/p&gt;
 &lt;p&gt;目前业界比较常用的方案是使用磁盘KV（一般底层基于RocksDB实现持久化存储，硬盘使用SSD），读写性能相比Redis稍逊色，但是相比内存而言，磁盘在容量上的优势非常明显。&lt;/p&gt;
 &lt;h2&gt;2.2 技术选型&lt;/h2&gt;
 &lt;p&gt;  &lt;strong&gt;第一，播放记录&lt;/strong&gt;。因需要支持至少三个月的播放历史记录，因此选用布隆过滤器存储用户观看过的视频记录，这样相比存储原始视频ID，空间占用上会极大压缩。我们按照5000万用户来设计，如果使用Redis来存储布隆过滤器形式的播放记录，也将是TB级别以上的数据，考虑到我们最终在主机本地内存中执行过滤操作，因此可以接受稍微低一点的读取性能，选用磁盘KV持久化存储布隆过滤器形式的播放记录。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;第二，下发记录&lt;/strong&gt;。因只需存储100条下发视频记录，整体的数据量不大，而且考虑到要对100条之前的数据淘汰，仍然使用Redis存储最近100条的下发记录。&lt;/p&gt;
 &lt;h1&gt;三、方案设计&lt;/h1&gt;
 &lt;p&gt;基于如上的技术选型，我们计划新增统一去重服务来支持写入下发和播放记录、根据下发和播放记录实现视频去重等功能。其中，重点要考虑的就是接收到播放埋点以后将其存入布隆过滤器。在收到播放埋点以后，以布隆过滤器形式写入磁盘KV需要经过三步，如图2所示：第一，读取并反序列化布隆过滤器，如布隆过滤器不存在则需创建布隆过滤器；第二，将播放视频ID更新到布隆过滤器中；第三，将更新后的布隆过滤器序列化并回写到磁盘KV中。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/72182f0401b543e7a1d1195192375b23~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（图2：统一去重服务主要步骤）&lt;/p&gt;
 &lt;p&gt;整个过程很清晰，但是考虑到需要支持千万级用户量，假设按照5000万用户目标设计，我们还需要考虑四个问题：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;第一&lt;/strong&gt;，视频按刷次下发（一刷5~10条视频），而播放埋点按照视频粒度上报，那么就视频推荐消重而言，数据的写入QPS比读取更高，然而，相比Redis磁盘KV的性能要逊色，磁盘KV本身的写性能比读性能低，要支持5000万用户量级，那么如何实现布隆过滤器写入磁盘KV是一个要考虑的重要问题。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;第二&lt;/strong&gt;，由于布隆过滤器不支持删除，超过一定时间的数据需要过期淘汰，否则不再使用的数据将会一直占用存储资源，那么如何实现布隆过滤器过期淘汰也是一个要考虑的重要问题。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;第三&lt;/strong&gt;，服务端和算法当前直接通过Redis交互，我们希望构建统一去重服务，算法调用该服务来实现过滤已看视频，而服务端基于Java技术栈，算法基于C++技术栈，那么需要在Java技术栈中提供服务给C++技术栈调用。我们最终采用gRPC提供接口给算法调用，注册中心采用了Consul，该部分非重点，就不详细展开阐述。&lt;/p&gt;
&lt;/li&gt;
  &lt;li&gt;
   &lt;p&gt;    &lt;strong&gt;第四&lt;/strong&gt;，切换到新方案后我们希望将之前存储在Redis ZSet中的播放记录迁移到布隆过滤器，做到平滑升级以保证用户体验，那么设计迁移方案也是要考虑的重要问题。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;3.1 整体流程&lt;/h2&gt;
 &lt;p&gt;统一去重服务的整体流程及其与上下游之间的交互如图3所示。服务端在下发视频的时候，将当次下发记录通过统一去重服务的Dubbo接口保存到Redis下发记录对应的Key下，使用Dubbo接口可以确保立即将下发记录写入。同时，监听视频播放埋点并将其以布隆过滤器形式存放到磁盘KV中，考虑到性能我们采用了批量写入方案，具体下文详述。统一去重服务提供RPC接口供推荐算法调用，实现对召回视频过滤掉用户已观看的视频。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9b8ef4889d1642c9a5981c05a8efafda~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（图3：统一去重服务整体流程）&lt;/p&gt;
 &lt;p&gt;磁盘KV写性能相比读性能差很多，尤其是在Value比较大的情况下写QPS会更差，考虑日活千万级情况下磁盘KV写性能没法满足直接写入要求，因此需要设计写流量汇聚方案，即将一段时间以内同一个用户的播放记录汇聚起来一次写入，这样就大大降低写入频率，降低对磁盘KV的写压力。&lt;/p&gt;
 &lt;h2&gt;3.2 流量汇聚&lt;/h2&gt;
 &lt;p&gt;为了实现写流量汇聚，我们需要将播放视频先暂存在Redis汇聚起来，然后隔一段时间将暂存的视频生成布隆过滤器写入磁盘KV中保存，具体而言我们考虑过N分钟仅写入一次和定时任务批量写入两种方式。接下来详细阐述我们在流量汇聚和布隆过滤器写入方面的设计和考虑。&lt;/p&gt;
 &lt;h3&gt;3.2.1 近实时写入&lt;/h3&gt;
 &lt;p&gt;监听到客户端上报的播放埋点后，原本应该直接将其更新到布隆过滤器并保存到磁盘KV，但是考虑到降低写频率，我们只能将播放的视频ID先保存到Redis中，N分钟内仅统一写一次磁盘KV，这种方案姑且称之为近实时写入方案吧。&lt;/p&gt;
 &lt;p&gt;最朴素的想法是每次写的时候，在Redis中保存一个Value，N分钟以后失效，每次监听到播放埋点以后判断这个Value是否存在，如果存在则表示N分钟内已经写过一次磁盘KV本次不写，否则执行写磁盘KV操作。这样的考虑主要是在数据产生时，先不要立即写入，等N分钟汇聚一小批流量之后再写入。这个Value就像一把“锁”，保护磁盘KV每隔N分钟仅被写入一次，如图4所示，如果当前为已加锁状态，再进行加锁会失败，可保护在加锁期间磁盘KV不被写入。从埋点数据流来看，原本连续不断的数据流，经过这把“锁”就变成了每隔N分钟一批的微批量数据，从而实现流量汇聚，并降低磁盘KV的写压力。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/817af5bff1ae4c2498db6b9bb80ec95e~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（图4：近实时写入方案）&lt;/p&gt;
 &lt;p&gt;近实时写入的出发点很单纯，优势也很明显，可以近实时地将播放埋点中的视频ID写入到布隆过滤器中，而且时间比较短（N分钟），可以避免Redis Zset中暂存的数据过长。但是，仔细分析还需要考虑很多特殊的场景，主要如下：&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;   &lt;strong&gt;第一&lt;/strong&gt;，Redis中保存一个Value其实相当于一个分布式锁，实际上很难保证这把“锁”是绝对安全的，因此可能会存在两次收到播放埋点均认为可以进行磁盘KV写操作，但这两次读到的暂存数据不一定一样，由于磁盘KV不支持布隆过滤器结构，写入操作需要先从磁盘KV中读出当前的布隆过滤器，然后将需要写入的视频ID更新到该布隆过滤器，最后再写回到磁盘KV，这样的话，写入磁盘KV后就有可能存在数据丢失。&lt;/p&gt;
  &lt;p&gt;   &lt;strong&gt;第二&lt;/strong&gt;，最后一个N分钟的数据需要等到用户下次再使用的时候才能通过播放埋点触发写入磁盘KV，如果有大量不活跃的用户，那么就会存在大量暂存数据遗留在Redis中占用空间。此时，如果再采用定时任务来将这部分数据写入到磁盘KV，那么也会很容易出现第一种场景中的并发写数据丢失问题。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;如此看来，近实时写入方案虽然出发点很直接，但是仔细想来，越来越复杂，只能另寻其他方案。&lt;/p&gt;
 &lt;h3&gt;3.2.2 批量写入&lt;/h3&gt;
 &lt;p&gt;既然近实时写入方案复杂，那不妨考虑简单的方案，通过定时任务批量将暂存的数据写入到磁盘KV中。我们将待写的数据标记出来，假设我们每小时写入一次，那么我们就可以把暂存数据以小时值标记。但是，考虑到定时任务难免可能会执行失败，我们需要有补偿措施，常见的方案是每次执行任务的时候，都在往前多1~2个小时的数据上执行任务，以作补偿。但是，明显这样的方案并不够优雅，我们从时间轮得到启发，并基于此设计了布隆过滤器批量写入的方案。&lt;/p&gt;
 &lt;p&gt;我们将小时值首尾相连，从而得到一个环，并且将对应的数据存在该小时值标识的地方，那么同一小时值（比如每天11点）的数据是存在一起的，如果今天的数据因任务未执行或执行失败未同步到磁盘KV，那么在第二天将会得到一次补偿。&lt;/p&gt;
 &lt;p&gt;顺着这个思路，我们可以将小时值对某个值取模以进一步缩短两次补偿的时间间隔，比如图5所示对8取模，可见1:00~2:00和9:00~10:00的数据都会落在图中时间环上的点1标识的待写入数据，过8个小时将会得到一次补偿的机会，也就是说这个取模的值就是补偿的时间间隔。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cefa551f48cc40a1bd45b09cf0c28008~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（图5：批量写入方案）&lt;/p&gt;
 &lt;p&gt;那么，我们应该将补偿时间间隔设置为多少呢？这是一个值得思考的问题，这个值的选取会影响到待写入数据在环上的分布。我们的业务一般都会有忙时、闲时，忙时的数据量会更大，根据短视频忙闲时特点，最终我们将补偿间隔设置为6，这样业务忙时比较均匀地落在环上的各个点。&lt;/p&gt;
 &lt;p&gt;确定了补偿时间间隔以后，我们觉得6个小时补偿还是太长了，因为用户在6个小时内有可能会看过大量的视频，如果不及时将数据同步到磁盘KV，会占用大量Redis内存，而且我们使用Redis ZSet暂存用户播放记录，过长的话会严重影响性能。于是，我们设计每个小时增加一次定时任务，第二次任务对第一次任务补偿，如果第二次任务仍然没有补偿成功，那么经过一圈以后，还可以得到再次补偿（兜底）。&lt;/p&gt;
 &lt;p&gt;细心一点应该会发现在图5中的“待写入数据”和定时任务并不是分布在环上的同一个点的，我们这样设计的考虑是希望方案更简单，定时任务只会去操作已经不再变化的数据，这样就能避免并发操作问题。就像Java虚拟机中垃圾回收一样，我们不能一边回收垃圾，一边却还在同一间屋子里扔着垃圾。所以，设计成环上节点对应定时任务只去处理前一个节点上的数据，以确保不会产生并发冲突，使方案保持简单。&lt;/p&gt;
 &lt;p&gt;批量写入方案简单且不存在并发问题，但是在Redis Zset需要保存一个小时的数据，可能会超过最大长度，但是考虑到现实中一般用户一小时内不会播放非常大量的视频，这一点是可以接受的。最终，我们选择了批量写入方案，其简单、优雅、高效，在此基础上，我们需要继续设计暂存大量用户的播放视频ID方案。&lt;/p&gt;
 &lt;h2&gt;3.3 数据分片&lt;/h2&gt;
 &lt;p&gt;为了支持5000万日活量级，我们需要为定时批量写入方案设计对应的数据存储分片方式。首先，我们依然需要将播放视频列表存放在Redis Zset，因为在没写入布隆过滤器之前，我们需要用这份数据过滤用户已观看过的视频。正如前文提到过，我们会暂存一个小时的数据，正常一个用户一个小时内不会播放超过一万条数据的，所以一般来说是没有问题的。除了视频ID本身以外，我们还需要保存这个小时到底有哪些用户产生过播放数据，否则定时任务不知道要将哪些用户的播放记录写入布隆过滤器，存储5000万用户的话就需要进行数据分片。&lt;/p&gt;
 &lt;p&gt;结合批量同步部分介绍的时间环，我们设计了如图6所示的数据分片方案，将5000万的用户Hash到5000个Set中，这样每个Set最多保存1万个用户ID，不至于影响Set的性能。同时，时间环上的每个节点都按照这个的分片方式保存数据，将其展开就如同图6下半部分所示，以played:user:${时间节点编号}:${用户Hash值}为Key保存某个时间节点某个分片下所有产生了播放数据的用户ID。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a4ccf4fd9f6640619f1b7906c5985ef1~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（图6：数据分片方案）&lt;/p&gt;
 &lt;p&gt;对应地，我们的定时任务也要进行分片，每个任务分片负责处理一定数目的数据分片。否则，如果两者一一对应的话，将分布式定时任务分成5000个分片，虽然对于失败重试是更好的，但是对于任务调度来说会存在压力，实际上公司的定时任务也不支持5000分分片。我们将定时任务分为了50个分片，任务分片0负责处理数据分片0~100，任务分片1负责处理数据分片100~199，以此类推。&lt;/p&gt;
 &lt;h2&gt;3.4 数据淘汰&lt;/h2&gt;
 &lt;p&gt;对于短视频推荐去重业务场景，我们一般保证让用户在看过某条视频后三个月内不会再向该用户推荐这条视频，因此就涉及到过期数据淘汰问题。布隆过滤器不支持删除操作，因此我们将用户的播放历史记录添加到布隆过滤器以后，按月存储并设置相应的过期时间，如图7所示，目前过期时间设置为6个月。在数据读取的时候，根据当前时间选择读取最近4个月数据用于去重。之所以需要读取4个月的数据，是因为当月数据未满一个月，为了保证三个月内不会再向用户重复推荐，需要读取三个完整月和当月数据。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/441ecced4d97418d87fd060485d06de6~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（图7：数据淘汰方案）&lt;/p&gt;
 &lt;p&gt;对于数据过期时间的设置我们也进行了精心考虑，数据按月存储，因此新数据产生时间一般在月初，如果仅将过期时间设置为6个月以后，那么会造成月初不仅产生大量新数据，也需要淘汰大量老数据，对数据库系统造成压力。所以，我们将过期时间进行了打散，首先随机到6个月后的那个月任意一天，其次我们将过期时间设置在业务闲时，比如：00:00~05:00，以此来降低数据库清理时对系统的压力。&lt;/p&gt;
 &lt;h2&gt;3.5 方案小结&lt;/h2&gt;
 &lt;p&gt;通过综合上述流量汇聚、数据分片和数据淘汰三部分设计方案，整体的设计方案如图8所示，从左至右播放埋点数据依次从数据源Kafka流向Redis暂存，最终流向磁盘KV持久化。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/37bfce6a96384faa89630f479ccb5c39~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（图8：整体方案流程）&lt;/p&gt;
 &lt;p&gt;首先，从Kafka播放埋点监听到数据以后，我们根据用户ID将该条视频追加到用户对应的播放历史中暂存，同时根据当前时间和用户ID的Hash值确定对应时间环，并将用户ID保存到该时间环对应的用户列表中。然后，每个分布式定时任务分片去获取上一个时间环的播放用户数据分片，再获取用户的播放记录更新到读出的布隆过滤器，最后将布隆顾虑其序列化后写入磁盘KV中。&lt;/p&gt;
 &lt;h1&gt;四、数据迁移&lt;/h1&gt;
 &lt;p&gt;为了实现从当前基于Redis ZSet去重平滑迁移到基于布隆过滤器去重，我们需要将统一去重服务上线前用户产生的播放记录迁移过来，以保证用户体验不受影响，我们设计和尝试了两种方案，经过对比和改进形成了最终方案。&lt;/p&gt;
 &lt;p&gt;我们已经实现了批量将播放记录原始数据生成布隆过滤器存储到磁盘KV中，因此，迁移方案只需要考虑将存储在原来Redis中的历史数据（去重服务上线前产生）迁移到新的Redis中即可，接下来就交由定时任务完成即可，方案如图9所示。用户在统一去重服务上线后新产生的增量数据通过监听播放埋点写入，新老数据双写，以便需要时可以降级。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/36a21f0574d34c6eac622548ff6daedd~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（图9：迁移方案一）&lt;/p&gt;
 &lt;p&gt;但是，我们忽略了两个问题：第一，新的Redis仅用作暂存，因此比老的Redis容量小很多，没法一次性将数据迁移过去，需要分多批迁移；第二，迁移到新的Redis后的存储格式和老的Redis不一样，除了播放视频列表，还需要播放用户列表，咨询DBA得知这样迁移比较难实现。&lt;/p&gt;
 &lt;p&gt;既然迁移数据比较麻烦，我们就考虑能不能不迁移数据呢，在去重的时候判断该用户是否已迁移，如未迁移则同时读取一份老数据一起用于去重过滤，并触发将该用户的老数据迁移到新Redis（含写入播放用户列表），三个月以后，老数据已可过期淘汰，此时就完成了数据迁移，如图10所示。这个迁移方案解决了新老Redis数据格式不一致迁移难的问题，而且是用户请求时触发迁移，也避免了一次性迁移数据对新Redis容量要求，同时还可以做到精确迁移，仅迁移了三个月内需要迁移数据的用户。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d389c23d74fd4cefa824d80dd61ae9c6~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（图10：迁移方案二）&lt;/p&gt;
 &lt;p&gt;于是，我们按照方案二进行了数据迁移，在上线测试的时候，发现由于用户首次请求的时候需要去迁移老的数据，造成去重接口耗时不稳定，而视频去重作为视频推荐重要环节，对于耗时比较敏感，所以就不得不继续思考新的迁移方案。我们注意到，在定时批量生成布隆过滤器的时候，读取到时间环对应的播放用户列表后，根据用户ID获取播放视频列表，然后生成布隆过滤器保存到磁盘KV，此时，我们只需要增加一个从老Redis读取用户的历史播放记录即可把历史数据迁移过来。为了触发将某个用户的播放记录生成布隆过滤器的过程，我们需要将用户ID保存到时间环上对应的播放用户列表，最终方案如图11所示。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#22270;&amp;#29255;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/526f6335f1e2481281b1d6e543b33072~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;（图11：最终迁移方案）&lt;/p&gt;
 &lt;p&gt;首先，DBA帮助我们把老Redis中播放记录的Key（含有用户ID）都扫描出来，通过文件导出；然后，我们通过大数据平台将导出的文件导入到Kafka，启用消费者监听并消费文件中的数据，解析后将其写入到当前时间环对应的播放用户列表。接下来，分布式批量任务在读取到播放用户列表中的某个用户后，如果该用户未迁移数据，则从老Redis读取历史播放记录，并和新的播放记录一起更新到布隆过滤器并存入磁盘KV。&lt;/p&gt;
 &lt;h1&gt;五、小结&lt;/h1&gt;
 &lt;p&gt;本文主要介绍短视频基于布隆过滤器构建推荐去重服务的设计与思考，从问题出发逐步设计和优化方案，力求简单、完美、优雅，希望能对读者有参考和借鉴价值。由于文章篇幅有限，有些方面未涉及，也有很多技术细节未详细阐述，如有疑问欢迎继续交流。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;作者：vivo互联网服务器团队-Zhang Wei&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62193-vivo-%E8%A7%86%E9%A2%91-%E6%9C%8D%E5%8A%A1</guid>
      <pubDate>Wed, 06 Apr 2022 01:34:55 CST</pubDate>
    </item>
    <item>
      <title>用户标签体系的设计和效果评估</title>
      <link>https://itindex.net/detail/62134-%E7%94%A8%E6%88%B7-%E6%A0%87%E7%AD%BE-%E4%BD%93%E7%B3%BB</link>
      <description>&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;/p&gt; &lt;p&gt;用户标签是构成用户画像的核心因素，是将用户在平台内所产生的业务数据,行为数据，日志数据等分析提炼后生成具有差异性特征的形容词。即用户通过平台，在什么时间什么场景下做了什么行为，平台将用户所有行为数据提炼出来形成支撑业务实现的可视化信息。&lt;/p&gt; &lt;p&gt;用户标签可以有很多种存在形式，可以是用户的自然属性，可以是对用户交易、资产数据的统计指标，也可以是基于某些规则，总结出的一些分层。无论是哪种形式，都是对用户的某个维度特征做描述与刻画，让使用者能快速获取信息。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;个人认为，好的标签需要具备如下四点特征：&lt;/p&gt; &lt;p&gt;1.    原子性。即用户标签是用户画像特征刻画的最细粒度。&lt;/p&gt; &lt;p&gt;2.    可复用性。标签可以被多次使用，而非一次性标签&lt;/p&gt; &lt;p&gt;3.    可度量性。标签值和价值可被度量和计算。&lt;/p&gt; &lt;p&gt;4.    可组合性。标签可被自由组合生成组合标签。&lt;/p&gt; &lt;p&gt; &lt;/p&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;1. 从更新频率来分：静态标签、动态标签&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;例如“性别”这个标签，一般来说是不会随着时间变动的，所以它属于静态标签；而“最近一次访问时间”会随着每次用户登录而更新，也就是动态标签。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;2. 从开发方式分：事实标签、规则标签、预测标签&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;这一种分类方式是从技术开发角度区分的。&lt;/p&gt; &lt;p&gt;“事实标签”是从底层数据表中取出原始数据，进行简单的加减乘除运算得到的标签；例如“最近一次登录距今天数”这个标签，它反映基本事实。&lt;/p&gt; &lt;p&gt;“规则标签”则是进行了业务定义后的标签；例如“流失用户”这个标签，基于我们的业务认知，可以将“最近一次登录距今天数”大于30天的用户定义为流失用户，不同公司会有自己的定义方式。&lt;/p&gt; &lt;p&gt; “预测标签”，是需要利用算法分析预测才能得到的标签了；例如电商产品常通过用户的下单行为，去猜测用户的性别；通常算法类标签涉及复杂的逻辑与权重，开发难度大，在所有标签中占比不高。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;3. 从生成规则分：单一标签、复合标签&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;一般来说，上述的统计类标签可以说是单一标签，而规则类和算法类标签就是需要多个单一标签组合而成的复合标签&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;strong&gt;4. 从层级上分：一级标签、二级标签、三级标签等&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;同样，层级也是为了业务理解更加有序才产生的，例如一级标签是大类，按具体行业和业务可以分为：人口属性，行为属性，营销属性，商业属性等。二级标签可以具体下分，比如商业属性下二级标签可以分为优惠券，三级标签分为优惠券-敏感度高/中/低用户。当然，如果业务逻辑复杂，可能还会有三级标签。&lt;/p&gt; &lt;p&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;为什么要建设用户标签体系？&lt;/strong&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;以作者多年从业经历来看，一般的中大型公司或多或少都已经有建设自己的标签，但实际使用效果却差强人意，很难驱动业务产生价值。我总结出互联网行业搭建统一的用户标签体系要解决的常见痛点：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;strong&gt;1.    &lt;/strong&gt;  &lt;strong&gt;标签口径不一致&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;用户画像、精准营销平台人群圈选、算法特征都会涉及到用户标签，各个系统存在标签同义不同值、同值不同义的问题，举个例子：互金信贷行业的通过率，就有至少三种不同的统计口径，风控部门是以授信通过或者审核通过为准，财务部门以放款为准等。不同部门因侧重点不一样导致对这个指标的定义不一样。企业建设统一的标签平台规范口径也是数据中台的重要内容。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;strong&gt;2.    &lt;/strong&gt;  &lt;strong&gt;标签指标重复建设&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;用户标签分散，重复建设，难以统一管理。形成了局部数据孤岛，存在重复建设的问题。&lt;/p&gt; &lt;p&gt;比如和标签生产相关的团队就有好几个：数据团队模型开发人员要做自己的模型变量标签，存在多个模型工程师重复建设同一标签而产生大量同质的标签表；数据分析团队归纳业务需求总结出来的标签，比如用户生命周期标签，若分析团队位于不同部门则重复建设情况更为严重，再加上技术开发同学做的营销平台，消息系统，优惠券平台等需要打常规的用户标签来选人等等。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;strong&gt;3.    &lt;/strong&gt;  &lt;strong&gt;标签生产周期长&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;互联网公司的标签生产流程大抵如下：&lt;/p&gt; &lt;p&gt;业务提标签需求—&amp;gt;数据对接人（一般是数据PM or 分析师）收集转化—&amp;gt; 提交给数据开发（离线开发与实时开发） —&amp;gt; 数据开发按业务逻辑清洗数据，导入平台系统—&amp;gt; 后台开发 做成数据服务统一对外输出标签。&lt;/p&gt; &lt;p&gt;一般如果没有做标签上线流程的配置化，此时还需要前端开发介入，整个流程耗时长，平均需求产生到上线耗时一周以上甚至更长时间，和同行朋友聊，有些国有企业生产常规运营标签耗时竟然可以达到1个月，这样的生产流程根本无法满足业务快速发展的需求&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;strong&gt;4.    &lt;/strong&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;运营提选人需求-&amp;gt;分析师提数—&amp;gt;风控人员规则过滤用户—&amp;gt; 运营手动分组&lt;/p&gt; &lt;p&gt;运营将名单导入到营销系统 —&amp;gt; 选择触达方式（消息/优惠券等）和触达周期（一次性/周期性/实时等） -&amp;gt; 触达用户&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;活动后：&lt;/strong&gt;运营将名单再次给分析师 —&amp;gt; 分析师提数给运营—&amp;gt; 运营分析活动效果&lt;/p&gt; &lt;p&gt;  &lt;strong&gt; &lt;/strong&gt;&lt;/p&gt; &lt;p&gt;这里面存在很多拍脑袋决策的节点，比如运营圈人规则看不到人群数量，容易出现圈定人群样本量过少无法进行营销活动；运营在看不到人群画像和分布的情况下，手动盲目对人群进行分组AB Test，容易导致AB Test结论不可靠。活动效果分析没有横向和纵向对比，无法客观得出活动到底做的怎么样。当然这里面还存在诸多手工操作的地方和维护困难的地方，比如每次圈人过风控规则，圈人后手工导入营销系统，手动将名单到给分析师提数做效果分析等。&lt;/p&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;/p&gt; &lt;p&gt;  &lt;strong&gt;核心原则：从业务中来，到业务中去；以终为始，怎么用来倒推怎么设计&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;任何脱离业务自造的标签都是自嗨，这也是很多大公司数据部门容易犯的错，数据部门想要从数据层面去驱动业务，基于自身过往从业经验，拍脑袋梳理和设计了上百个标签，却发现业务根本不买单。数据部门价值体现的唯一方式就是融入业务团队，知道业务来龙去脉和痛点。总结下来正确的顺序是明确商业目的，梳理业务流程，收集业务痛点，汇集整理标签，最后才是开发标签反哺业务。基于作者多年经验，如何设计标签可以归纳为以下两种方法：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;strong&gt;方法一：基于业务主流程来设计标签&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;以信贷行业为例，梳理后信贷业务主流程如下：&lt;/p&gt; &lt;p&gt;激活 —&amp;gt;注册—&amp;gt;登录—&amp;gt;认证—&amp;gt;申请进件—&amp;gt;风控—&amp;gt;放款—&amp;gt;还款—&amp;gt; 逾期催收&lt;/p&gt; &lt;p&gt;以激活到注册流程为例，为精准化识别用户渠道及后续做渠道成本结构优化，我们这个环节可能需要的标签是注册渠道,获客渠道,渠道类型,结算类型,获客成本,注册设备等&lt;/p&gt; &lt;p&gt;再以申请进件到风控流程为例，结合流程中常见的业务场景，可能需要的标签：首次/最近一次申请时间/产品/额度/是否通过，总申请次数/金额，拒绝次数/放弃次数，通过类型（人工/系统自动）等&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;方法二：基于业务场景来设计标签&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;以典型运营场景为例，信贷业务主要靠老户复贷挣钱，促老户复贷是经常会做的一个运营活动，思考活动运营的三个要素（活动对象，在什么场景，执行什么策略），我们需要的标签可能是用户类型（新老户），最近一次成功还款时间/金额，最近一次借款产品，产品偏好，优惠券敏感度/响应度，额度敏感度/响应度 等等&lt;/p&gt; &lt;p&gt;  &lt;br /&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;方法三：基于北极星指标自顶向下设计标签&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;一般公司每年会基于大的战略方向制定公司整体的北极星指标（指引业务发展的指标），然后基于整体业务指标自顶向下拆分到各业务部门，各业务部门再根据运营策略拆解成更细的指标。&lt;/p&gt; &lt;p&gt;举个例子，某信贷公司制定当年度北极星指标为：利润，注册量，放款量，逾期率。其中利润为主指标，其他三个指标围绕利润指标进行平衡。想提升利润核心是提升放款量，但提升放款量会带来获客成本上升以及坏账成本上升，所以这是三者的平衡。我们先来拆解下利润指标：&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;利润 = 收入 – 成本&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;收入 = 放款人数 * 人均放款金额  *  收益率&lt;/p&gt; &lt;p&gt;成本 = 获客成本 + 坏账成本&lt;/p&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;p&gt;下面我们看看成本指标，信贷公司最大的成本在于两块：获客成本和坏账成本&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;降低获客成本&lt;/strong&gt;：本质上需要接入更多优质渠道以及优化CPA/CPS结算的转化率，基于此这里涉及到的标签是 注册时间,注册渠道,获客渠道,渠道类型,结算类型,获客成本,注册设备等&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;降低坏账&lt;/strong&gt;：本质上是对逾期用户进行管理，需要很多贷款信息标签和逾期信息标签，比如累计逾期金额，累计逾期笔数，最近一次逾期时间，最长逾期时间等等&lt;/p&gt; &lt;p&gt;此外，设计一个好的用户标签平台还需要考虑如下特征：&lt;/p&gt; &lt;p&gt;1.      &lt;strong&gt;数据和业务团队双赢策略&lt;/strong&gt;— 标签生成自助化&lt;/p&gt; &lt;p&gt;让使用方自助生成标签是数据团队和业务团队双赢的策略，即提高了业务团队运营的效率，解决了标签的业务字段逻辑沟通的成本，同时释放了数据团队开发标签维护标签的工作。标签生成自助化前期开发成本较高，适用于在中期上线第一版后再来落地。具体如何设计自助化打标功能，可以在后续文章中逐一分享。&lt;/p&gt; &lt;p&gt;2.      &lt;strong&gt;标签系统价值的可持续性&lt;/strong&gt;— 建立有效的标签管理维护机制&lt;/p&gt; &lt;p&gt;标签的维护包括标签规则及元信息维护，标签生产调度机制及信息同步，有统一的输出接口。&lt;/p&gt; &lt;p&gt;这是持续释放用户标签平台的重要步骤，也是容易被忽视的环节。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;3.    &lt;/strong&gt;  &lt;strong&gt;标签平台的运营&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;标签平台是数据产品，既然是产品就需要做运营，让我们的用户更好更高效的使用起来。 及时关注用户反馈，经常做一些运营手段来触发用户，让产品和用户交互起来。这里引申出一个更大的话题：如何做数据产品的运营？这个话题后续再逐步分享。&lt;/p&gt; &lt;p&gt; &lt;/p&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;p&gt;对标签的质量进行科学完整地评估，有助于控制标签质量，指导标签的管理者、开发者不断地提升标签质量。通过创建一套完整的评估体系，对于质量过差的标签，可以考虑不进行上线，等达到基本的质量要求后才能开放给业务使用。不然，既对业务带来不了价值，也容易让标签画像系统失去用户的信任。&lt;/p&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;1.    &lt;/strong&gt;  &lt;strong&gt;数据层面&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;一般使用三个指标：  &lt;strong&gt;覆盖度，准确度，稳定性&lt;/strong&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;1）  &lt;strong&gt;覆盖度&lt;/strong&gt;是指在一个标签中，有业务含义的人群数量与总人群数量的比例。&lt;/p&gt; &lt;p&gt;    举个例子：【优惠券敏感度】标签，全量用户是100万的规模，其中20万打上了“高”标签，20万打上了“中”标签，30万打上了“低”标签，其他30万人都没有打上任何标签。那么，【优惠券敏感度】标签的覆盖度就是70%。这个覆盖度还算是可以，如果覆盖度过低可能会有下面的负面影响：&lt;/p&gt; &lt;p&gt;用标签进行人群圈选的时候，人数过少，无法满足运营活动对样本量的最低要求；&lt;/p&gt; &lt;p&gt;用标签统计平台用户的特征时，和真实情况会有统计偏差，即样本无法代表整体。&lt;/p&gt; &lt;p&gt;一般而言，用户自己填的标签和模型算法打出来的标签，覆盖度会偏低。&lt;/p&gt; &lt;p&gt;2）  &lt;strong&gt;准确度&lt;/strong&gt;是指给用户打的标签中，准确反映事实的人群数量与总人群数量的比例。&lt;/p&gt; &lt;p&gt;举例子：【性别】标签，总用户100万，真实情况是男60万，女40万，系统打标成男50万，女30万，其他20万 根据交叉矩阵，真实是男且标签是男用户40万，真实是女且标签为女用户25万，则标签准确率为  （40 + 25）/ 80 = 81.25%&lt;/p&gt; &lt;p&gt; 真实情况是现实世界标签的准确度往往是很难评估的。一般会用一些外围样本数据来辅助验证，比如对于性别标签，可以抽样让客服电话调研拿到真实性别数据，通过样本来估算整体。&lt;/p&gt; &lt;p&gt;3）  &lt;strong&gt;稳定性&lt;/strong&gt;是指给用户打的标签中，能在指定时间点前被准确计算出来的次数比例。&lt;/p&gt; &lt;p&gt;举个例子，信贷行业中的关联指标【通讯录中近30天有借款逾期人员的比例】，这类指标需要计算几个亿的通讯录表，和业务表关联好几次，计算复杂度高，高峰时期容易跑不出来。 稳定性标签还要根据各标签的计算复杂度来综合评估，一般静态类标签稳定性比较高，算法预测类标签复杂计算逻辑或者关联上下游表比较多的标签在特殊情况下稳定性会差一些。一般而言，稳定性要达到99%以上才能被业务接受，关键时刻不能掉链子。&lt;/p&gt; &lt;p&gt;  &lt;strong&gt; &lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;2.    &lt;/strong&gt;  &lt;strong&gt;应用层面&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt; &lt;/strong&gt;&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;1)    &lt;/strong&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;/p&gt; &lt;p&gt;举个例子：标签产品目标用户（产品，运营）共计100人，知道该产品的用户80人，则触达率为 80%&lt;/p&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;2）标签使用度&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;使用度可以综合从以下几个指标评估，包括 使用次数，使用热度，服务调用次数。可考虑人均聚合或者阶段汇总聚合。对于应用使用度低的标签，可以针对性地进行分析，不断提升每个标签的使用价值。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;strong&gt;3.    &lt;/strong&gt;  &lt;strong&gt;业务层面&lt;/strong&gt;&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;业务价值是业务人员对标签系统的主要考核价值。标签系统业务层面的应用很广泛，从精准营销，精细化运营到个性化推荐，广告匹配系统，BI系统。以精准营销平台为例，一般业务价值可以从降本增效来考虑，比如营销成本降低，营销频次提高，营销人效提升等角度来衡量。&lt;/p&gt; &lt;p&gt;参考指标：&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;strong&gt;营销成本降低&lt;/strong&gt;：以前运营圈人活动平均响应3天 -&amp;gt; 现在0.5天&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;覆盖场景数提升&lt;/strong&gt;：以前一周内覆盖50%运营场景 —&amp;gt; 一周内覆盖90%运营场景&lt;/p&gt; &lt;p&gt;  &lt;strong&gt;触达用户数提升&lt;/strong&gt;：每日触达2万用户 —&amp;gt; 每日可触达10万用户&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;strong&gt;营销ROI提升：&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;另外一个比较好的指标就是业务运营的ROI，业务如果用了一个标签，对一群人进行了投放，ROI是日常投放的好几倍，那这个标签的价值可以说是毋庸置疑了。这时，我们可以说这个标签的业务价值很高。标签系统实际上可以大幅降低了业务运营的成本，导致整体ROI提升，这需要和业务配合一起做评估。比如有个同类活动在使用标签系统前的ROI和使用后的ROI对比，更会彰显标签系统的价值。&lt;/p&gt; &lt;p&gt;如果能找到一些和业务核心KPI直接挂钩的评估手段，那会更加彰显标签平台的重要性。&lt;/p&gt; &lt;p&gt;这里有个问题：如何去准确统计这些指标，需要数据同事和业务同事沟通敲定&lt;/p&gt; &lt;p&gt;标签体系的业务价值衡量，确实是个难点，很难直接评估。而业务向上汇报过程中往往会将“标签平台”价值一带而过，强调“人”和“运营”的重要性而忽视“工具”和“平台”的重要性。这就需要数据同学自己具备业务价值量化评估的能力，一个好的方式是多和业务部门合作，参加业务部门运营活动会议，用数据去影响和驱动业务部门，让业务离不开数据团队，自然业务就会在给大老板的汇报中多多提现“数据标签”的价值。这样才能实现业务和数据团队的双赢局面。&lt;/p&gt; &lt;p&gt; &lt;/p&gt; &lt;p&gt;  &lt;strong&gt;用户标签体系总结&lt;/strong&gt;&lt;/p&gt; &lt;p&gt;总结下全文的创作结构如下：&lt;/p&gt; &lt;p&gt;1.    什么是用户标签体系&lt;/p&gt; &lt;p&gt;2.    为什么要建设用户标签体系&lt;/p&gt; &lt;p&gt;3.    如何设计用户标签体系&lt;/p&gt; &lt;p&gt;4.    如何评估用户标签效果&lt;/p&gt; &lt;p&gt;用户标签体系是个庞大的系统工程，不可能一蹴而就，需要随着业务发展情况而不断迭代完善和丰富。在设计过程中，需要抛弃一上来就大而全的设计理念，根据业务需求场景来逐步落实和丰富标签，毕竟能产生业务价值才是评价标签体系的根本。还要不断研究和学习业界优秀的标签平台（CDP/DMP平台）会给自己设计产品带来一些灵感，比如业界做的比较好的像 神策数据用户画像，腾讯广点通，阿里达摩盘，字节跳动CDP等。&lt;/p&gt; &lt;p&gt;企业在发展的过程中，要依据具体的数据成熟度和数据应用度来衡量是否有必要建立自己的用户标签体系。大厂标配的CDP平台并非适用于所有公司。希望业界朋友都能认识到数据驱动的价值，而数据产品存在的本质也就是 降低企业经营和业务决策的成本，不是吗？&lt;/p&gt; 
            &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62134-%E7%94%A8%E6%88%B7-%E6%A0%87%E7%AD%BE-%E4%BD%93%E7%B3%BB</guid>
      <pubDate>Tue, 01 Mar 2022 11:25:38 CST</pubDate>
    </item>
    <item>
      <title>实战！聊聊幂等设计</title>
      <link>https://itindex.net/detail/62000-%E5%B9%82%E7%AD%89-%E8%AE%BE%E8%AE%A1</link>
      <description>&lt;h2&gt;前言&lt;/h2&gt;
 &lt;p&gt;大家好，我是捡田螺的小男孩。今天我们一起来聊聊幂等设计。&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;什么是幂等&lt;/li&gt;
  &lt;li&gt;为什么需要幂等&lt;/li&gt;
  &lt;li&gt;接口超时，如何处理呢？&lt;/li&gt;
  &lt;li&gt;如何设计幂等？&lt;/li&gt;
  &lt;li&gt;实现幂等的8种方案&lt;/li&gt;
  &lt;li&gt;HTTP的幂等&lt;/li&gt;
&lt;/ol&gt;
 &lt;ul&gt;
  &lt;li&gt;公众号：   &lt;strong&gt;捡田螺的小男孩&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;1. 什么是幂等?&lt;/h2&gt;
 &lt;p&gt;幂等是一个数学与计算机科学概念。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;在数学中，幂等用函数表达式就是：   &lt;code&gt;f(x) = f(f(x))&lt;/code&gt;。比如求绝对值的函数，就是幂等的，   &lt;code&gt;abs(x) = abs(abs(x))&lt;/code&gt;。&lt;/li&gt;
  &lt;li&gt;计算机科学中，幂等表示一次和多次请求某一个资源应该具有同样的副作用，或者说，多次请求所产生的影响与一次请求执行的影响效果相同。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;2. 为什么需要幂等&lt;/h2&gt;
 &lt;p&gt;举个例子：&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;我们开发一个转账功能，假设我们调用下游接口   &lt;strong&gt;超时&lt;/strong&gt;了。一般情况下，   &lt;strong&gt;超时&lt;/strong&gt;可能是   &lt;strong&gt;网络传输丢包&lt;/strong&gt;的问题，也可能是请求时没送到，还有可能是请求到了，   &lt;strong&gt;返回结果却丢&lt;/strong&gt;了。这时候我们是否可以重试呢？如果   &lt;strong&gt;重试&lt;/strong&gt;的话，是否会多转了一笔钱呢？&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;  &lt;img alt="&amp;#36716;&amp;#36134;&amp;#36229;&amp;#26102;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9f3cef955d6a400b87bee3be4265e5df~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;当前互联网的系统几乎都是解耦隔离后，会存在各个不同系统的相互远程调用。调用远程服务会有三个状态：成功，失败，或者超时。前两者都是明确的状态，而超时则是  &lt;strong&gt;未知状态&lt;/strong&gt;。我们转账  &lt;strong&gt;超时&lt;/strong&gt;的时候，如果下游转账系统做好  &lt;strong&gt;幂等&lt;/strong&gt;控制，我们发起  &lt;strong&gt;重试&lt;/strong&gt;，那即可以  &lt;strong&gt;保证转账正常进行，又可以保证不会多转一笔&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;其实除了转账这个例子，日常开发中，还有  &lt;strong&gt;很多很多例子需要考虑幂等&lt;/strong&gt;。比如：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;MQ（消息中间件）消费者读取消息时，有可能会读取到重复消息。（   &lt;strong&gt;重复消费&lt;/strong&gt;）&lt;/li&gt;
  &lt;li&gt;比如提交form表单时，如果快速点击提交按钮，可能产生了两条一样的数据（   &lt;strong&gt;前端重复提交&lt;/strong&gt;）&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;3. 接口超时了，到底如何处理？&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;/li&gt;
&lt;/ul&gt;
 &lt;blockquote&gt;
  &lt;p&gt;拿我们的转账例子来说，转账系统提供一个查询   &lt;strong&gt;转账记录&lt;/strong&gt;的接口，如果   &lt;strong&gt;渠道系统&lt;/strong&gt;调用   &lt;strong&gt;转账系统&lt;/strong&gt;超时时，   &lt;strong&gt;渠道系统&lt;/strong&gt;先去查询一下这笔记录，看下这笔转账记录成功还是失败，如果成功就走成功流程，失败再重试发起转账。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8169d8400af542e79a12ec7eed1f860b~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&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;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5ee3569e84ad4503875d9dc6c7173a5d~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;两种方案都是挺不错的，但是如果是  &lt;strong&gt;MQ重复消费的场景&lt;/strong&gt;，方案一处理并不是很妥，所以，我们还是要求下游系统  &lt;strong&gt;对外接口支持幂等&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a65c2e6c0c0840a982704a0982467771~tplv-k3u1fbpfcp-watermark.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;4. 如何设计幂等&lt;/h2&gt;
 &lt;p&gt;既然这么多场景需要考虑幂等，那我们如何设计幂等呢？&lt;/p&gt;
 &lt;p&gt;幂等意味着一条请求的唯一性。不管是你哪个方案去设计幂等，都需要一个  &lt;strong&gt;全局唯一的ID&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;strong&gt;全局唯一的ID&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;4.1 全局的唯一性ID&lt;/h3&gt;
 &lt;p&gt;全局唯一性ID，我们怎么去生成呢？你可以回想下，数据库主键Id怎么生成的呢？&lt;/p&gt;
 &lt;p&gt;是的，我们可以使用  &lt;code&gt;UUID&lt;/code&gt;，但是UUID的缺点比较明显，它字符串占用的空间比较大，生成的ID过于随机，可读性差，而且没有递增。&lt;/p&gt;
 &lt;p&gt;我们还可以使用  &lt;code&gt;雪花算法（Snowflake）&lt;/code&gt; 生成唯一性ID。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;雪花算法是一种生成分布式全局唯一ID的算法，生成的ID称为   &lt;code&gt;Snowflake IDs&lt;/code&gt;。这种算法由Twitter创建，并用于推文的ID。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;一个Snowflake ID有64位。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;第1位：Java中long的最高位是符号位代表正负，正数是0，负数是1，一般生成ID都为正数，所以默认为0。&lt;/li&gt;
  &lt;li&gt;接下来前41位是时间戳，表示了自选定的时期以来的毫秒数。&lt;/li&gt;
  &lt;li&gt;接下来的10位代表计算机ID，防止冲突。&lt;/li&gt;
  &lt;li&gt;其余12位代表每台机器上生成ID的序列号，这允许在同一毫秒内创建多个Snowflake ID。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;img alt="&amp;#38634;&amp;#33457;&amp;#31639;&amp;#27861;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5cacf71fc911443787f4a339bfd86bda~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;当然，全局唯一性的ID，还可以使用百度的  &lt;code&gt;Uidgenerator&lt;/code&gt;，或者美团的  &lt;code&gt;Leaf&lt;/code&gt;。&lt;/p&gt;
 &lt;h3&gt;4.2 幂等设计的基本流程&lt;/h3&gt;
 &lt;p&gt;幂等处理的过程，说到底其实就是过滤一下已经收到的请求，当然，请求一定要有一个  &lt;code&gt;全局唯一的ID标记&lt;/code&gt;哈。然后，怎么判断请求是否之前收到过呢？把请求储存起来，收到请求时，先查下存储记录，记录存在就返回上次的结果，不存在就处理请求。&lt;/p&gt;
 &lt;p&gt;一般的幂等处理就是这样啦，如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6d7c50899738456baf91c2b8fcbb202e~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h2&gt;5. 实现幂等的8种方案&lt;/h2&gt;
 &lt;p&gt;幂等设计的基本流程都是类似的，我们简简单单来过一下幂等实现的8中方案哈~&lt;/p&gt;
 &lt;h3&gt;5.1 select+insert+主键/唯一索引冲突&lt;/h3&gt;
 &lt;p&gt;日常开发中，为了实现交易接口幂等，我是这样实现的：&lt;/p&gt;
 &lt;p&gt;交易请求过来，我会先根据请求的  &lt;strong&gt;唯一流水号&lt;/strong&gt;   &lt;code&gt;bizSeq&lt;/code&gt;字段，先  &lt;code&gt;select&lt;/code&gt;一下  &lt;strong&gt;数据库的流水表&lt;/strong&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;如果数据已经存在，就拦截是重复请求，直接返回成功；&lt;/li&gt;
  &lt;li&gt;如果数据不存在，就执行   &lt;code&gt;insert&lt;/code&gt;插入，如果   &lt;code&gt;insert&lt;/code&gt;成功，则直接返回成功，如果   &lt;code&gt;insert&lt;/code&gt;产生主键冲突异常，则捕获异常，接着直接返回成功。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;流程图如下&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a6f279f2c152431e804e7083762ada50~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;伪代码如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;/**
 * 幂等处理
 */
Rsp idempotent（Request req）{
  Object requestRecord =selectByBizSeq（bizSeq）;
  
  if(requestRecord !=null){
    //拦截是重复请求
     log.info(&amp;quot;重复请求，直接返回成功，流水号：{}&amp;quot;,bizSeq);
     return rsp;
  }
  
  try{
    insert(req);
  }catch(DuplicateKeyException e){
    //拦截是重复请求，直接返回成功
    log.info(&amp;quot;主键冲突，是重复请求，直接返回成功，流水号：{}&amp;quot;,bizSeq);
    return rsp;
  }
  
  //正常处理请求
  dealRequest(req);
  
  return rsp;
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;为什么前面已经  &lt;code&gt;select&lt;/code&gt;查询了，还需要  &lt;code&gt;try...catch...&lt;/code&gt;捕获重复异常呢？&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;是因为高并发场景下，两个请求去   &lt;code&gt;select&lt;/code&gt;的时候，可能都没查到，然后都走到insert的地方啦。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;当然，用  &lt;strong&gt;唯一索引&lt;/strong&gt;代替  &lt;strong&gt;数据库主键&lt;/strong&gt;也是可以的哈，都是  &lt;strong&gt;全局唯一的ID&lt;/strong&gt;即可。&lt;/p&gt;
 &lt;h3&gt;5.2. 直接insert + 主键/唯一索引冲突&lt;/h3&gt;
 &lt;p&gt;在5.1方案中，都会先查一下  &lt;strong&gt;流水表&lt;/strong&gt;的交易请求，判断是否存在，然后不存在再插入请求记录。如果  &lt;strong&gt;重复请求的概率比较低&lt;/strong&gt;的话，我们可以直接插入请求，利用  &lt;strong&gt;主键/唯一索引冲突&lt;/strong&gt;，去判断是  &lt;strong&gt;重复请求&lt;/strong&gt;。&lt;/p&gt;
 &lt;p&gt;流程图如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/da6dcbb0776b4c9d8d42b013c4b36cc3~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;伪代码如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;/**
 * 幂等处理
 */
Rsp idempotent（Request req）{
  
  try{
    insert(req);
  }catch(DuplicateKeyException e){
     //拦截是重复请求，直接返回成功
    log.info(&amp;quot;主键冲突，是重复请求，直接返回成功，流水号：{}&amp;quot;,bizSeq);
    return rsp;
  }
  
  //正常处理请求
  dealRequest(req);
  return rsp;
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &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;h3&gt;5.3 状态机幂等&lt;/h3&gt;
 &lt;p&gt;很多业务表，都是有状态的，比如转账流水表，就会有  &lt;code&gt;0-待处理，1-处理中、2-成功、3-失败状态&lt;/code&gt;。转账流水更新的时候，都会涉及流水状态更新，即涉及状态机 (即状态变更图)。我们可以利用状态机实现幂等，一起来看下它是怎么实现的。&lt;/p&gt;
 &lt;p&gt;比如转账成功后，把  &lt;strong&gt;处理中&lt;/strong&gt;的转账流水更新为  &lt;strong&gt;成功&lt;/strong&gt;状态，SQL这么写：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;update transfr_flow set status=2 where biz_seq=‘666’ and status=1;
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;  &lt;strong&gt;简要流程图&lt;/strong&gt;如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/00591721e4d04cdabfae905748f9022f~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;伪代码实现如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;Rsp idempotentTransfer（Request req）{
   String bizSeq = req.getBizSeq();
   int rows= &amp;quot;update transfr_flow set status=2 where biz_seq=#{bizSeq} and status=2;&amp;quot;
   if(rows==1){
      log.info(“更新成功,可以处理该请求”);
      //其他业务逻辑处理
      return rsp;
   }else if(rows==0){
      log.info(“更新不成功，不处理该请求”);
      //不处理，直接返回
      return rsp;
   }
   
   log.warn(&amp;quot;数据异常&amp;quot;)
   return rsp：
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;状态机是怎么  &lt;strong&gt;实现幂等&lt;/strong&gt;的呢？&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;第1次请求来时，bizSeq流水号是    &lt;code&gt;666 &lt;/code&gt;，该流水的状态是处理中，值是    &lt;code&gt;1 &lt;/code&gt;，要更新为   &lt;code&gt;2-成功的状态 &lt;/code&gt;，所以该update语句可以正常更新数据，sql执行结果的影响行数是1，流水状态最后变成了2。&lt;/li&gt;
  &lt;li&gt;第2请求也过来了，如果它的流水号还是    &lt;code&gt;666 &lt;/code&gt;，因为该流水状态已经   &lt;code&gt;2-成功的状态 &lt;/code&gt;了，所以更新结果是0，不会再处理业务逻辑，接口直接返回成功。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;5.4 抽取防重表&lt;/h3&gt;
 &lt;p&gt;  &lt;strong&gt;5.1和5.2的方案&lt;/strong&gt;，都是建立在业务流水表上  &lt;code&gt;bizSeq&lt;/code&gt;的唯一性上。很多时候，我们  &lt;strong&gt;业务表唯一流水号&lt;/strong&gt;希望后端系统生成，又或者我们希望  &lt;strong&gt;防重功能与业务表分隔开&lt;/strong&gt;来，这时候我们可以单独搞个  &lt;strong&gt;防重表&lt;/strong&gt;。当然防重表也是利用主键/索引的唯一性，如果插入防重表冲突即直接返回成功，如果插入成功，即去处理请求。&lt;/p&gt;
 &lt;h3&gt;5.5 token令牌&lt;/h3&gt;
 &lt;p&gt;token 令牌方案一般包括两个请求阶段：&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;客户端请求申请获取token，服务端生成token返回&lt;/li&gt;
  &lt;li&gt;客户端带着token请求，服务端校验token&lt;/li&gt;
&lt;/ol&gt;
 &lt;p&gt;流程图如下：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ae6ebfd2ed2d4ce0a46b144b41da2bde~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ol&gt;
  &lt;li&gt;客户端发起请求，申请获取token。&lt;/li&gt;
  &lt;li&gt;服务端生成全局唯一的token，保存到redis中（一般会设置一个过期时间），然后返回给客户端。&lt;/li&gt;
  &lt;li&gt;客户端带着token，发起请求。&lt;/li&gt;
  &lt;li&gt;服务端去redis确认token是否存在，一般用    &lt;code&gt;redis.del(token) &lt;/code&gt;的方式，如果存在会删除成功，即处理业务逻辑，如果删除失败不处理业务逻辑，直接返回结果。&lt;/li&gt;
&lt;/ol&gt;
 &lt;h3&gt;5.6 悲观锁(如select for update)&lt;/h3&gt;
 &lt;p&gt;什么是  &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;strong&gt;加锁&lt;/strong&gt;呀，一般配合事务来实现。&lt;/p&gt;
 &lt;p&gt;举个更新订单的业务场景：&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;假设先查出订单，如果查到的是处理中状态，就处理完业务，再然后更新订单状态为完成。如果查到订单，并且是不是处理中的状态，则直接返回&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;整体的伪代码如下：&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;begin;  # 1.开始事务
select * from order where order_id=&amp;apos;666&amp;apos; # 查询订单，判断状态
if（status !=处理中）{
   //非处理中状态，直接返回；
   return ;
}
## 处理业务逻辑
update order set status=&amp;apos;完成&amp;apos; where order_id=&amp;apos;666&amp;apos; # 更新完成
commit; # 5.提交事务
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;这种场景是非原子操作的，在高并发环境下，可能会造成一个业务被执行两次的问题：&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;当一个请求A在执行中时，而另一个请求B也开始状态判断的操作。因为请求A还未来得及更改状态，所以请求B也能执行成功，这就导致一个业务被执行了两次。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;可以使用数据库悲观锁（  &lt;code&gt;select ...for update&lt;/code&gt;）解决这个问题.&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;begin;  # 1.开始事务
select * from order where order_id=&amp;apos;666&amp;apos; for update # 查询订单，判断状态,锁住这条记录
if（status !=处理中）{
   //非处理中状态，直接返回；
   return ;
}
## 处理业务逻辑
update order set status=&amp;apos;完成&amp;apos; where order_id=&amp;apos;666&amp;apos; # 更新完成
commit; # 5.提交事务
&lt;/code&gt;&lt;/pre&gt;
 &lt;ul&gt;
  &lt;li&gt;这里面order_id需要是   &lt;strong&gt;索引&lt;/strong&gt;或   &lt;strong&gt;主键&lt;/strong&gt;哈，要锁住这条记录就好，如果不是   &lt;strong&gt;索引或者主键&lt;/strong&gt;，会   &lt;strong&gt;锁表&lt;/strong&gt;的！&lt;/li&gt;
  &lt;li&gt;悲观锁在同一事务操作过程中，锁住了一行数据。别的请求过来只能   &lt;strong&gt;等待&lt;/strong&gt;，如果当前事务耗时比较长，就很影响接口性能。所以一般不建议用悲观锁做这个事情。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;5.7 乐观锁&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;blockquote&gt;
  &lt;p&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;code&gt;version&lt;/code&gt;版本号，每次更新记录   &lt;code&gt;version&lt;/code&gt;都升级一下（   &lt;code&gt;version=version+1&lt;/code&gt;）。具体流程就是先查出当前的版本号   &lt;code&gt;version&lt;/code&gt;，然后去更新修改数据时，确认下是不是刚刚查出的版本号，如果是才执行更新&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;p&gt;比如，我们更新前，先查下数据，查出的版本号是  &lt;code&gt;version =1&lt;/code&gt;&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;select order_id，version from order where order_id=&amp;apos;666&amp;apos;；
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;然后使用  &lt;code&gt;version =1 &lt;/code&gt;和  &lt;code&gt;订单Id&lt;/code&gt;一起作为条件，再去更新&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;update order set version = version +1，status=&amp;apos;P&amp;apos; where  order_id=&amp;apos;666&amp;apos; and version =1
&lt;/code&gt;&lt;/pre&gt;
 &lt;p&gt;最后更新成功，才可以处理业务逻辑，如果更新失败，默认为重复请求，直接返回。&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;流程图如下：&lt;/strong&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88cc275ef2024b32b4c778048262acac~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;  &lt;strong&gt;为什么版本号建议自增的呢？&lt;/strong&gt;&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;因为乐观锁存在ABA的问题，如果version版本一直是自增的就不会出现ABA的情况啦。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h3&gt;5.8 分布式锁&lt;/h3&gt;
 &lt;p&gt;分布式锁实现幂等性的逻辑就是，请求过来时，先去尝试获得分布式锁，如果获得成功，就执行业务逻辑，反之获取失败的话，就舍弃请求直接返回成功。执行流程如下图所示：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d9d25fc98d4f4e03a5e67545c93cd581~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;分布式锁可以使用Redis，也可以使用ZooKeeper，不过还是Redis相对好点，因为较轻量级。&lt;/li&gt;
  &lt;li&gt;Redis分布式锁，可以使用命令   &lt;code&gt;SET EX PX NX  + 唯一流水号&lt;/code&gt;实现，分布式锁的   &lt;code&gt;key&lt;/code&gt;必须为业务的唯一标识哈&lt;/li&gt;
  &lt;li&gt;Redis执行设置key的动作时，要设置过期时间哈，这个过期时间不能太短，太短拦截不了重复请求，也不能设置太长，会占存储空间。&lt;/li&gt;
&lt;/ul&gt;
 &lt;h2&gt;6. HTTP的幂等&lt;/h2&gt;
 &lt;p&gt;我们的接口，一般都是基于http的，所以我们再来聊聊Http的幂等吧。 HTTP 请求方法主要有以下这几种，我们看下各个接口是否都是幂等的。&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;GET方法&lt;/li&gt;
  &lt;li&gt;HEAD方法&lt;/li&gt;
  &lt;li&gt;OPTIONS方法&lt;/li&gt;
  &lt;li&gt;DELETE方法&lt;/li&gt;
  &lt;li&gt;POST 方法&lt;/li&gt;
  &lt;li&gt;PUT方法&lt;/li&gt;
&lt;/ul&gt;
 &lt;h3&gt;6.1 GET 方法&lt;/h3&gt;
 &lt;p&gt;HTTP 的GET方法用于获取资源，可以  &lt;strong&gt;类比&lt;/strong&gt;于数据库的  &lt;code&gt;select&lt;/code&gt;查询，不应该有副作用，所以是幂等的。
它不会改变资源的状态，不论你调用一次还是调用多次，效果一样的，都没有副作用。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;如果你的GET方法是获取最近最新的新闻，不同时间点调用，返回的资源内容虽然不一样，但是最终对资源本质是没有影响的哈，所以还是幂等的。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h3&gt;6.2 HEAD 方法&lt;/h3&gt;
 &lt;p&gt;HTTP HEAD和GET有点像，主要区别是  &lt;code&gt;HEAD&lt;/code&gt;不含有呈现数据，而仅仅是HTTP的头信息，所以它也是幂等的。如果想判断某个资源是否存在，很多人会使用  &lt;code&gt;GET&lt;/code&gt;，实际上用  &lt;code&gt;HEAD&lt;/code&gt;则更加恰当。即  &lt;code&gt;HEAD&lt;/code&gt;方法通常用来做探活使用。&lt;/p&gt;
 &lt;h3&gt;6.3 OPTIONS方法&lt;/h3&gt;
 &lt;p&gt;HTTP OPTIONS 主要用于获取当前URL所支持的方法，也是有点像查询，因此也是幂等的。&lt;/p&gt;
 &lt;h3&gt;6.4 DELETE方法&lt;/h3&gt;
 &lt;p&gt;HTTP DELETE 方法用于删除资源，它是的幂等的。比如我们要删除  &lt;code&gt;id=666&lt;/code&gt;的帖子，一次执行和多次执行，影响的效果是一样的呢。&lt;/p&gt;
 &lt;h3&gt;6.5 POST 方法&lt;/h3&gt;
 &lt;p&gt;HTTP POST 方法用于创建资源，可以类比于  &lt;code&gt;update/提交&lt;/code&gt;，显然一次和多次提交更新是有副作用，执行效果是不一样的，  &lt;strong&gt;不满足幂等性&lt;/strong&gt;。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;比如：POST http://www.tianluo.com/articles的语义是在http://www.tianluo.com/articles下创建一篇帖子，HTTP 响应中应包含帖子的创建状态以及帖子的 URI。两次相同的POST请求会在服务器端创建两份资源，它们具有不同的 URI；所以，   &lt;strong&gt;POST方法不具备幂等性&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h3&gt;6.6 PUT 方法&lt;/h3&gt;
 &lt;p&gt;HTTP PUT 方法用于创建或更新操作，所对应的URI是要创建或更新的资源本身，有副作用，它应该满足幂等性。&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;比如：PUT http://www.tianluo.com/articles/666的语义是创建或更新 ID 为666的帖子。对同一 URI 进行多次 PUT 的副作用和一次 PUT 是相同的；因此，PUT 方法具有幂等性。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h2&gt;参考与感谢&lt;/h2&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;a href="https://time.geekbang.org/column/article/4050" title="&amp;#24377;&amp;#21147;&amp;#35774;&amp;#35745;&amp;#31687;&amp;#20043;&amp;#8220;&amp;#24130;&amp;#31561;&amp;#24615;&amp;#35774;&amp;#35745;&amp;#8221;"&gt;弹力设计篇之“幂等性设计”&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/62000-%E5%B9%82%E7%AD%89-%E8%AE%BE%E8%AE%A1</guid>
      <pubDate>Tue, 04 Jan 2022 00:27:32 CST</pubDate>
    </item>
    <item>
      <title>前端监控系统设计</title>
      <link>https://itindex.net/detail/61977-%E5%89%8D%E7%AB%AF-%E7%9B%91%E6%8E%A7-%E7%B3%BB%E7%BB%9F</link>
      <description>&lt;p&gt;前言： 创建一个可随意插拔的插件式前端监控系统&lt;/p&gt;
 &lt;h1&gt;一、数据采集&lt;/h1&gt;
 &lt;h2&gt;1.异常数据&lt;/h2&gt;
 &lt;h3&gt;1.1 静态资源异常&lt;/h3&gt;
 &lt;p&gt;使用window.addEventListener(&amp;apos;error&amp;apos;,cb)&lt;/p&gt;
 &lt;p&gt;由于这个方法会捕获到很多error，所以我们要从中筛选出静态资源文件加载错误情况，这里只监控了js、css、img&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// 捕获静态资源加载失败错误 js css img
window.addEventListener(&amp;apos;error&amp;apos;, e =&amp;gt; {
    const target = e.targetl
    if (!target) return
    const typeName = e.target.localName;
    let sourceUrl = &amp;quot;&amp;quot;;
    if (typeName === &amp;quot;link&amp;quot;) {
        sourceUrl = e.target.href;
    } else if (typeName === &amp;quot;script&amp;quot; || typeName === &amp;quot;img&amp;quot;) {
        sourceUrl = e.target.src;
    }

    if (sourceUrl) {
        lazyReportCache({
            url: sourceUrl,
            type: &amp;apos;error&amp;apos;,
            subType: &amp;apos;resource&amp;apos;,
            startTime: e.timeStamp,
            html: target.outerHTML,
            resourceType: target.tagName,
            paths: e.path.map(item =&amp;gt; item.tagName).filter(Boolean),
            pageURL: getPageURL(),
        })
    }
}, true)

&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;1.2 js错误&lt;/h3&gt;
 &lt;p&gt;通过 window.onerror 获取错误发生时的行、列号，以及错误堆栈&lt;/p&gt;
 &lt;p&gt;生产环境需要上传打包后生成的map文件，利用source-map 对压缩后的代码文件和行列号得出未压缩前的报错行列数和源码文件&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;// parseErrorMsg.js
const fs = require(&amp;apos;fs&amp;apos;);
const path = require(&amp;apos;path&amp;apos;);
const sourceMap = require(&amp;apos;source-map&amp;apos;);

export default async function parseErrorMsg(error) {
  const mapObj = JSON.parse(getMapFileContent(error.url))
  const consumer = await new sourceMap.SourceMapConsumer(mapObj)
  // 将 webpack://source-map-demo/./src/index.js 文件中的 ./ 去掉
  const sources = mapObj.sources.map(item =&amp;gt; format(item))
  // 根据压缩后的报错信息得出未压缩前的报错行列数和源码文件
  const originalInfo = consumer.originalPositionFor({ line: error.line, column: error.column })
  // sourcesContent 中包含了各个文件的未压缩前的源码，根据文件名找出对应的源码
  const originalFileContent = mapObj.sourcesContent[sources.indexOf(originalInfo.source)]
  return {
    file: originalInfo.source,
    content: originalFileContent,
    line: originalInfo.line,
    column: originalInfo.column,
    msg: error.msg,
    error: error.error
  }
}

function format(item) {
  return item.replace(/(\.\/)*/g, &amp;apos;&amp;apos;)
}

function getMapFileContent(url) {
  return fs.readFileSync(path.resolve(__dirname, `./dist/${url.split(&amp;apos;/&amp;apos;).pop()}.map`), &amp;apos;utf-8&amp;apos;)
}

&lt;/code&gt;&lt;/pre&gt;
 &lt;h3&gt;1.3 自定义异常&lt;/h3&gt;
 &lt;p&gt;通过console.error打印出来的，我们将其认为是自定义错误&lt;/p&gt;
 &lt;p&gt;使用 window.console.error 上报自定义异常信息&lt;/p&gt;
 &lt;h3&gt;1.4 接口异常&lt;/h3&gt;
 &lt;ol&gt;
  &lt;li&gt;当状态码异常时，上报异常&lt;/li&gt;
  &lt;li&gt;重写 onloadend 方法，当其 response 对象中 code 值不为 &amp;apos;000000&amp;apos; 时上报异常&lt;/li&gt;
  &lt;li&gt;重写 onerror 方法，当网络中断时无法触发 onload(end) 事件，会触发 onerror, 此时上报异常&lt;/li&gt;
&lt;/ol&gt;
 &lt;h3&gt;1.5 监听未处理的promise错误&lt;/h3&gt;
 &lt;p&gt;当Promise 被reject 且没有reject 处理器的时候，就会触发 unhandledrejection 事件&lt;/p&gt;
 &lt;p&gt;使用 window.addEventListener(&amp;apos;unhandledrejection&amp;apos;,cb)&lt;/p&gt;
 &lt;h2&gt;2.性能数据&lt;/h2&gt;
 &lt;h3&gt;2.1 FP/FCP/LCP/CLS&lt;/h3&gt;
 &lt;p&gt;chrome 开发团队提出了一系列用于检测网页性能的指标：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;FP(first-paint)，从页面加载开始到第一个像素绘制到屏幕上的时间&lt;/li&gt;
  &lt;li&gt;FCP(first-contentful-paint)，从页面加载开始到页面内容的任何部分在屏幕上完成渲染的时间&lt;/li&gt;
  &lt;li&gt;LCP(largest-contentful-paint)，从页面加载开始到最大文本块或图像元素在屏幕上完成渲染的时间&lt;/li&gt;
  &lt;li&gt;CLS(layout-shift)，从页面加载开始和其   &lt;a href="https://link.juejin.cn/?target=https%3A%2F%2Fdevelopers.google.com%2Fweb%2Fupdates%2F2018%2F07%2Fpage-lifecycle-api" title="https://developers.google.com/web/updates/2018/07/page-lifecycle-api"&gt;生命周期状态&lt;/a&gt;变为隐藏期间发生的所有意外布局偏移的累积分数&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;br /&gt;
其中，前三个性能指标都可以直接通过   &lt;a href="https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FPerformanceObserver" title="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver"&gt;PerformanceObserver&lt;/a&gt; （PerformanceObserver 是一个性能监测对象，用于监测性能度量事件 ）来获取。而CLS 则需要通过一些计算。&lt;/p&gt;
 &lt;p&gt;在了解一下计算方式之前，我们先了解一下会话窗口的概念：一个或多个布局偏移间，它们之间有少于1秒的时间间隔，并且第一个和最后一个布局偏移时间间隔上限为5秒，超过5秒的布局偏移将被划分到新的会话窗口。&lt;/p&gt;
 &lt;p&gt;Chrome 速度指标团队在完成  &lt;a href="https://web.dev/evolving-cls/#why-5-seconds"&gt;大规模分析&lt;/a&gt;后，将  &lt;strong&gt;所有会话窗口中的偏移累加最大值&lt;/strong&gt;用来反映页面布局最差的情况（即CLS）。&lt;/p&gt;
 &lt;p&gt;如下图：会话窗口2只有一个微小的布局偏移，则会话窗口2会被忽略，CLS只计算会话窗口1中布局偏移的总和。&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="&amp;#25289;&amp;#20302;&amp;#24179;&amp;#22343;&amp;#20540;&amp;#30340;&amp;#23567;&amp;#24067;&amp;#23616;&amp;#20559;&amp;#31227;&amp;#31034;&amp;#20363;" src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0b66d57252474d798020b4666b0de1c3~tplv-k3u1fbpfcp-zoom-1.image"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;h3&gt;2.2 DOMContentLoaded事件 和  onload 事件&lt;/h3&gt;
 &lt;ul&gt;
  &lt;li&gt;DOMContentLoaded： HTML文档被加载和解析完成。在文档中没有脚本的情况下，浏览器解析完文档便能触发DOMContentLoaded；当文档中有脚本时，脚本会阻塞文档的解析，而脚本需要等位于脚本前面的css加载完才能执行。但是在任何情况下，DOMContentLoaded 都不需要等图片等其他资源的解析。&lt;/li&gt;
  &lt;li&gt;onload: 需要等页面中图片、视频、音频等其他所有资源都加载后才会触发。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;为什么我们在开发时强调把css放在头部，js放在尾部？&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0ce9f508d1be4349a76ec85dab58de9f~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;首先文件放置顺序决定下载的优先级，而浏览器为了避免样式变化导致页面重排or重绘，会阻塞内容的呈现，等所有css加载并解析完成后才一次性呈现页面内容，在此期间就会出现“白屏”。&lt;/p&gt;
 &lt;p&gt;而现代浏览器为了优化用户体验，无需等到所有HTML文档都解析完成才开始构建布局渲染树，也就是说浏览器能够渲染不完整的DOM tree和cssom，尽快减少白屏时间。&lt;/p&gt;
 &lt;p&gt;假设我们把js放在头部，js会阻塞解析dom，导致FP(First Paint)延后，所以我们将js放在尾部，以减少FP的时间，但不会减少 DOMContentLoaded 被触发的时间。&lt;/p&gt;
 &lt;h3&gt;2.3 资源加载耗时及是否命中缓存情况&lt;/h3&gt;
 &lt;p&gt;通过   &lt;a href="https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FPerformanceObserver" title="https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceObserver"&gt;PerformanceObserver&lt;/a&gt; 收集，当浏览器不支持 PerformanceObserver，还可以通过 performance.getEntriesByType(entryType) 来进行降级处理，其中：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;   &lt;strong&gt;Navigation Timing&lt;/strong&gt; 收集了HTML文档的性能指标&lt;/li&gt;
  &lt;li&gt;   &lt;strong&gt;Resource Timing&lt;/strong&gt; 收集了文档依赖的资源的性能指标，如：css，js，图片等等&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;这里不统计以下资源类型：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;beacon: 用于上报数据，不统计&lt;/li&gt;
  &lt;li&gt;xmlhttprequest：单独统计&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;我们能够获取到资源对象的如下信息：&lt;/p&gt;
 &lt;p&gt;  &lt;img alt="image.png" src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fc15eb2223104322a149de03a70a8f95~tplv-k3u1fbpfcp-watermark.image?"&gt;&lt;/img&gt;&lt;/p&gt;
 &lt;p&gt;使用performance.now()精确计算程序执行时间：&lt;/p&gt;
 &lt;ul&gt;
  &lt;li&gt;performance.now()  与 Date.now()  不同的是，返回了以微秒（百万分之一秒）为单位的时间，更加精准。并且与 Date.now()  会受系统程序执行阻塞的影响不同，performance.now()  的时间是以恒定速率递增的，不受系统时间的影响（系统时间可被人为或软件调整）。&lt;/li&gt;
  &lt;li&gt;Date.now()  输出的是 UNIX 时间，即距离 1970 的时间，而 performance.now()  输出的是相对于 performance.timing.navigationStart(页面初始化) 的时间。&lt;/li&gt;
  &lt;li&gt;使用 Date.now()  的差值并非绝对精确，因为计算时间时受系统限制（可能阻塞）。但使用 performance.now()  的差值，并不影响我们计算程序执行的精确时间。&lt;/li&gt;
&lt;/ul&gt;
 &lt;p&gt;  &lt;strong&gt;判断该资源是否命中缓存：&lt;/strong&gt;  &lt;br /&gt;
在这些资源对象中有一个 transferSize 字段，它表示获取资源的大小，包括响应头字段和响应数据的大小。如果这个值为 0，说明是从缓存中直接读取的（强制缓存）。如果这个值不为 0，但是 encodedBodySize 字段为 0，说明它走的是协商缓存（encodedBodySize 表示请求响应数据 body 的大小）。不符合以上条件的，说明未命中缓存。&lt;/p&gt;
 &lt;h3&gt;2.4 接口请求耗时以及接口调用成败情况&lt;/h3&gt;
 &lt;p&gt;对XMLHttpRequest 原型链上的send 以及open方法进行改写&lt;/p&gt;
 &lt;pre&gt;  &lt;code&gt;import { originalOpen, originalSend, originalProto } from &amp;apos;../utils/xhr&amp;apos;
import { lazyReportCache } from &amp;apos;../utils/report&amp;apos;

function overwriteOpenAndSend() {
    originalProto.open = function newOpen(...args) {
        this.url = args[1]
        this.method = args[0]
        originalOpen.apply(this, args)
    }

    originalProto.send = function newSend(...args) {
        this.startTime = Date.now()

        const onLoadend = () =&amp;gt; {
            this.endTime = Date.now()
            this.duration = this.endTime - this.startTime

            const { duration, startTime, endTime, url, method } = this
            const { readyState, status, statusText, response, responseUrl, responseText } = this
            console.log(this)
            const reportData = {
                status,
                duration,
                startTime,
                endTime,
                url,
                method: (method || &amp;apos;GET&amp;apos;).toUpperCase(),
                success: status &amp;gt;= 200 &amp;amp;&amp;amp; status &amp;lt; 300,
                subType: &amp;apos;xhr&amp;apos;,
                type: &amp;apos;performance&amp;apos;,
            }

            lazyReportCache(reportData)

            this.removeEventListener(&amp;apos;loadend&amp;apos;, onLoadend, true)
        }

        this.addEventListener(&amp;apos;loadend&amp;apos;, onLoadend, true)
        originalSend.apply(this, args)
    }
}

export default function xhr() {
    overwriteOpenAndSend()
}
&lt;/code&gt;&lt;/pre&gt;
 &lt;h1&gt;二、数据上报&lt;/h1&gt;
 &lt;h2&gt;1. 上报方法&lt;/h2&gt;
 &lt;p&gt;采用  &lt;a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Navigator/sendBeacon"&gt;sendBeacon&lt;/a&gt; 和 XMLHttpRequest 相结合的方式&lt;/p&gt;
 &lt;p&gt;为什么要使用sendBeacon?&lt;/p&gt;
 &lt;blockquote&gt;
  &lt;p&gt;统计和诊断代码通常要在 unload 或者 beforeunload (en-US) 事件处理器中发起一个同步 XMLHttpRequest 来发送数据。同步的 XMLHttpRequest 迫使用户代理延迟卸载文档，并使得下一个导航出现的更晚。下一个页面对于这种较差的载入表现无能为力。   &lt;br /&gt;
   &lt;strong&gt;navigator.sendBeacon()&lt;/strong&gt;  方法可用于通过HTTP将少量数据异步传输到Web服务器，同时不会延迟页面的卸载或影响下一导航的载入性能。这就解决了提交分析数据时的所有的问题：数据可靠，传输异步并且不会影响下一页面的加载。&lt;/p&gt;
&lt;/blockquote&gt;
 &lt;h2&gt;2. 上报时机&lt;/h2&gt;
 &lt;ol&gt;
  &lt;li&gt;先缓存上报数据，缓存到一定数量后，利用 requestIdleCallback/setTimeout 延时上报。&lt;/li&gt;
  &lt;li&gt;在即将离开当前页面(刷新或关闭)时上报 （onBeforeUnload ）/ 在页面不可见时上报（onVisibilitychange，判断document.visibilityState/ document.hidden 状态）&lt;/li&gt;
&lt;/ol&gt;
&lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/61977-%E5%89%8D%E7%AB%AF-%E7%9B%91%E6%8E%A7-%E7%B3%BB%E7%BB%9F</guid>
      <pubDate>Tue, 28 Dec 2021 10:27:25 CST</pubDate>
    </item>
    <item>
      <title>Netflix系统架构设计方案</title>
      <link>https://itindex.net/detail/61973-netflix-%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84-%E8%AE%BE%E8%AE%A1</link>
      <description>&lt;br /&gt;【编者的话】Netflix是全球最大的在线视频网站之一，它是怎么设计的呢？这篇文章介绍了Netflix系统架构的设计方案。原文： &lt;a href="https://medium.com/interviewnoodle/netflix-system-architecture-bedfc1d4bce5"&gt;Netflix System Architecture&lt;/a&gt;。 &lt;br /&gt;
 &lt;br /&gt;我们来讨论一下如何设计Netflix。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/77132ff186523f95ac4bed4ac20866bb.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="1.jpeg" src="http://dockone.io/uploads/article/20211227/77132ff186523f95ac4bed4ac20866bb.jpeg" title="1.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
相信每个人都会通过某些网站或应用在线追剧或者看电影，而Netflix是我最喜欢的在线视频网站，不过今天我不推荐任何电影，相反，我想展示的是Netflix背后令人惊艳的系统逻辑。 &lt;br /&gt;
 &lt;h3&gt;需求&lt;/h3&gt; &lt;h4&gt;功能性需求&lt;/h4&gt; &lt;ol&gt;  &lt;li&gt;创建帐户、登录、删除帐户&lt;/li&gt;  &lt;li&gt;订阅或取消订阅不同的计划&lt;/li&gt;  &lt;li&gt;允许用户拥有和处理多个帐户&lt;/li&gt;  &lt;li&gt;允许用户观看视频&lt;/li&gt;  &lt;li&gt;允许用户下载视频并离线观看&lt;/li&gt;  &lt;li&gt;允许用户通过视频标题搜索和发现视频&lt;/li&gt;  &lt;li&gt;Netflix制作人可以从后台上传视频并在平台上展示&lt;/li&gt;  &lt;li&gt;平台可以显示趋势、最受欢迎的视频和分类，以方便用户选择&lt;/li&gt;  &lt;li&gt;可以选择不同语言的字幕，这样用户即使听不懂这些语言，也可以观看视频&lt;/li&gt;  &lt;li&gt;视频分组（剧集、娱乐节目、电影，单独处理每个视频）&lt;/li&gt;  &lt;li&gt;根据用户行为进行分析，为用户推荐类似的视频&lt;/li&gt;  &lt;li&gt;在同一账号下的不同设备之间进行同步，用户可以使用不同的设备继续观看同一视频而无需重播&lt;/li&gt;  &lt;li&gt;支持全天候（24/7）回放&lt;/li&gt;  &lt;li&gt;支持回退&lt;/li&gt;&lt;/ol&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;h4&gt;非功能性需求&lt;/h4&gt; &lt;ol&gt;  &lt;li&gt;用户可以观看实时视频流，没有任何卡顿或延迟问题&lt;/li&gt;  &lt;li&gt;系统是高度可靠的&lt;/li&gt;  &lt;li&gt;高可用&lt;/li&gt;  &lt;li&gt;可扩展&lt;/li&gt;  &lt;li&gt;视频数据持久化且易于访问&lt;/li&gt;&lt;/ol&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;h3&gt;容量预估&lt;/h3&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/f927c3a5b92fd4242f73de601afc23c7.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="2.png" src="http://dockone.io/uploads/article/20211227/f927c3a5b92fd4242f73de601afc23c7.png" title="2.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
我们可以基于一些数学计算来估计所需的带宽和存储空间。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/717570d8a083a3ab6fe31562e0fef7cc.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="3.jpeg" src="http://dockone.io/uploads/article/20211227/717570d8a083a3ab6fe31562e0fef7cc.jpeg" title="3.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/177bce63b9cda594fb3cf4d93d6106fb.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="4.jpeg" src="http://dockone.io/uploads/article/20211227/177bce63b9cda594fb3cf4d93d6106fb.jpeg" title="4.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h4&gt;假设&lt;/h4&gt; &lt;ol&gt;  &lt;li&gt;日活用户总数 = 1亿&lt;/li&gt;  &lt;li&gt;日活峰值用户：1亿 * 3 = 3亿&lt;/li&gt;  &lt;li&gt;3个月最大日活峰值用户：3亿 * 2 = 6亿&lt;/li&gt;  &lt;li&gt;每个用户每天平均观看的视频数 = 5&lt;/li&gt;  &lt;li&gt;视频平均大小 = 500 MB&lt;/li&gt;  &lt;li&gt;后台平均每天上传的视频数 = 1000&lt;/li&gt;  &lt;li&gt;每天观看的总视频数 = 1亿*5 = 5亿&lt;/li&gt;  &lt;li&gt;每天观看的总视频峰值 = 15亿&lt;/li&gt;  &lt;li&gt;每天观看的最大视频峰值 = 30亿&lt;/li&gt;  &lt;li&gt;每天总出口流量 = 5亿* 500 MB = 250 PB (Peta Byte)&lt;/li&gt;  &lt;li&gt;出口带宽 = 29.1 GB/秒&lt;/li&gt;  &lt;li&gt;每天上传总入口流量 = 1000 * 500MB = 500 GB&lt;/li&gt;  &lt;li&gt;入口带宽 = 5.8 MB/秒&lt;/li&gt;  &lt;li&gt;5年所需的总存储空间 = 500 GB * 5 * 365 = 912.5 TB（请注意，Netflix会为每个视频准备多种格式和分辨率的版本，可针对不同类型设备进行优化，所以存储空间将超过912.5 TB）。&lt;/li&gt;&lt;/ol&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/ce73dd2bf82a71e79f180bcf9e26b560.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="5.png" src="http://dockone.io/uploads/article/20211227/ce73dd2bf82a71e79f180bcf9e26b560.png" title="5.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/5dc64d7c5d0a17357076cda652a1456c.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="6.png" src="http://dockone.io/uploads/article/20211227/5dc64d7c5d0a17357076cda652a1456c.png" title="6.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h3&gt;系统组件&lt;/h3&gt; &lt;h4&gt;系统组件详细设计&lt;/h4&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/1096862bddd80bc7889832e4f6e88d86.jpg" rel="lightbox" target="_blank"&gt;   &lt;img alt="7.jpg" src="http://dockone.io/uploads/article/20211227/1096862bddd80bc7889832e4f6e88d86.jpg" title="7.jpg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/7c70231d75400988604b6342e2275496.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="8.png" src="http://dockone.io/uploads/article/20211227/7c70231d75400988604b6342e2275496.png" title="8.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
1、客户端应用 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;手机（iOS，Android，华为，等等）&lt;/li&gt;  &lt;li&gt;平板（iPad，Android，Windows）&lt;/li&gt;  &lt;li&gt;电视&lt;/li&gt;  &lt;li&gt;电脑&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt;基于React.js实现的前端可以拥有较好的加载/启动速度、持久性/模块化和运行时性能。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/4ed1c9880b0f85aecafd2a1618d99d77.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="9.png" src="http://dockone.io/uploads/article/20211227/4ed1c9880b0f85aecafd2a1618d99d77.png" title="9.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
2、后端 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/26ef6bb6d634a9ba6d5b99aa6372fd1a.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="10.jpeg" src="http://dockone.io/uploads/article/20211227/26ef6bb6d634a9ba6d5b99aa6372fd1a.jpeg" title="10.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
Netflix从2011年开始实施微服务架构，完全基于云来管理工作负载。通过小型、可管理的API组件支持并处理来自应用程序和网站的请求，微服务内部通过请求和获取数据而相互依赖。后端技术栈包括了Java，MySQL，Gluster，Apache Tomcat，Chukwa，Cassandra，Kafka和Hadoop。后端系统不单单需要处理流媒体视频，还需要处理其他所有事情，比方说数据处理、加载新内容、网络流量管理、全球资源分发等。Netflix目前部署在AWS之上。 &lt;br /&gt;
 &lt;br /&gt;数据处理涉及点击视频后发生的所有事件，系统需要在几纳秒的时间内处理完视频并将其传输给用户。整个系统每天大约需要处理6000亿个事件，产生1.5PB的数据，在高峰期（傍晚和夜间）每秒大约需要处理800万个事件。这些事件包括UI活动、视频查看活动、日志错误、故障排除、诊断事件、处理事件和性能事件等。所有这些事件都是通过Kafka和Apache Chukwe完成的。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/ace5788f28172809861ac4be529e0460.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="11.png" src="http://dockone.io/uploads/article/20211227/ace5788f28172809861ac4be529e0460.png" title="11.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
Kafka和Apache Chukwe： &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;从系统的不同部分获取产生的数据。&lt;/li&gt;  &lt;li&gt;Apache Chukwe是一个开源数据收集系统，用于从分布式系统中收集日志或事件。它建立在HDFS和Map-reduce框架之上，具有Hadoop的可伸缩性和健壮性特性。此外，它还包含许多功能强大、灵活的工具箱，用于显示、监控和分析结果。Chukwe从系统的不同部分收集事件，并提供仪表盘帮助我们进行事件的查看、监控和分析。Chukwe以Hadoop文件序列格式（S3）写入事件，大数据团队可以处理这些S3 Hadoop文件，并以Parque格式将数据写入Hive。这个过程被称为批处理，基本上以每小时或每天的频率扫描整个数据。为了将在线事件上传到EMR/S3，Chukwa还向Kafka（实时数据处理的入口）提供流量。Kafka负责将数据从前端Kafka注入到不同的后端：S3，Elasticsearch和下游Kafka，消息的路由可以通过Apache Samja框架完成。通过Chukwe发送的流量既可以是完整的流也可以是过滤过的流，所以有时候你可能需要对Kafka流量进行进一步过滤，这就是我们需要考虑将流量从一个Kafka topic路由到另一个Kafka topic的原因。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt;Elastic Search： &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;Netflix目前有大约150个Elastic Search集群，其实例分布在3500个主机上。&lt;/li&gt;  &lt;li&gt;Netflix通过Elastic Search来实现数据的可视化、客户支持以及系统中的错误检测。例如，如果客户无法播放视频，那么客户服务主管将利用Elastic Search来解决问题。回放团队会去Elastic Search搜索该用户，试图找到为什么视频不能在用户设备上播放的原因。他们可以了解特定用户所发生的所有信息和事件，知道是什么导致了视频流出错。系统管理员还可以基于Elastic Search跟踪某些信息，比如跟踪资源使用情况、检测注册或登录问题等。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/36360818e9f0887c0bc6b86d8fa6d21d.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="12.jpeg" src="http://dockone.io/uploads/article/20211227/36360818e9f0887c0bc6b86d8fa6d21d.jpeg" title="12.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
后端服务： &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;用户和认证服务（主要负责用户认证和配置文件）。数据存储在关系型数据库中，如MySQL或PostgreSQL。&lt;/li&gt;  &lt;li&gt;订阅管理服务（管理用户的订阅）。由于该服务处理的数据本质上是高度事务性的，因此RDBMS是一个合适的选择。&lt;/li&gt;  &lt;li&gt;视频服务（向终端用户提供视频）。这个服务将视频元数据存储在RDBMS中，比如MySQL或PostgreSQL。为了获得更快的响应时间，该服务将使用Redis或Memcached这样的内存缓存来实现绕写（write-around）缓存。&lt;/li&gt;  &lt;li&gt;转码服务（检查上传视频的质量，用不同的编解码器压缩视频，创建不同分辨率版本）。一旦视频被上传到Transcoder服务，它将把视频上传到内部分布式存储，比如S3，并向数据库添加条目。Kafka或RabbitMQ在队列中处理消息，后端工作组件收到队列的消息，内部S3下载视频，并将其转码为不同的格式。转码完成后，后端工作组件将视频上传到外部S3，并将数据库中的视频状态更新为active，供终端用户查看。后端工作组件还会在支持全文搜索的搜索数据库中添加视频元数据条目，这样终端用户就能够使用标题或摘要搜索视频。外部S3存储的视频也将通过CDN缓存，以减少延迟，提高播放性能。&lt;/li&gt;  &lt;li&gt;全球搜索服务（允许终端用户使用元数据，如标题、摘要等搜索视频）。元数据存储在Elastic Search数据库中，因此可以基于Elasticsearch或Solr支持全文搜索，用户可以根据标题搜索电影、剧集或与视频相关的任何元数据。该服务还可以根据最近观看、评论、推荐和流行程度对结果进行排名，以获得更好的用户体验。此外，Elastic Search可以在失败的情况下跟踪用户事件，客户服务团队可以使用Elastic Search来解决问题。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt;3、云 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;Netflix将其IT基础设施迁移到公共云上。使用的云服务是AWS和Open connect（Netflix的定制CDN）。这两种云服务并行工作，用于视频处理和向终端用户分发内容。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt;4、CDN &lt;br /&gt;
 &lt;br /&gt;一个全球分布的服务器网络集群。当我们播放视频的时候，设备上显示的视频将从最近的CDN服务器获取，从而极大降低响应时间。 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;CDN在多个地方复制内容，这样视频可以更贴近用户，传输距离更短。&lt;/li&gt;  &lt;li&gt;CDN机器大量使用缓存，所以即使没有从服务器上找到视频，也可以从缓存中获取。&lt;/li&gt;  &lt;li&gt;CDN不会缓存不太受欢迎的视频（比方说每天只有不到20次观看量的视频）&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/66eec1f78bee357359e899b1b2311ec0.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="13.png" src="http://dockone.io/uploads/article/20211227/66eec1f78bee357359e899b1b2311ec0.png" title="13.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
5、Open connect &lt;br /&gt;
 &lt;br /&gt;Netflix的内部定制全球CDN，负责向全球Netflix用户存储和传送电影和电视节目。当我们按下播放按钮，视频就会从全球不同位置的Open connect服务器中传输给我们。如果视频已经缓存在Open connect服务器上，客户端可以轻松访问到，而如果视频没有被缓存，Netflix必须从AWS的S3存储中获取并处理该视频，然后Open connect才可以将该视频流推送到客户端应用程序。 &lt;br /&gt;
 &lt;br /&gt;6、缓存 &lt;br /&gt;
 &lt;br /&gt;Redis和Memcached以键值对的方式缓存数据库中的数据，可以有效减少对数据库的访问。客户端通过服务器访问数据库之前，系统会检查缓存中是否有数据，如果有，就可以绕过数据库访问。但是，如果数据不在缓存中，必须访问数据库并获取数据，并在缓存中填充相同的数据。因此，随后的请求就不需要访问数据库了。这种缓存策略称为绕写（write-around）缓存。我们使用最近最少使用（LRU）策略作为缓存数据的驱逐策略，最早获取的缓存将会被丢弃。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/4a292ccb18f8e8ab19025da17e4fe452.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="14.jpeg" src="http://dockone.io/uploads/article/20211227/4a292ccb18f8e8ab19025da17e4fe452.jpeg" title="14.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;EV缓存实际上是Memcached的包装器&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/7a9f0c4f83d34a941543d3271402e066.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="15.jpeg" src="http://dockone.io/uploads/article/20211227/7a9f0c4f83d34a941543d3271402e066.jpeg" title="15.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
Netflix在AWS EC2上部署了很多集群，这些集群包含有很多Memcached节点以及缓存客户端。数据在同一个分区的集群中共享，多个缓存副本存储在分片节点中。每次当客户端写入数据时，所有集群中的所有节点都会被更新，但当读取数据时，读取操作只会被发送到最近的集群及其节点上，如果某个节点不可用，则从另一个可用节点读取。这种方法提高了性能、可用性和可靠性。 &lt;br /&gt;
 &lt;br /&gt;7、可扩展性 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;水平扩展——在负载均衡器后面增加更多的应用服务器，以增加服务的容量。&lt;/li&gt;  &lt;li&gt;数据库备份——关系数据库配置为主从关系，写操作发生在主数据库上，从从数据库读取数据。读操作不会因为写操作而被锁住，因此可以提高读查询的性能。当数据写入主数据库并复制到从数据库时，会有轻微的复制延迟（几毫秒）。&lt;/li&gt;  &lt;li&gt;数据库分片——将数据分布到多个服务器上，以便高效的进行读写操作。比方说，我们可以使用video_id对视频元数据数据库进行分片，哈希函数把每个video_id随机映射到一个服务器上，从而存储对应的视频元数据。&lt;/li&gt;  &lt;li&gt;缓存分片——将缓存分发到多个服务器上。Redis支持跨多个实例划分数据，为数据分布使用一致的哈希算法确保在一个实例消失时保持负载均匀分布。&lt;/li&gt;  &lt;li&gt;搜索数据库分片——Elasticsearch原生支持分片和备份。通过在多个分片上并行运行分片，有助于改进查询运行时。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt;8、安全 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;HTTPS——通过HTTPS加密客户端和服务器之间的通信，确保中间没有人能够看到数据（特别是密码）。&lt;/li&gt;  &lt;li&gt;身份验证——每个API请求必须完成登录验证，通过检查授权HTTP报头中auth_token的有效性来进行身份验证，确保请求是合法的。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt;9、弹性 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;备份——通过主从部署备份数据库。如果一个节点宕机，其他节点将按预期提供服务并继续运行。&lt;/li&gt;  &lt;li&gt;队列——在处理上传的视频时使用。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/73d5a79d3b53bbf7b2d69b98ee06bdda.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="16.png" src="http://dockone.io/uploads/article/20211227/73d5a79d3b53bbf7b2d69b98ee06bdda.png" title="16.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
10、负载均衡 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;一个负载均衡器后面有多个服务器，包括冗余资源。负载均衡器将持续对其背后的服务器进行健康检查，如果发现任意一个服务器停止工作，负载均衡器将停止向它转发流量，并将其从集群中移除，从而确保请求不会因为服务器没有响应而失败。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/22a37e811c819af7f7f99b511e2065b2.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="17.jpeg" src="http://dockone.io/uploads/article/20211227/22a37e811c819af7f7f99b511e2065b2.jpeg" title="17.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
负载均衡器负责将流量路由到前端服务。ELB（Elastic load balancing，弹性负载均衡）执行两层负载均衡方案，首先基于区域（zone）进行负载均衡，然后对实例（服务器）进行负载均衡。 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;第一级由基础DNS组成，提供基于轮询的负载均衡（Round Robin Balancing）。当请求到达第一个负载均衡器时，它会根据配置选择一个区域（使用轮询机制）。&lt;/li&gt;  &lt;li&gt;第二级是一组负载均衡器实例，执行轮询负载均衡，将请求分发到位于同一区域的多个业务实例中。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt;11、Geo-redundancy &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;在跨多个地理位置的数据中心部署服务的精确副本，一旦某个数据中心无法提供服务，仍然可以由其他数据中心提供服务。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt;12、ZUUL &lt;br /&gt;
 &lt;br /&gt;提供动态路由、监控、弹性和安全性，支持基于查询参数、URL路径的简单路由。 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;Netty服务器负责处理网络协议、web服务、连接管理和代理工作。当请求到达Netty服务器时，它负责将请求转发到入口过滤器。&lt;/li&gt;  &lt;li&gt;入口过滤器（The inbound filter）负责身份验证、路由或装饰请求。然后将请求转发给端点过滤器。&lt;/li&gt;  &lt;li&gt;端点过滤器（Endpoint filter）用于返回静态响应，或者将请求转发到后端服务。一旦它从后端服务接收到响应，就将请求发送到出口过滤器。&lt;/li&gt;  &lt;li&gt;出口过滤器（Outbound filter）用于压缩内容、计算指标或添加/删除自定义标头。在此之后，响应被发送回Netty服务器，然后发送给客户端。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/2ecfecfae1eb9bc0d06853c0af7d7b60.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="18.png" src="http://dockone.io/uploads/article/20211227/2ecfecfae1eb9bc0d06853c0af7d7b60.png" title="18.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
优势： &lt;br /&gt;
 &lt;ol&gt;  &lt;li&gt;可以创建规则，将流量的不同部分分配到不同的服务器，从而实现对流量的分片。&lt;/li&gt;  &lt;li&gt;开发人员可以在某些机器上对新部署的集群进行负载测试，可以在这些集群上路由部分现网流量，并检查特定服务器可以承受多少负载。&lt;/li&gt;  &lt;li&gt;可以用于测试新服务。当我们需要升级服务并希望检查该服务如何处理实时API请求时，可以将特定服务部署在一台服务器上，并将部分流量重定向到新服务，以便实时检查该服务状态。&lt;/li&gt;  &lt;li&gt;可以通过在端点过滤器或防火墙上设置自定义规则来过滤恶意请求。&lt;/li&gt;&lt;/ol&gt; &lt;br /&gt;
 &lt;br /&gt;13、Hystrix &lt;br /&gt;
 &lt;br /&gt;在一个复杂的分布式系统中，一个服务器可能依赖于另一个服务器的响应。这些服务器之间的依赖关系可能会造成延迟，如果其中一个服务器在某个时刻不可避免的出现故障，整个系统可能都会停止工作。为了解决这个问题，可以将主机应用程序与这些外部故障隔离开来。Hystrix库就是为此而设计的，通过添加延迟容忍和容错逻辑，帮助我们控制分布式服务之间的交互。Hystrix通过隔离服务、远程系统和第三方库之间的访问点来实现这一点。Hystrix可以帮助我们实现： &lt;br /&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;li&gt;启用近实时监控、警报和运维控制。&lt;/li&gt;  &lt;li&gt;并发感知的请求缓存，通过请求崩溃实现自动批处理&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;h3&gt;数据库组件&lt;/h3&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/c7482a26cf117f145b5248c44ceaaade.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="19.png" src="http://dockone.io/uploads/article/20211227/c7482a26cf117f145b5248c44ceaaade.png" title="19.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
Netflix使用不同的DB来存储不同类型的文件，例如用于不同目的的SQL和NoSQL。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/14c4703975feca529bcd2d145b7182da.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="20.jpeg" src="http://dockone.io/uploads/article/20211227/14c4703975feca529bcd2d145b7182da.jpeg" title="20.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/0850585191299b1837515e3da6b67ab4.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="21.jpeg" src="http://dockone.io/uploads/article/20211227/0850585191299b1837515e3da6b67ab4.jpeg" title="21.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/d0675af82d744895bd714aa816b87f44.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="22.png" src="http://dockone.io/uploads/article/20211227/d0675af82d744895bd714aa816b87f44.png" title="22.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h4&gt;MySQL&lt;/h4&gt; &lt;ul&gt;  &lt;li&gt;符合ACID，因此可用于管理影片标题、计费和事务用途。&lt;/li&gt;  &lt;li&gt;在AWS EC2上部署MySQL来存储数据&lt;/li&gt;  &lt;li&gt;MySQL配置为主主模式，在大型AWS EC2实例上使用InnoDB引擎构建。&lt;/li&gt;  &lt;li&gt;设置遵循“同步复制协议（Synchronous replication protocol）”。数据复制是同步完成的，表明节点之间存在主主关系，只有当数据由本地和远程节点同步以确保高可用性时，才会认为主节点上的任何写操作已经完成。读查询不是由主节点处理，而是由副本处理，只有写查询是由主数据库处理。在故障转移的情况下，副节点将作为主节点，并将负责处理写操作。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/abe154a6f5c23e6915885a2797794639.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="23.jpeg" src="http://dockone.io/uploads/article/20211227/abe154a6f5c23e6915885a2797794639.jpeg" title="23.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/264801c53477b3444b14ad72f3ebd9ce.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="24.png" src="http://dockone.io/uploads/article/20211227/264801c53477b3444b14ad72f3ebd9ce.png" title="24.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h4&gt;Cassandra（NoSQL）&lt;/h4&gt; &lt;ul&gt;  &lt;li&gt;Cassandra是开源的、分布式的、基于列的NoSQL数据库，可以在服务器上存储大量数据。Netflix使用Cassandra来存储用户历史。它可以有效处理大量读请求，并优化大量读请求的延迟。随着用户群的增长，存储多行数据变得越来越困难，而且成本高且速度慢。所以，Netflix设计了基于时间框架和最近使用的新数据库。&lt;/li&gt;  &lt;li&gt;当Netflix的用户越来越多时，每个用户的观看历史数据也开始增加。&lt;/li&gt;  &lt;li&gt;更小的存储空间开销。&lt;/li&gt;  &lt;li&gt;随着用户查看次数的增长而增长的一致性读写性能（在Cassandra中查看历史数据写读比约为9:1）。&lt;/li&gt;  &lt;li&gt;非规范化数据模型&lt;/li&gt;  &lt;li&gt;超过50个Cassandra集群&lt;/li&gt;  &lt;li&gt;超过500个节点&lt;/li&gt;  &lt;li&gt;每天超过30TB的备份数据&lt;/li&gt;  &lt;li&gt;最大集群有72个节点&lt;/li&gt;  &lt;li&gt;每个集群超过250K每秒写操作&lt;/li&gt;  &lt;li&gt;最初，观看历史记录存储在Cassandra的单行中。当Netflix的用户越来越多，行数和总体数据大小都增加了。这导致了更高的存储成本、更高的操作成本和更低的应用程序性能。解决方案是压缩旧的行……&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/606f0173422472b57391e15bf9a3e378.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="25.png" src="http://dockone.io/uploads/article/20211227/606f0173422472b57391e15bf9a3e378.png" title="25.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;LiveVH（实时观看历史记录）——只保存更新频繁的最近的数据，以未压缩的形式存储较少的行，可用于许多分析操作，比如在执行ETL（提取，转换和加载）后对用户提供建议。&lt;/li&gt;  &lt;li&gt;CompressedVH（压缩观看历史）——压缩后保存的用户浏览及观看历史旧数据，几乎不更新。存储大小也减少了，每行只存储一列。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt;数据库定义： &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/f4b2b6b10dfca0c5b474edee37f72b25.jpg" rel="lightbox" target="_blank"&gt;   &lt;img alt="26.jpg" src="http://dockone.io/uploads/article/20211227/f4b2b6b10dfca0c5b474edee37f72b25.jpg" title="26.jpg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h3&gt;API&lt;/h3&gt; &lt;h4&gt;使用REST API&lt;/h4&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/dd602e0521e1a68bcd157adaa3b5e1d5.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="27.png" src="http://dockone.io/uploads/article/20211227/dd602e0521e1a68bcd157adaa3b5e1d5.png" title="27.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;strong&gt;用户注册&lt;/strong&gt; &lt;br /&gt;
 &lt;br /&gt;请求： &lt;br /&gt;
 &lt;pre&gt;POST /api/v1/users  &lt;br /&gt;
X-API-Key: api_key  &lt;br /&gt;
{  &lt;br /&gt;
name:  &lt;br /&gt;
email:  &lt;br /&gt;
password:  &lt;br /&gt;
}   &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
通过HTTP POST方法，在数据库中创建一个资源或新条目。X-API-Key是传递给HTTP报头的API key，用于识别不同的客户端并进行速率限制。 &lt;br /&gt;
 &lt;br /&gt;响应： &lt;br /&gt;
 &lt;pre&gt;201 Created  &lt;br /&gt;
{  &lt;br /&gt;
message:  &lt;br /&gt;
}   &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
HTTP状态码201告诉用户已成功注册。用于失败情况的其他可能的HTTP状态码： &lt;br /&gt;
 &lt;pre&gt;400 Bad Request  &lt;br /&gt;
409 Conflict  &lt;br /&gt;
500 Internal Server Error  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
 &lt;strong&gt;用户登录&lt;/strong&gt; &lt;br /&gt;
 &lt;br /&gt;请求： &lt;br /&gt;
 &lt;pre&gt;POST /api/v1/users/session  &lt;br /&gt;
X-API-Key: api_key  &lt;br /&gt;
{  &lt;br /&gt;
email:  &lt;br /&gt;
password:  &lt;br /&gt;
}   &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
响应： &lt;br /&gt;
 &lt;pre&gt;200 OK  &lt;br /&gt;
{  &lt;br /&gt;
auth_token:  &lt;br /&gt;
}   &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
API应该返回一个auth_token，它可以在header中传递给需要认证的后续API调用。auth_token可以使用JWT生成。 &lt;br /&gt;
 &lt;br /&gt; &lt;strong&gt;用户登出&lt;/strong&gt; &lt;br /&gt;
 &lt;br /&gt;请求： &lt;br /&gt;
 &lt;pre&gt;DELETE /api/v1/users/session  &lt;br /&gt;
X-API-Key: api_key  &lt;br /&gt;
Authorization: auth_token  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
使用HTTP DELETE方法删除数据库中的行条目，意味着我们正在终止一个会话。 &lt;br /&gt;
 &lt;br /&gt;响应： &lt;br /&gt;
 &lt;pre&gt;200 OK  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
HTTP状态码200表示成功登出。 &lt;br /&gt;
 &lt;br /&gt; &lt;strong&gt;订阅&lt;/strong&gt; &lt;br /&gt;
 &lt;br /&gt;请求： &lt;br /&gt;
 &lt;pre&gt;POST /api/v1/subscription  &lt;br /&gt;
X-API-Key: api_key  &lt;br /&gt;
Authorization: auth_token  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
HTTP POST方法创建一个新的订阅，在Authorization头中传递auth-token来验证用户。 &lt;br /&gt;
 &lt;br /&gt;响应： &lt;br /&gt;
 &lt;pre&gt;201 Created  &lt;br /&gt;
{  &lt;br /&gt;
subscription_id:  &lt;br /&gt;
plan_name:  &lt;br /&gt;
valid_till:  &lt;br /&gt;
}   &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
HTTP状态码201与subcription_id、plan_name和valid_till一起在用户界面中呈现。 &lt;br /&gt;
 &lt;br /&gt;可能的HTTP失败状态码： &lt;br /&gt;
 &lt;pre&gt;401 Unauthorized  &lt;br /&gt;
400 Bad request  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
 &lt;strong&gt;取消订阅&lt;/strong&gt; &lt;br /&gt;
 &lt;br /&gt;请求： &lt;br /&gt;
 &lt;pre&gt;DELETE /api/v1/subscription  &lt;br /&gt;
X-API-Key: api_key  &lt;br /&gt;
Authorization: auth_token  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
HTTP DELETE方法是可以取消订阅，该接口将从订阅数据库中删除一个行条目。 &lt;br /&gt;
 &lt;br /&gt;响应： &lt;br /&gt;
 &lt;pre&gt;200 OK  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
HTTP状态码200意味着成功完成。 &lt;br /&gt;
 &lt;br /&gt; &lt;strong&gt;批量获取视频&lt;/strong&gt; &lt;br /&gt;
 &lt;br /&gt;请求： &lt;br /&gt;
 &lt;pre&gt;GET /api/v1/videos?page_id=  &lt;br /&gt;
X-API-Key: api_key  &lt;br /&gt;
Authorization: auth_token  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
该API用于在登录后呈现主页，包含了由机器学习模型确定的推荐视频。page_id用于API中的分页，next_page_id用于从下一页请求结果。 &lt;br /&gt;
 &lt;br /&gt;响应： &lt;br /&gt;
 &lt;pre&gt;200 OK  &lt;br /&gt;
{  &lt;br /&gt;
page_id:  &lt;br /&gt;
next_page_id:  &lt;br /&gt;
videos: [  &lt;br /&gt;
{  &lt;br /&gt;
  id:  &lt;br /&gt;
  title:  &lt;br /&gt;
  summary:  &lt;br /&gt;
  url:  &lt;br /&gt;
  watched_till:  &lt;br /&gt;
},...  &lt;br /&gt;
]  &lt;br /&gt;
}   &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
HTTP状态码200表示操作成功。 &lt;br /&gt;
 &lt;br /&gt;其他故障状态码： &lt;br /&gt;
 &lt;pre&gt;401 Unauthorized  &lt;br /&gt;
500 Bad request  &lt;br /&gt;
429 Too many requests  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
HTTP状态码429意味着用户达到速率限制，需要等待一段时间才能再次发出请求，以避免拒绝服务攻击。 &lt;br /&gt;
 &lt;br /&gt; &lt;strong&gt;搜索API&lt;/strong&gt; &lt;br /&gt;
 &lt;br /&gt;请求： &lt;br /&gt;
 &lt;pre&gt;GET /api/v1/search?q=&amp;amp;page_id=  &lt;br /&gt;
X-API-Key: api_key  &lt;br /&gt;
Authorization: auth_token  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
通过标题搜索视频。 &lt;br /&gt;
 &lt;br /&gt;响应： &lt;br /&gt;
 &lt;pre&gt;200 OK  &lt;br /&gt;
{  &lt;br /&gt;
page_id:  &lt;br /&gt;
next_page_id:  &lt;br /&gt;
videos: [  &lt;br /&gt;
{  &lt;br /&gt;
  id:  &lt;br /&gt;
  title:  &lt;br /&gt;
  summary:  &lt;br /&gt;
  url:  &lt;br /&gt;
  watched_till:  &lt;br /&gt;
},...  &lt;br /&gt;
]  &lt;br /&gt;
}   &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
HTTP状态码200表示操作成功，响应中包括了id、title、summary、url和watched_till等信息，不过也有可能找不到相关视频。 &lt;br /&gt;
 &lt;br /&gt; &lt;strong&gt;获取视频&lt;/strong&gt; &lt;br /&gt;
 &lt;br /&gt;请求： &lt;br /&gt;
 &lt;pre&gt;GET /api/v1/videos/:video_id  &lt;br /&gt;
X-API-Key: api_key  &lt;br /&gt;
Authorization: auth_token  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
 &lt;br /&gt;播放特定视频。 &lt;br /&gt;
 &lt;br /&gt;响应： &lt;br /&gt;
 &lt;pre&gt;200 OK  &lt;br /&gt;
{  &lt;br /&gt;
id:  &lt;br /&gt;
title:  &lt;br /&gt;
summary:  &lt;br /&gt;
url:  &lt;br /&gt;
watched_till:  &lt;br /&gt;
}   &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
HTTP状态码200表示匹配到了视频。 &lt;br /&gt;
 &lt;br /&gt;其他故障状态码： &lt;br /&gt;
 &lt;pre&gt;401 Unauthorized  &lt;br /&gt;
404 Video not found  &lt;br /&gt;
429 Too many requests  &lt;br /&gt;
500 Internal server error  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
 &lt;strong&gt;上传API&lt;/strong&gt; &lt;br /&gt;
 &lt;br /&gt;请求： &lt;br /&gt;
 &lt;pre&gt;POST /api/v1/videos  &lt;br /&gt;
X-API-Key: api_key  &lt;br /&gt;
Authorization: auth_token  &lt;br /&gt;
{  &lt;br /&gt;
title:  &lt;br /&gt;
summary:  &lt;br /&gt;
censor_rating:  &lt;br /&gt;
video_contents:  &lt;br /&gt;
}   &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
从后台上传视频。 &lt;br /&gt;
 &lt;br /&gt;响应： &lt;br /&gt;
 &lt;pre&gt;202 Accepted  &lt;br /&gt;
{  &lt;br /&gt;
video_url:  &lt;br /&gt;
}   &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
HTTP状态代码202表示视频已经排队进行异步处理和质量检查，处理结果可以通过电子邮件或其他告警机制发送给用户。 &lt;br /&gt;
 &lt;br /&gt;一些HTTP失败的场景： &lt;br /&gt;
 &lt;pre&gt;401 Unauthorized  &lt;br /&gt;
400 Bad request  &lt;br /&gt;
500 Internal server error  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
 &lt;strong&gt;更新观看时间戳&lt;/strong&gt; &lt;br /&gt;
 &lt;br /&gt;请求： &lt;br /&gt;
 &lt;pre&gt;PUT /api/v1/videos/:video_id/watched_till  &lt;br /&gt;
X-API-Key: api_key  &lt;br /&gt;
Authorization: auth_token  &lt;br /&gt;
{  &lt;br /&gt;
watched_till:  &lt;br /&gt;
}   &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
之所以使用HTTP PUT方法，是因为我们需要用其他数据替换同一个数据库表中的行条目，或者我们需要更新服务器上的资源。这个API将用于更新时间戳，直到用户看完了特定的视频。 &lt;br /&gt;
 &lt;br /&gt;响应： &lt;br /&gt;
 &lt;pre&gt;200 OK  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
HTTP状态码200表示操作成功。 &lt;br /&gt;
 &lt;br /&gt;其他HTTP失败状态码： &lt;br /&gt;
 &lt;pre&gt;401 Unauthorized  &lt;br /&gt;
400 Bad request  &lt;br /&gt;
500 Internal server error  &lt;br /&gt;
&lt;/pre&gt; &lt;br /&gt;
 &lt;h3&gt;微服务架构&lt;/h3&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/b3eedacf42b6159fe16db50f37b85b50.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="28.png" src="http://dockone.io/uploads/article/20211227/b3eedacf42b6159fe16db50f37b85b50.png" title="28.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/d14e49f1672de7ef388aef4678d41d1d.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="29.jpeg" src="http://dockone.io/uploads/article/20211227/d14e49f1672de7ef388aef4678d41d1d.jpeg" title="29.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
由于服务中的任何更改都可以很容易完成，因此微服务可以更快的完成部署。可以跟踪每个服务的性能，如果有任何问题，则可以快速将其与其他正在运行的服务隔离开来。 &lt;br /&gt;
 &lt;ol&gt;  &lt;li&gt;关键服务——为经常与该服务交互的用户提供服务。这些服务独立于其他服务，以便在进行任何故障转移时，用户可以继续执行基本操作。&lt;/li&gt;  &lt;li&gt;无状态服务——向客户端提供API请求，即使有任何服务器出现故障，也可以继续与其他实例一起工作，从而确保高可用性。例如，REST API服务为最多的用户提供服务。&lt;/li&gt;&lt;/ol&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/bc878a34b0fd759df10460080676819f.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="30.png" src="http://dockone.io/uploads/article/20211227/bc878a34b0fd759df10460080676819f.png" title="30.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h4&gt;上传内容&lt;/h4&gt;上传的内容是视频格式的电影或剧集，处理单元包括了输入协议、输入编解码器、输出编解码器和输出协议，以服务于各种设备和不同的网络速度。当我们在高速网络上观看视频时，视频的质量很好。Netflix为同一部电影创建不同分辨率的多个副本（大约1100-1200个）。Netflix将原始视频分成不同的小块，并在AWS中使用并行工作单元将这些小块转换成不同的格式。这些处理单元用于编码或转码，即将视频从一种格式转换为另一种格式，如改变分辨率，高宽比，减少文件大小等。在转码之后，一旦我们拥有同一电影的多个文件副本，这些文件就被传输到Open connect服务器。 &lt;br /&gt;
 &lt;h3&gt;系统架构概要设计&lt;/h3&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/57d5ecabd298499392eef0bb36d29539.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="31.png" src="http://dockone.io/uploads/article/20211227/57d5ecabd298499392eef0bb36d29539.png" title="31.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h3&gt;数据架构&lt;/h3&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/6d0a2499f6fc017b6b3bfd8cbaab4373.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="32.png" src="http://dockone.io/uploads/article/20211227/6d0a2499f6fc017b6b3bfd8cbaab4373.png" title="32.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/c9b47936921c41ee69e9b87abcada22c.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="33.png" src="http://dockone.io/uploads/article/20211227/c9b47936921c41ee69e9b87abcada22c.png" title="33.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/60923ade505460b5d8a17e593e1bea84.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="34.jpeg" src="http://dockone.io/uploads/article/20211227/60923ade505460b5d8a17e593e1bea84.jpeg" title="34.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h3&gt;电影推荐&lt;/h3&gt; &lt;ul&gt;  &lt;li&gt;电影推荐使用Apache Spark和机器学习。当载入所观看的首页时，会有好几行不同类型的电影。&lt;/li&gt;  &lt;li&gt;Netflix希望用户最大限度的点击视频，而这些点击取决于标题图像。Netflix必须为特定的视频选择正确的引人注目的标题图像。为了做到这一点，Netflix为一部特定的电影创建了多个艺术作品，并随机向用户展示这些图像。对于同一部电影，不同的用户可以使用不同的图像。根据用户的喜好和观看历史，Netflix会预测用户最喜欢哪类电影，或者最喜欢哪位演员。Netflix将根据用户的口味，显示合适的图像。&lt;/li&gt;  &lt;li&gt;Netflix会分析数据，从而决定应该向用户展示什么样的电影，这是基于用户的历史数据和偏好计算的。此外，Netflix还会对电影进行排序，并计算这些电影在其平台上的相关性排名。大多数机器学习流水线都运行在这些大型Spark集群上，然后使用这些流水线进行选择、排序、标题相关性排名和艺术品个性化等操作。当用户打开Netflix的首页时，用户就会被每个视频显式的图像所吸引。&lt;/li&gt;  &lt;li&gt;现在，Netflix还会计算特定图像被点击的次数。如果电影的中心图像的点击量是1500次，而其他图像的点击量更少，那么Netflix就会让中心图像永远作为电影《心灵捕手》的标题图像。这被称为数据驱动，Netflix用这种方法执行数据分析。为了做出正确的决策，需要根据与每张图片关联的访问数量计算数据。&lt;/li&gt;  &lt;li&gt;用户与服务的交互（观看历史记录以及评价）。&lt;/li&gt;  &lt;li&gt;有相似品味和喜好的其他用户。&lt;/li&gt;  &lt;li&gt;用户先前观看的视频的元数据信息，如标题、类型、类别、演员、发行年份等。&lt;/li&gt;  &lt;li&gt;用户的设备、活跃时间以及活跃时长。&lt;/li&gt;  &lt;li&gt;   &lt;br /&gt;两种类型的算法：     &lt;br /&gt;
   &lt;ul&gt;    &lt;li&gt;协同过滤算法：这种算法的思想是，如果两个用户有相似的评级历史，那么他们将在未来的行为相似。例如，假设有两个人，一个人喜欢这部电影，给它打了高分，那么另一个人也很可能会喜欢这部电影。  &lt;/li&gt;    &lt;li&gt;基于内容的推荐：这个算法的思想是，过滤那些与用户之前喜欢的视频相似的视频。基于内容的过滤高度依赖于电影名称、发行年份、演员、类型等信息。因此，要实现这种过滤，重要的是要知道描述每个项目的信息，还需要一些描述用户喜好的用户配置文件。&lt;/li&gt;&lt;/ul&gt;&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211227/e43f4fd89eabbd660595d88b3175fad8.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="35.png" src="http://dockone.io/uploads/article/20211227/e43f4fd89eabbd660595d88b3175fad8.png" title="35.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
译文链接： &lt;a href="https://mp.weixin.qq.com/s/NukLmTlhbGVXQ5SnovajZQ" rel="nofollow" target="_blank"&gt;https://mp.weixin.qq.com/s/NukLmTlhbGVXQ5SnovajZQ&lt;/a&gt;
                                                                 &lt;div&gt;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                &lt;/div&gt;
                                
                                                                 &lt;ul&gt;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            &lt;/ul&gt;
                                                            &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/61973-netflix-%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84-%E8%AE%BE%E8%AE%A1</guid>
      <pubDate>Mon, 27 Dec 2021 13:17:11 CST</pubDate>
    </item>
    <item>
      <title>从0开始设计Twitter系统架构</title>
      <link>https://itindex.net/detail/61966-%E8%AE%BE%E8%AE%A1-twitter-%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84</link>
      <description>&lt;br /&gt;【编者的话】Twitter是全球最大的社交网络之一，如果让我们从0开始设计twitter的系统架构，该怎么做呢？有哪些服务是必须的？有哪些点需要提前考虑？这篇文章简单介绍了设计类twitter系统的思路并在最后给出了参考设计。原文： &lt;a href="https://medium.com/interviewnoodle/twitter-system-architecture-8dafce16aec4"&gt;Twitter System Architecture&lt;/a&gt;。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/e939b1343aae112cf0746565eb731631.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="1.png" src="http://dockone.io/uploads/article/20211219/e939b1343aae112cf0746565eb731631.png" title="1.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
Twitter是全球领先的在线社交网络服务，用户可以在这里发布和阅读被称为“推文（tweets）”的短消息。在系统架构设计面试过程中，当被问及如何设计Twitter时，大多数候选人都会将其设计为单体服务。然而，将Twitter这样的大型服务设计为单体，表明候选人缺乏设计分布式系统的经验。从微服务甚至lambda（或函数）的角度来设计分布式系统在今天是很正常的选择。目前的趋势是，没有人会将新服务设计为单体，公司正逐渐将其庞大的单体服务转换为一组微服务。因此，候选人应该以微服务的方式设计Twitter。 &lt;br /&gt;
 &lt;h3&gt;功能需求&lt;/h3&gt; &lt;ol&gt;  &lt;li&gt;用户可以发布或分享新的推文（tweet）&lt;/li&gt;  &lt;li&gt;每条推文最多不超过140个字符&lt;/li&gt;  &lt;li&gt;用户可以删除推文，但不能更新/编辑发布的推文（写操作）&lt;/li&gt;  &lt;li&gt;户可以标记喜欢的推文（写操作）&lt;/li&gt;  &lt;li&gt;用户可以关注或取消关注另一个用户（写操作），关注一个用户意味着用户可以看到其他用户在他的时间线上的推文&lt;/li&gt;  &lt;li&gt;可以生成两种类型的时间线（读操作），用户时间线由他最后N个推文组成，主页时间线由他正在关注的用户的热门推文按照时间降序生成&lt;/li&gt;  &lt;li&gt;用户可以根据关键字搜索推文（读操作）&lt;/li&gt;  &lt;li&gt;用户需要有一个帐户来发布或读取推文（暂时使用外部身份服务）&lt;/li&gt;  &lt;li&gt;用户可以注册和删除帐户&lt;/li&gt;  &lt;li&gt;Twitter支持包含文字和图片/视频的推文，但在我们当前的设计中，将只支持文本&lt;/li&gt;  &lt;li&gt;分析/监视服务，以确定其负载、运行状况和功能&lt;/li&gt;  &lt;li&gt;分析还可为用户提供关于关注谁、推文通知、热门话题、推送通知和分享推文的意见或建议&lt;/li&gt;&lt;/ol&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/3eaaa0d991d9d293fd786be6d862320e.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="2.png" src="http://dockone.io/uploads/article/20211219/3eaaa0d991d9d293fd786be6d862320e.png" title="2.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h3&gt;非功能需求&lt;/h3&gt; &lt;ol&gt;  &lt;li&gt;服务的高可用是最重要的需求，这意味着用户可以在自己的主页时间线上阅读推文，而感受不到任何停顿&lt;/li&gt;  &lt;li&gt;生成时间线的时间最长不得超过半秒&lt;/li&gt;  &lt;li&gt;不需要强一致性，只需要最终一致性，可以使用关键词数据库用于搜索基于关键词的推文&lt;/li&gt;  &lt;li&gt;随着用户和推文的增加，系统负载也在增加，因此系统应该具有可伸缩性&lt;/li&gt;  &lt;li&gt;持久化用户数据&lt;/li&gt;&lt;/ol&gt; &lt;br /&gt;
 &lt;br /&gt;现在我们来做一些计算。 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;日活跃用户平均请求/天 = 150M*60/86400 = 100k/秒&lt;/li&gt;  &lt;li&gt;峰值用户 = 平均并发用户* 3 = 300k&lt;/li&gt;  &lt;li&gt;三月内最大峰值用户数 = 峰值用户数*2 = 600k&lt;/li&gt;  &lt;li&gt;读QPS = 300k&lt;/li&gt;  &lt;li&gt;写QPS = 5k&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/6129cc1e9990b40e2e0142874d6836be.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="3.jpeg" src="http://dockone.io/uploads/article/20211219/6129cc1e9990b40e2e0142874d6836be.jpeg" title="3.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h3&gt;twitter服务的概要设计&lt;/h3&gt;由于系统的复杂性，可以将其划分为若干个服务，其中包括若干个微服务。 &lt;br /&gt;
 &lt;ol&gt;  &lt;li&gt;推文服务（Tweet service）&lt;/li&gt;  &lt;li&gt;用户时间线服务（User timeline service）&lt;/li&gt;  &lt;li&gt;扇出服务（Fanout Service）&lt;/li&gt;  &lt;li&gt;主页时间线服务（Home timeline service）&lt;/li&gt;  &lt;li&gt;社交网络服务（Social graph service）&lt;/li&gt;  &lt;li&gt;搜索服务（Search service）&lt;/li&gt;&lt;/ol&gt; &lt;br /&gt;
 &lt;br /&gt;下面是Twitter服务中不同逻辑组件或微服务架构。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/d6d8eaf374735f698591b36e237803b8.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="4.png" src="http://dockone.io/uploads/article/20211219/d6d8eaf374735f698591b36e237803b8.png" title="4.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h3&gt;twitter服务的详细设计&lt;/h3&gt;所有微服务都可以被称为模块。 &lt;br /&gt;
 &lt;h4&gt;推文服务（Tweet service）&lt;/h4&gt; &lt;ul&gt;  &lt;li&gt;接收用户推文，转发用户推文到关注者时间线和搜索服务&lt;/li&gt;  &lt;li&gt;存储用户信息，推文信息，包括用户的推文数量以及用户喜欢的状态&lt;/li&gt;  &lt;li&gt;包括应用服务器、分布式的内存缓存以及后端的分布式数据库，或者使用直接由数据库（例如Redis）支持的内存缓存&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/f9bf33bb631ca99ebe73809b251618e7.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="5.png" src="http://dockone.io/uploads/article/20211219/f9bf33bb631ca99ebe73809b251618e7.png" title="5.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
然后我们看一下tweet服务的数据库表结构。 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/46bbf5f317917311662687007d769926.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="6.png" src="http://dockone.io/uploads/article/20211219/46bbf5f317917311662687007d769926.png" title="6.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
用户（Users）表包含用户的所有信息，推文（Tweet）表存储所有推文，Favorite_tweet表存储了喜欢的推文记录，也就是说，每当用户喜欢一条推文时，就会在Favorite_tweet表中插入一条记录。 &lt;br /&gt;
 &lt;h4&gt;生成唯一的推文Id&lt;/h4&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/17e70207b1bc1345769dff954b5ee2b1.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="7.jpeg" src="http://dockone.io/uploads/article/20211219/17e70207b1bc1345769dff954b5ee2b1.jpeg" title="7.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
当用户调用postTweet()时，调用会发送给应用服务器。应用服务器为该推文生成一个唯一的id，同样的机制也可以用来为推文生成短URL。另一个方式是基于应用服务器的UUID（Universally unique identifier）。推文ID生成后，应用服务器将该推文插入分布式缓存和数据库的tweet表中。由于需要在执行推文的创建/更新/删除操作的同时更新缓存和数据库，所以我们使用缓存透写机制。 &lt;br /&gt;
 &lt;h4&gt;可扩展性设计&lt;/h4&gt;我们可以将分布式缓存和数据库划分为多个分区和副本。 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;基于用户ID分片&lt;/li&gt;  &lt;li&gt;基于推文ID分片&lt;/li&gt;  &lt;li&gt;基于用户ID和推文ID进行两层/级别分片&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/d710a76e52e64378ea08382f7caa4a87.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="8.jpeg" src="http://dockone.io/uploads/article/20211219/d710a76e52e64378ea08382f7caa4a87.jpeg" title="8.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/59a6eaef6d68695edcd12e1ad11edcee.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="9.jpeg" src="http://dockone.io/uploads/article/20211219/59a6eaef6d68695edcd12e1ad11edcee.jpeg" title="9.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h4&gt;社交网络服务（Social graph service）&lt;/h4&gt; &lt;ul&gt;  &lt;li&gt;实现Following API，跟踪用户之间的关注关系&lt;/li&gt;  &lt;li&gt;包括应用服务器、分布式缓存和数据库&lt;/li&gt;  &lt;li&gt;用于存储用户关系的数据库表结构&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/4db40df55e5168a9bc70fb2f0173ca79.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="10.png" src="http://dockone.io/uploads/article/20211219/4db40df55e5168a9bc70fb2f0173ca79.png" title="10.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
Following API： &lt;br /&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;li&gt;异步的缺点是用户在取消关注后，如果刷新信息事件流，会发现这些信息仍然存在，但最终它们会被删除&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;h4&gt;用户时间线服务（User timeline service）&lt;/h4&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;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/1a21f911036b50b39b0eb1e82ea3c526.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="11.png" src="http://dockone.io/uploads/article/20211219/1a21f911036b50b39b0eb1e82ea3c526.png" title="11.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;当用户发布一条推文时，tweet服务调用用户时间线服务，将该推文插入到用户时间线的推文列表顶部，运算复杂度为O(1)。&lt;/li&gt;  &lt;li&gt;此外，分析仪表板可以配置参数K，表示可以保留的推文个数，K默认为1000，表示保留用户时间线轴中的最后K条推文。&lt;/li&gt;  &lt;li&gt;在用户时间线列表中，推文按creationTime（创建时间）降序存储。当用户时间线列表达到最大K条推文时，最老的条目将被删除。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;h4&gt;扇出服务（Fanout Service）&lt;/h4&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;br /&gt;
 &lt;br /&gt; &lt;h4&gt;主页时间线服务（Home timeline service）&lt;/h4&gt; &lt;ul&gt;  &lt;li&gt;显示用户的主页时间线&lt;/li&gt;  &lt;li&gt;包括来自其他关注的用户的推文，按照推文的creationTime（创建时间）降序显示。&lt;/li&gt;  &lt;li&gt;其设计类似于用户时间线服务。&lt;/li&gt;  &lt;li&gt;但是比用户时间线服务稍微复杂一点，因为用户将插入最新的推文，并且当推文数量超过K值时需要删除最老的推文，如果用户关注了很多其他用户，服务还需要一些机制来给不同关注用户的推文赋予不同的权重。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;h4&gt;搜索服务（Search service）&lt;/h4&gt; &lt;ul&gt;  &lt;li&gt;为用户提供搜索查询服务&lt;/li&gt;  &lt;li&gt;扇出服务将推文传递给搜索服务&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/7b69b7d1af4fb02efade52a554b36ce0.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="12.png" src="http://dockone.io/uploads/article/20211219/7b69b7d1af4fb02efade52a554b36ce0.png" title="12.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;ul&gt;  &lt;li&gt;Ingester（或ingestion engine）：给推文标记上许多标签、术语或关键字。例如这条推文：“我想成为像亚马逊的杰夫·贝索斯一样非常富有的人”，它会过滤掉那些在搜索中没有用的词。除了杰夫·贝索斯（Jeff Bezos）和亚马逊（Amazon），所有其他词都将被丢弃。Ingester可以通过配置或数据库获得词汇表。&lt;/li&gt;  &lt;li&gt;一个叫做“词根提取（stemming）”的过程对剩下的单词进行分析，以确定它们的词根。Stemming是处理词干、词根或词根的词形变化(或派生)的过程。因此，会在数据库中保存一个查找表。这种方法的优点是可以简单、快速、轻松的处理异常。缺点是新的或不熟悉的单词即使是完全符合规则的，也不会被处理。&lt;/li&gt;  &lt;li&gt;传递到搜索索引&lt;/li&gt;  &lt;li&gt;搜索索引微服务将创建反向索引，并存储从内容(如单词)到其所在文档或一组文档中的位置的术语映射索引，在我们的例子中，这是一个或一组推文。&lt;/li&gt;  &lt;li&gt;Blender服务：在twitter平台上为用户提供搜索查询。当请求搜索查询时，首先确定搜索条件，然后进行词干分析，最后使用词根在术语的倒排索引上运行搜索查询。&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;h4&gt;照片和视频&lt;/h4&gt; &lt;ul&gt;  &lt;li&gt;使用NoSQL数据库&lt;/li&gt;  &lt;li&gt;媒体文件（使用文件系统）&lt;/li&gt;  &lt;li&gt;数据表格式&lt;/li&gt;&lt;/ul&gt; &lt;br /&gt;
 &lt;br /&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/17817fd98a8e5fca05a7973d2193039a.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="13.png" src="http://dockone.io/uploads/article/20211219/17817fd98a8e5fca05a7973d2193039a.png" title="13.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h3&gt;Twitter的网络&lt;/h3&gt; &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/f8ca7839ce0d87ce507bd3139ec595b7.jpeg" rel="lightbox" target="_blank"&gt;   &lt;img alt="14.jpeg" src="http://dockone.io/uploads/article/20211219/f8ca7839ce0d87ce507bd3139ec595b7.jpeg" title="14.jpeg"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;h3&gt;Twitter的最终详细设计&lt;/h3&gt;系统设计： &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/88fe98c7f9a44894fab29ace63e3823f.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="15.png" src="http://dockone.io/uploads/article/20211219/88fe98c7f9a44894fab29ace63e3823f.png" title="15.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
数据架构： &lt;br /&gt;
 &lt;div&gt;
  &lt;a href="http://dockone.io/uploads/article/20211219/845e01ee48177b18957cb494e860d0a9.png" rel="lightbox" target="_blank"&gt;   &lt;img alt="16.png" src="http://dockone.io/uploads/article/20211219/845e01ee48177b18957cb494e860d0a9.png" title="16.png"&gt;&lt;/img&gt;&lt;/a&gt;
&lt;/div&gt;
 &lt;br /&gt;
 &lt;br /&gt;译文链接： &lt;a href="https://mp.weixin.qq.com/s/v08_YF78Im-Z_Qy8iX3yMQ" rel="nofollow" target="_blank"&gt;https://mp.weixin.qq.com/s/v08_YF78Im-Z_Qy8iX3yMQ&lt;/a&gt;
                                                                 &lt;div&gt;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                &lt;/div&gt;
                                
                                                                 &lt;ul&gt;
                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    &lt;/ul&gt;
                                                            &lt;div&gt; &lt;a href="https://itindex.net/"  title="IT 资讯"&gt;&lt;img src="https://itindex.net/images/iconWarning.gif" title="IT 资讯" border="0"/&gt; &lt;/a&gt;</description>
      <category />
      <guid isPermaLink="true">https://itindex.net/detail/61966-%E8%AE%BE%E8%AE%A1-twitter-%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84</guid>
      <pubDate>Mon, 20 Dec 2021 00:39:18 CST</pubDate>
    </item>
  </channel>
</rss>

