<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
            <title type="text">源来如此-张豫湘</title>
            <subtitle type="text">搬砖人</subtitle>
    <updated>2026-03-09T10:36:21+08:00</updated>
        <id>https://0522-isniceday.top</id>
        <link rel="alternate" type="text/html" href="https://0522-isniceday.top" />
        <link rel="self" type="application/atom+xml" href="https://0522-isniceday.top/atom.xml" />
    <rights>Copyright © 2026, 源来如此-张豫湘</rights>
    <generator uri="https://halo.run/" version="1.4.2">Halo</generator>
            <entry>
                <title><![CDATA[用户增长]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/yong-hu-zeng-chang" />
                <id>tag:https://0522-isniceday.top,2025-04-14:yong-hu-zeng-chang</id>
                <published>2025-04-14T10:30:22+08:00</published>
                <updated>2025-04-14T10:30:34+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="1-用户增长是什么">1. 用户增长是什么</h1><h2 id="11-是什么">1.1. 是什么</h2><p><strong>简单来说，用户增长（User Growth/UG）的根本目的是为了扩大产品的用户群，为企业带来收入和利润增长，进而实现商业成功。</strong></p><p><strong>由业产研、数据等多部门协同，围绕产品的三个阶段（产品前的分析、产品中的运营、产品后的突破），实现用户的拉新、促活、留存、变现、裂变的高效率、低成本的增长体系</strong></p><h2 id="12-用户增长的三个阶段">1.2. 用户增长的三个阶段</h2><ol><li>产品出现之前的用户增长：洞察用户需求，找到驱动用户增长的根本因素</li><li>产品生命周期内的用户增长：有了产品之后，快速引爆用户增长</li><li>突破产品生命周期的用户增长：开启用户增长的第二曲线</li></ol><p>通过对于上面三个阶段的不断循环，不断开创app新的增长曲线</p><h1 id="2-如何实现用户增长">2. 如何实现用户增长</h1><h2 id="21-建目标">2.1. 建目标</h2><h3 id="211-第一步找到产品的核心价值">2.1.1. 第一步：找到产品的核心价值</h3><p>核心价值可以理解为，产品使用时有眼前一亮的感觉的时刻，说明用户真正发现了产品的核心价值 -- 产品为何存在、他们为何需要它以及他们能从中获得什么的时刻。正是在“啊哈时刻”，用户认识到了这个产品对他们来说为什么不可或缺</p><p>可以考虑用如下手段挖掘：</p><ol><li>不可或缺性调查：向产品的活跃用户询问如果无法继续使用产品的“失望”程度，是否存在完全可替代的竞品等。</li><li>衡量用户留存率：如果留存率不稳定或者留存率非常低，说明产品仍然没有核心用户群。</li><li>实地到访客户</li></ol><h3 id="212-第二步明确增长杠杆">2.1.2. 第二步：明确增长杠杆</h3><p>增长杠杆：识别与增长相关的关键因素以及衡量指标，明确发力点和方向</p><p>明确过程可以大致划分如下：</p><ol><li>详细的数据调研：在建立基本增长等式并确定北极星指标之前，前提是能够收集用户行为数据并衡量产品表现和实验结果。没有科学全面的数据分析能力支撑，所有对产品表现和实验结果的假设都是无从验证的。因此首先需要整合所有的数据资源，对用户整个体验过程进行明确的跟踪（每个环节都有不同的指标，见1.2）。</li><li>建议基本增长等式：基本增长等式（fundamental growth equation）是一个汇总了所有与增长相关的关键因素的简单等式，这些因素共同驱动企业的增长。基本增长等式是增长杠杆的集合，注意每个产品或企业的等式很可能是不相同的。</li></ol><p>例如 eBay 的等式如下：</p><p><strong>发布物品的卖家数量 x 发布物品的数量 x 买家数量 x 成功交易数量 = 总商品增长数量</strong></p><p><strong>一句话总结下：</strong></p><p><strong>增长等式可以简单理解为产品中不同用户角色的关键（核心）行为，例如HK钱包而言，最核心的业务行为就是用户支付，商户入驻等，那么增长杠杆明显就是：用户支付 x 入驻商户数</strong></p><ol><li>确定“北极星指标”</li></ol><p>建立基本增长等式后，为了缩小关注范围，最好能够选择一个关键的能够决定最终成败的指标，用以指导后续所有的增长活动。增长黑客们一般将这样的指标称为“北极星指标”，意味着这个指标会向北极星一样指引团队持续关注长期可持续的用户增长，不会被一个短期增长手段蒙蔽双眼。</p><p><strong>北极星指标应该能够精准捕捉到企业为用户创造的核心价值。要确定这个指标，可以关注基本增长等式中与“产品核心价值”最密切相关的变量</strong>。</p><p>简单来说就是两个点：</p><ol><li>反映业务价值</li><li>容易沟通</li><li>顺应业务阶段</li></ol><p>例如：HK钱包的“北极星指标”指标就可以为用户TPV</p><h2 id="22-定模型">2.2. 定模型</h2><h3 id="221-用户增长的生命周期">2.2.1. 用户增长的生命周期</h3><p>AARRR 模型（又称海盗模型、增长黑客模型），它从生命周期的角度描述了用户进入平台需经历的五个环节：</p><ol><li><p>拉新：用户从不同渠道来使用你的产品 -- 拉新/推广</p></li><li><ol><li>日新增用户数（<strong>DNU</strong>）</li><li>用户获客成本：所有推广运营费用总和与新获取用户的数量的比值</li></ol></li><li><p>促活：用户在你的产品上体验到了<strong>核心价值</strong> -- 促活</p></li><li><ol><li>日活/月活</li><li>日均使用市场</li></ol></li><li><p>留存：用户回来继续不断的使用你的产品 -- 留存</p></li><li><ol><li><strong>留存率（Retention Ratio）：<strong>某段时间内新增用户数，经过一段时间后，仍然使用的用户占新增用户的比率。一般关注</strong>次日留存率</strong>、<strong>三日留存率</strong>、<strong>七日留存率</strong>等</li><li><strong>流失率（Churn Ratio）</strong>：某段时间范围内流失的用户数量，除以这段时间开始时的用户总数即为流失率。注意不同产品对“流失”的定义是不同的。</li></ol></li><li><p>变现：用户在你的产品上发生了可使你收益的行为</p></li><li><ol><li><strong>付费率（PUR）</strong>：付费用户数占活跃用户的比例。</li><li><strong>平均用户收入（ARPU）</strong>：统计时间内，活跃用户产生的平均收入，一般以月计算</li></ol></li><li><p>裂变：用户通过你的产品，推荐引导他人来使用你的产品 -- 人传人</p></li><li><ol><li><strong>K 因子</strong>：自传播、病毒式传播中的重要指标，K = 每个用户发出的邀请数 x 接受到邀请的人转化为新用户的转化率。如果 K &gt; 1，用户会像滚雪球一样增大；当 K &lt; 1 时，用户群到一定规模时就会停止自传播增长</li></ol></li></ol><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1744461178146-8d1ff456-b732-45de-b295-20661ad40405.png" alt="img" /></p><h3 id="222-不断进行策略的ab实验">2.2.2. 不断进行策略的AB实验</h3><h1 id="3-总结">3. 总结</h1><p>从上述可以知道，创建一个新产品，例如AI Alisa，本质上是要用户来用，需要用户来获利的，因此完整的用户增长过程是公司内，推动一些事项的基础。</p><p>那么上面两个章节讲述了用户增长的周期，以及如何实现用户增长，再回顾用户增长的三个阶段：</p><ol><li>定目标：产品出现之前的用户增长，洞察用户需求，找到驱动用户增长的根本因素（第一步：找到产品核心价值+ 第二步：明确增长杠杆）</li><li>建模型：产品生命周期内的用户增长：有了产品之后，快速引爆用户增长（结合生命周期 + 第三步：不断进行策略的AB实验）</li><li>突破产品生命周期的用户增长：开启用户增长的第二曲线</li></ol><p>那么周期的划分和实现增长路径两者有什么关系呢？周期的划分是对所有产品的用户增长过程的用户生命周期的抽象划分，而每个阶段都是用户增长的一个环节，每个环节的结果都决定的用户增长的好坏。那增长路径的实现方案，更多的是指导如何挖掘增长点，明确增长目标和手段，应用于不同的增长阶段作为指导或者方向，因此我们可以先明确增长路径，再看如何在各个周期的阶段进行发力。</p><h1 id="4-实践举例">4. 实践举例</h1><h2 id="41-语音搜索">4.1. 语音搜索</h2><p>step-1 ：定目标：明确增长杠杆</p><p>通过数据分析，搜索产品本质是提供入口完成内容分发，因此搜索的增长等式：</p><p>搜索人数UV * 搜索次数PV * 搜索结果点击率CTR = 内容分发点击总数</p><p>为什么是内容分发点击总数而不是搜索结果点击率作为北极星指标，因为由增长等式可以知道，搜索结果点击率只是增长等式的一个因素，但是最终是为内容分发点击总数服务，例如只有转化率很高，但搜索人数UV、搜索次数PV很低，那么对端内的业务其实没有多大的助力</p><p>因此能够得到搜索的“北极星指标” = 内容分发点击总数</p><p>备注：这里的搜索指的是点击搜索按钮，走到结果搜索结果页面的简称</p><p>step-2 ：建立增长模型，从增长模型的不同环节去找与“语音搜索”相关的价值，其实就是看针对“北极星指标”，在增长模型的哪个环节，“语音搜索可以提升价值</p><p>此时，我们可以通过不同阶段的用户增长的周期去提供角度进行分析，通过不同的环节去进行发力，提升整体的“北极星指标”：</p><p>拉新：通过不同方式，吸引用户使用搜索，例如底纹词、热词等，提升搜索UV</p><p>促活：通过活动运营，例如积分任务等，让用户每天都进行搜索，提升搜索UV/PV</p><p>留存：通过长期运营活动，例如积分签到等，促进用户留存，提升搜索PV</p><p>变现：搜索广告、搜索能力商业化等，提升搜索转化率</p><p>裂变：搜索口令，提升搜索UV/PV</p><p>其中通过拉新环节的用户漏斗进行分析，我们发现搜索转化漏斗中发现用户在搜前到搜结果的过程中，发现不少用户在搜前页产生了流失，通过基于流失用户的分层分析，发现按照年龄分层后，流失的用户中大于40岁的用户较多，也就是大龄用户较多，然后基于香港app使用现状发现，存在较多的大龄用户在文字生成方面，习惯于使用语音，那么语音功能的如果能通过优化拉新的路径，减少中间流失，其实也是变向提升了搜索的““北极星指标”</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[向量检索]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/xiang-liang-jian-suo" />
                <id>tag:https://0522-isniceday.top,2025-04-12:xiang-liang-jian-suo</id>
                <published>2025-04-12T14:50:00+08:00</published>
                <updated>2025-04-25T11:15:31+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>目前较为流行的向量库：Milvus<br />核心组成：向量生成、检索方式、距离度量、索引构建、优化算法<br />向量生成算法<br />向量生成算法是将文本、图像等数据转换为向量表示的方法，常见的向量生成算法有以下几种：<br />词袋模型（Bag of Words，BOW）<br />● 原理：将文本看作一个袋子，忽略单词的顺序，只统计每个单词在文本中出现的次数。通过构建一个词汇表，将每个文本表示为一个向量，向量的维度等于词汇表的大小，向量中的每个元素对应词汇表中某个单词在该文本中的出现频率。<br />● 优点：简单直观，易于理解和实现，在一些文本分类等任务中能取得一定的效果。<br />● 缺点：忽略了单词的顺序和语义信息，无法捕捉文本中的语法和语义结构。<br />词嵌入（Word Embedding）<br />● 原理：将单词映射到低维向量空间中，使得语义上相似的单词在向量空间中距离较近。常见的词嵌入算法有Word2Vec、GloVe等。以Word2Vec为例，它通过在大规模文本语料上进行训练，利用神经网络模型学习单词的上下文信息，从而得到每个单词的向量表示。<br />● 优点：能够捕捉单词的语义和句法信息，生成的向量具有较好的语义相似性度量能力，在自然语言处理任务中广泛应用，如文本分类、命名实体识别等。<br />● 缺点：对于生僻词或未登录词的向量表示可能不准确，且词向量的维度通常较高，计算成本较大。<br />词向量平均法（Average Word Vectors）<br />● 原理：先使用词嵌入算法得到每个单词的向量表示，然后将文本中所有单词的向量进行平均，得到文本的向量表示。这种方法假设文本的语义可以通过其包含的单词的语义平均来近似。<br />● 优点：简单直接，在一些简单的文本分类和信息检索任务中能快速得到文本的向量表示。<br />● 缺点：忽略了单词在文本中的重要性差异，也没有考虑句子的语法结构和单词之间的语义关系。<br />TF-IDF算法<br />● 原理：TF-IDF是一种统计方法，用以评估一个字词对于一个文件集或一个语料库中的其中一份文件的重要程度。TF（Term Frequency）表示词在文档中出现的频率，IDF（Inverse Document Frequency）表示词在整个文档集合中的稀有程度。通过将TF和IDF相乘，得到每个词的TF - IDF值，以此来构建文本的向量表示。<br />● 优点：能够突出文本中的重要词汇，抑制常见词汇的影响，在信息检索和文本分类等任务中表现良好。<br />● 缺点：同样忽略了单词的顺序和语义信息，对于文本的语义理解能力有限。<br />BM25相较于TF-IDF<br />优点<br />● 考虑文档长度：通过引入文档长度的调节因子，能够更好地处理不同长度的文档，避免长文档因为词频高而获得过高的得分。<br />● 性能更优：在很多信息检索任务中，BM25 的性能通常优于 TF - IDF。<br />卷积神经网络（Convolutional Neural Network，CNN）<br />● 原理：在处理文本时，CNN通过卷积层和池化层对文本进行特征提取。卷积层使用多个卷积核在文本上滑动，提取局部特征，池化层则对卷积层提取的特征进行压缩和筛选，最终得到文本的向量表示。在处理图像时，CNN通过卷积层、池化层和全连接层等组件，自动提取图像的特征，将图像映射为向量。<br />● 优点：能够自动提取文本或图像的特征，无需人工设计特征工程，在图像识别、文本分类等任务中取得了显著的成果。<br />● 缺点：模型结构复杂，训练时间长，需要大量的标注数据。<br />循环神经网络（Recurrent Neural Network，RNN）及其变体（LSTM、GRU）<br />● 原理：RNN可以处理序列数据，它通过隐藏状态来记忆之前的信息，并在每个时间步更新隐藏状态，从而生成序列的向量表示。长短期记忆网络（Long - Short Term Memory，LSTM）和门控循环单元（Gated Recurrent Unit，GRU）是RNN的变体，它们通过引入门控机制来更好地控制信息的流动，解决了RNN中的梯度消失和长期依赖问题。<br />● 优点：特别适合处理具有顺序信息的数据，如文本和时间序列数据，能够有效地捕捉序列中的长期依赖关系。<br />● 缺点：计算复杂度较高，训练难度较大，处理长序列时可能会出现性能下降。<br />检索方式（算法）<br />向量如何存储就是索引构建算法<br />KNN<br />蛮力检索算法，胜于召回率但查询效率不高<br />ANN<br />是一种近似检索算法，需预先计算向量间的距离，并将距离相近的向量存储在一起，胜于检索速度但会有少量的精度牺牲<br />常见的包括：<br />HNSW：HNSW 是一种基于图的数据结构，它通过构建多层图来组织高维向量。在底层图中，每个节点与相邻的节点相连，随着层级的上升，节点之间的连接范围会逐渐扩大。在搜索时，从高层图开始快速定位到大致区域，然后逐步下降到低层图进行精确查找。<br />● 优点：搜索速度快，尤其是在大规模数据集上表现出色；支持动态插入和删除操作，能够适应数据的实时变化。<br />● 缺点：构建索引的时间和空间开销相对较大，需要存储图的结构信息。<br />IVF<br />PQ ：算法将高维向量分解为多个低维子向量，然后对每个子向量进行量化，将其映射到一个离散的码本中。在搜索时，通过比较查询向量和码本中的码字之间的距离，快速筛选出可能的候选向量。<br />● 优点：可以显著减少存储开销和搜索时间，尤其是在高维向量空间中表现出色。<br />● 缺点：由于量化过程会引入一定的误差，可能会影响搜索的准确性。</p><p><span A="">距离度量（召回匹配）<br />余弦距离<br />● 定义：余弦距离是通过计算两个向量夹角的余弦值来衡量它们的方向差异。余弦值的范围是 ([-1, 1])，两个向量方向完全相同，余弦值为 1；方向完全相反，余弦值为 -1；相互垂直时，余弦值为 0。<br />● 公式：对于两个 n 维向量 (\vec</span><span B="">=(A_1,A_2,\cdots,A_n)) 和 (\vec</span><span sim="">=(B_1,B_2,\cdots,B_n))，它们的余弦相似度 (\text</span><span A="">(\vec</span><span B="">,\vec</span><span sim="">)) 计算公式为： (\text</span><span A="">(\vec</span><span B="">,\vec</span><span A="">)=\frac{\vec</span><span B="">\cdot\vec</span><span A="">}{|\vec</span><span B="">||\vec</span><span i="1" n="">|}=\frac{\sum_</span><span i="1" n="">A_iB_i}{\sqrt{\sum_</span><span i="1" n="">A_i2}\sqrt{\sum_</span>B_i<sup n=""><span sim="">2}}) 余弦距离通常定义为 (1 - \text</span><span A="">(\vec</span><span B="">,\vec</span><span A="">))。<br />● 特点：余弦距离主要关注向量的方向，而不考虑向量的长度。这使得它在文本挖掘、信息检索等领域非常有用，因为在这些场景中，文档的长度可能不同，但只要它们的关键词分布相似，就可以认为它们是相似的。<br />● 使用场景<br />○ 信息检索：当用户在搜索引擎中输入关键词进行查询时，搜索引擎会将用户输入的关键词表示为一个向量，然后计算该向量与索引中每个文档向量的余弦距离。例如，用户搜索 “人工智能在医疗领域的应用”，搜索引擎会找到与该查询向量余弦距离较小的文档，这些文档通常与用户的查询主题相关度较高，然后将它们作为搜索结果返回给用户。<br />○ 文本分类：假设有一个已经标注好类别的文本数据集，如分为 “科技”“娱乐”“体育” 等类别。对于一篇新的待分类文本，将其表示为向量后，计算它与每个类别中代表性文本向量的余弦距离。新文本与哪个类别向量的余弦距离最小，就将其归为该类别。例如，一篇关于 “量子计算的最新研究进展” 的文章，通过计算余弦距离，它与 “科技” 类别中的文本向量更相似，因此会被分类到 “科技” 类别中<br />点积距离<br />● 定义：点积是两个向量对应元素乘积之和。点积的结果是一个标量，它反映了两个向量在方向上的一致性程度以及向量的长度信息。<br />● 公式：对于两个 n 维向量 (\vec</span><span B="">=(A_1,A_2,\cdots,A_n)) 和 (\vec</span><span A="">=(B_1,B_2,\cdots,B_n))，它们的点积 (\vec</span><span B="">\cdot\vec</span><span A="">) 计算公式为： (\vec</span><span B="">\cdot\vec</span><span i="1">=\sum_</span></sup><span A="">A_iB_i) 在一些情况下，为了将点积转换为距离度量，可以使用负点积或者进行归一化处理。<br />● 特点：点积考虑了向量的长度和方向，当两个向量的长度较长且方向相似时，点积的值会较大。在深度学习中，点积常用于计算神经网络中的权重和输入的乘积。<br />● 使用场景<br />○ 全连接层：在神经网络的全连接层中，输入向量与权重矩阵进行点积运算。例如，一个具有多个神经元的全连接层，每个神经元都有一组权重。假设输入是一个表示图像特征的向量，通过与每个神经元的权重向量进行点积，再加上偏置项，得到该神经元的输出。这个过程可以看作是对输入特征进行加权求和，从而实现对图像特征的提取和转换。<br />○ 注意力机制：在一些自然语言处理任务中，如机器翻译、文本摘要等，注意力机制会使用点积来计算查询向量与键向量之间的相似度得分。例如，在翻译句子时，通过计算当前单词与源语言句子中各个单词的点积得分，来确定当前单词对源语言中不同部分的注意力权重，从而更准确地生成翻译结果<br />欧式距离<br />● 定义：欧氏距离是最常见的距离度量方式，它表示在欧几里得空间中两个点之间的直线距离。在向量空间中，欧氏距离衡量了两个向量之间的绝对差异。<br />● 公式：对于两个 n 维向量 (\vec</span><span B="">=(A_1,A_2,\cdots,A_n)) 和 (\vec</span><span A="">=(B_1,B_2,\cdots,B_n))，它们的欧氏距离 (d(\vec</span><span B="">,\vec</span><span A="">)) 计算公式为： (d(\vec</span><span B="">,\vec</span><span i="1" n="">)=\sqrt{\sum_</span>(A_i - B_i)2})<br />● 特点：欧氏距离直观地反映了向量之间的空间距离，距离越小表示两个向量越相似。它在许多领域都有广泛应用，如聚类分析、模式识别等。<br />● 使用场景<br />○ 地理信息系统（GIS）：在地图应用中，需要计算两个地理位置之间的距离。例如，要计算从北京天安门（经纬度：39.915°N，116.397°E）到上海东方明珠塔（经纬度：31.248°N，121.498°E）的距离。可以将经纬度坐标看作是二维平面上的点，使用欧氏距离公式计算它们之间的直线距离（实际应用中可能需要考虑地球的曲率等因素进行修正，但欧氏距离提供了一个基本的计算方法）。<br />○ 机器人导航：机器人在一个已知的物理空间中移动，需要知道自己与目标物体或地点的距离。例如，机器人要前往房间内的一个特定位置，它通过传感器获取自身位置和目标位置的坐标信息，然后使用欧氏距离来计算从当前位置到目标位置的距离，以便规划移动路径<br />例如，在文本相似度计算中，余弦距离更合适；在衡量物理空间中两点的距离时，欧氏距离更常用；而点积则在神经网络等领域发挥着重要作用</p><p>稀疏检索/稠密检索<br />稀疏检索<br />● 基本概念：稀疏检索基于词袋模型，将文档和查询表示为稀疏向量。在这种表示中，向量的维度通常对应于整个词汇表中的单词，而向量的元素值表示该单词在文档或查询中出现的频率或其他相关统计信息（如 TF - IDF 值）。由于大多数文档只包含词汇表中一小部分单词，所以这些向量中大部分元素为零，呈现出稀疏的特点。<br />● 检索过程：常见的稀疏检索方法包括基于倒排索引的检索，倒排索引记录了每个单词在哪些文档中出现以及出现的位置等信息，通过查找倒排索引可以快速找到包含查询词的文档，然后再计算这些文档与查询的相似度进行排序。<br />● 优点：稀疏检索的优点是索引结构简单，易于理解和实现，能够快速地对大规模文档集合进行索引和检索。同时，它对于处理精确匹配的查询非常有效，能够准确地找到包含特定关键词的文档。<br />● 缺点：然而，稀疏检索也存在一些局限性。它过于依赖单词的精确匹配，对于语义相似但用词不同的查询和文档可能无法准确检索到。而且，由于只考虑了单词的出现频率等简单统计信息，忽略了单词之间的语义关系和上下文信息，所以在处理复杂的语义查询时效果可能不太理想</p><p>稠密检索<br />● 基本概念：稠密检索将文档和查询映射为低维的稠密向量，这些向量中的每个元素都有实际的数值，且通常通过深度学习模型等方式学习得到。与稀疏向量不同，稠密向量能够捕捉到文本的语义信息，将文本中的语义特征表示为向量空间中的向量，使得语义相似的文本在向量空间中的距离更近。<br />● 检索过程：在检索时，同样通过计算查询向量与文档向量之间的相似度（如余弦相似度、欧式距离等）来衡量文档与查询的相关性。由于稠密向量包含了更丰富的语义信息，所以能够更好地处理语义复杂的查询。例如，一些基于深度学习的稠密检索模型会将查询和文档同时输入到模型中，通过模型的编码器将它们分别编码为稠密向量，然后计算向量之间的相似度得分，根据得分对文档进行排序。<br />● 优点：稠密检索的主要优点是能够更好地处理语义信息，对于语义相似但表述不同的查询和文档有更好的检索效果。它能够利用深度学习模型学习到文本的深层语义特征，从而提高检索的准确性和召回率，尤其在处理复杂的自然语言查询时表现更优。<br />● 缺点：不过，稠密检索也有一些缺点。训练深度学习模型来生成稠密向量通常需要大量的标注数据和计算资源，训练过程较为复杂和耗时。而且，稠密向量的索引和检索过程相对稀疏检索来说更复杂，需要使用一些专门的向量索引结构和算法来提高检索效率。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[HBase底层架构]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/h-b-a-s-e-di-ceng-jia-gou" />
                <id>tag:https://0522-isniceday.top,2025-04-06:h-b-a-s-e-di-ceng-jia-gou</id>
                <published>2025-04-06T19:31:07+08:00</published>
                <updated>2025-04-06T19:31:07+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="1-hbase是什么">1. HBase是什么</h1><p>是一个分布式的、面向列存储的 NoSQL数据库，基于 Google 的 Bigtable 设计，用于处理海量的结构化数据。HBase 架构的独特性使其在大数据领域得到了广泛应用，主要用来存储非结构化和半结构化的松散数据</p><p>HBase 最早由 Apache Hadoop 的开发者开发，用于解决在 Hadoop <a href="https://cloud.tencent.com/product/chdfs?from_column=20065&amp;from=20065">分布式文件系统</a>（HDFS）上存储和检索大量数据时面临的挑战。传统的<a href="https://cloud.tencent.com/product/tencentdb-catalog?from_column=20065&amp;from=20065">关系型数据库</a>在处理大规模数据时效率低下，难以扩展。而 HBase 作为一个 NoSQL 数据库，提供了对大量数据的高效读写操作，并且具有高度的扩展性。</p><p>项目需求是构建一个可以处理数十亿条记录的大规模<a href="https://cloud.tencent.com/product/cdcs?from_column=20065&amp;from=20065">数据存储</a>系统，要求系统能够承载高并发的读写请求，同时在数据量急剧增长的情况下，系统性能不会显著下降。HBase 的设计正是为此类需求量身定制。</p><h1 id="2-hbase数据检索与存储">2. HBase数据检索与存储</h1><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1743408736660-3f2c34c0-2dff-4fcb-a87f-b0d9295dbc4f.png" alt="img" /></p><table><thead><tr><th>模块</th><th>职责</th></tr></thead><tbody><tr><td>HBase Client</td><td>HBase Client 为用户提供了访问 HBase 的接口，可以通过元数据表来定位到目标数据的 RegionServer，另外 HBase Client 还维护了对应的 cache 来加速 Hbase 的访问，比如缓存元数据的信息</td></tr><tr><td>HMaster</td><td>HMaster 是 HBase 集群的主节点，负责整个集群的管理工作，主要工作职责如下分配Region：负责启动的时候分配Region到具体的 RegionServer；负载均衡：一方面负责将用户的数据均衡地分布在各个 Region Server 上，防止Region Server数据倾斜过载。另一方面负责将用户的请求均衡地分布在各个 Region Server 上，防止Region Server 请求过热维护数据：发现失效的 Region，并将失效的 Region 分配到正常的 RegionServer 上，并且在Region Sever 失效的时候，协调对应的HLog进行任务的拆分。</td></tr><tr><td>Region Server<img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1743409646915-89b30b16-9a2b-4314-b94a-018414b4ebd0.png" alt="img" /></td><td>直接对接用户的读写请求，干活的节点，每一个Region server大约可以管理1000个region，处理对这些HRegion的IO请求，也就是说客户端直接和HRegionServer打交道，主要职责如下管理 HMaster 为其分配的 Region；负责与底层的 HDFS 交互，存储数据到 HDFS；负责 Region 变大以后的拆分以及 StoreFile 的合并工作。</td></tr><tr><td>Region（table）</td><td>Region Server 包含多个Region。每一个 Region 都有起始 RowKey 和结束 RowKey，代表了存储的Row的范围，保存着表中某段连续的数据。一开始Region可能只有一个，随着数据增多（StoreFile变大,默认256M），进行Region的水平切分，分成了多个Region。当 Region 很多时，HMaster 会将 Region 保存到其他 Region Server 上</td></tr><tr><td>HLog（WAL）</td><td>每个Region Server会有一个HLog，负责记录着数据的操作日志，当HBase出现故障时可以进行日志重放、故障恢复。例如故障时MemStore没有写会磁盘</td></tr><tr><td>Store（列蔟）</td><td>一个 Region 包含多个 Store ，每个 Store 都对应一个 Column Family, Store 包含 MemStore 和 StoreFile</td></tr><tr><td>MemStore（Store的内存存储）</td><td>数据的写操作会先写到 MemStore 中，当MemStore 中的数据增长到一个阈值（默认64M）后。Region Server 会启动 flasheatch 进程将 MemStore 中的数据写人 StoreFile 持久化存储，每次写入后都形成一个单独的 StoreFile。找数据会先从MemStore开始找，找不到才去StoreFile</td></tr><tr><td>StoreFile（HFile）</td><td>StoreFile底层是以 HFile 的格式保存。HBase以Store的大小来判断是否需要切分Region</td></tr><tr><td>HFile是HBase中KeyValue数据的存储格式，是hadoop的二进制格式文件。一个StoreFile对应着一个HFile。而HFile是存储在HDFS之上的</td><td> </td></tr><tr><td>Zookeeper</td><td>HBase 通过 ZooKeeper 来完成选举 HMaster、监控 Region Server、维护元数据集群配置等工作，主要工作职责如下选举Master：如果 HMaster 异常，则会通过选举机制（写入节点成功的节点作为HMaster）产生新的 HMaster 来提供服务监控Region Server: 通过 ZooKeeper 来监控 Region Server 的状态，当Region Server 有异常的时候，通过回调的形式通知 HMaster 有关Region Server 上下线的信息维护元数据和集群配置信息</td></tr><tr><td>HDFS</td><td>为 HBase 提供底层数据存储服务，同时为 HBase提供高可用的支持</td></tr></tbody></table><h1 id="3-hbase数据模型与操作">3. HBase数据模型与操作</h1><h2 id="31-数据模型">3.1. 数据模型</h2><p>基本存储单位是表（Table），表由行（Row）和列族（Column Family）组成。每个列族可以包含多个列（Column），而列的数据通过时间戳（Timestamp）进行版本控制</p><table><thead><tr><th>Row Key</th><th>Column Family(Cloumn Family):Column(列限定符)</th><th>Value</th><th>Timestamp</th></tr></thead><tbody><tr><td>row1</td><td>cf1:col1</td><td>value1</td><td>1627871234000</td></tr><tr><td>row1</td><td>cf1:col2</td><td>value2</td><td>1627871235000</td></tr><tr><td>row2</td><td>cf1:col1</td><td>value3</td><td>1627871236000</td></tr></tbody></table><table><thead><tr><th>字段</th><th>含义</th></tr></thead><tbody><tr><td>RowKey</td><td>唯一标识一行记录，由于HBase都是按照rowKey进行region路由，因此针对rowKey的设计需要注意</td></tr><tr><td>Column Family</td><td>HBase中的每个列都由Cloumn Family（列簇）和Cloumn Qualifier（列限定符）进行限定，例如info：name，info：age。建表时，只需指明列簇，而列限定符无需预先定义</td></tr><tr><td><strong>Time Stamp</strong></td><td>用于标识数据的不同版本（version），每条数据写入时，如果不指定时间戳，系统会自动为其加上该字段，其值为写入HBase的时间</td></tr></tbody></table><p><strong>Cell</strong></p><pre><code>{ RowKey, ColumnFamily: ColumnQualifier, TimeStamp}</code></pre><p>唯一确定的单元。cell 中的数据是没有类型的，全部是字节码形式存贮</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1743415548312-957fbd53-a3fb-454a-a6f5-a8605041c18a.png" alt="img" /></p><h2 id="32-主要操作">3.2. 主要操作</h2><p>HBase 提供了丰富的 API 进行数据操作，包括 <code>Put</code>、<code>Get</code>、<code>Delete</code> 和 <code>Scan</code>。<code>Put</code> 用于写入数据，<code>Get</code> 用于读取数据，<code>Delete</code> 用于删除数据，<code>Scan</code> 用于批量读取数据。</p><ol><li>Put：将数据写入表中。</li><li>Get：根据行键读取数据。</li><li>Delete：删除指定行或列的数据。</li><li>Scan：遍历表中的数据。</li></ol><h3 id="321-写流程">3.2.1. 写流程</h3><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1743428857086-f60c1fb6-d614-4680-ae40-7e333be75a90.png" alt="img" /></p><ol><li>获取meta表信息：客户端先访问zookeeper，获取Meta表位于那个region server，根据根据请求的信息<code>（namespace:table/rowkey）</code>,在meta表中查询出目标数据位于哪个region server的哪个region中，然后缓存在客户端中</li><li>与目标数据的region server进行通讯</li><li>将数据写入到WAL中</li><li>将数据写入到对应的memstore中，</li><li>向客户端发送写入成功的信息</li><li>等达到memstore的刷写时机后，将数据刷写到HFILE中</li></ol><p>更新操作：并没有真正更新原有数据，而是使用时间戳属性实现了多版本；</p><p>删除操作：没有真正删除原有数据，只是插入了一条标记为&quot;deleted&quot;标签的数据，而真正的数据删除发生在系统异步执行Major Compact的时候</p><h3 id="322-读流程">3.2.2. 读流程</h3><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1743429536041-0d94bc80-4b44-480a-acb5-96f0cee43ff8.png" alt="img" /></p><ol><li>获取meta表信息：客户端先访问zookeeper，获取Meta表位于那个region server，根据根据请求的信息<code>（namespace:table/rowkey）</code>,在meta表中查询出目标数据位于哪个region server的哪个region中，然后缓存在客户端中</li><li>请求对应region server</li><li>分别在<strong>Block Cache（读缓存）</strong>，<strong>MemStore和 Store File查询目标数据</strong>，并<strong>将查到的数据</strong>进行**合并，**此处所有数据是指同一条数据的不同版本（time stamp）或者不同的类型（Put/Delete）</li><li>将从文件中查询到的数据块缓存到block cache</li><li>将<strong>合并后的数据</strong>返回给<strong>客户端</strong></li></ol><p><strong>API操作主要划分如下：</strong></p><ol><li><strong>GET</strong></li><li><strong>SCAN - 一般</strong>HBase会根据设置条件将一次大的scan操作拆分为多个RPC请求，每个RPC请求称为一次next请求，每次只返回规定数量的结果，类似于ES的search-after。每次可以设置setBatch(条数)、setMaxResultSize(数据量大小)</li></ol><h4 id="3221-scan查询详细流程">3.2.2.1. Scan查询详细流程</h4><p>Scan查询大致分为下述四个流程</p><ol><li>Client-Server读取交互逻辑</li><li>HBase的scan框架</li><li>HBase过滤淘汰不符合条件的HFile</li><li>从HFile中读取待查找Key</li></ol><h5 id="32211-client-server读取交互逻辑">3.2.2.1.1. Client-Server读取交互逻辑</h5><p>Client首先会从ZooKeeper中获取元数据hbase:meta表所在的RegionServer，然后根据待读写rowkey发送请求到元数据所在RegionServer，获取数据所在的目标RegionServer和Region（并将这部分元数据信息缓存到本地），最后将请求进行封装发送到目标RegionServer进行处理。</p><p>HBase Client端与Server端的scan操作并没有设计为一次RPC请求，这是因为一次大规模的scan操作很有可能就是一次全表扫描，扫描结果非常之大，通过一次RPC将大量扫描结果返回客户端会带来至少两个非常严重的后果：</p><p>•大量数据传输会导致集群网络带宽等系统资源短时间被大量占用，严重影响集群中其他业务。</p><p>•客户端很可能因为内存无法缓存这些数据而导致客户端OOM。</p><p>实际上HBase会根据设置条件将一次大的scan操作拆分为多个RPC请求，每个RPC请求称为一次next请求，每次只返回规定数量的结果。下面是一段scan的客户端示例代码：</p><h5 id="32212-hbase的scan框架">3.2.2.1.2. HBase的scan框架</h5><p>Scan中，会根据startRowKey、endRowKey查询多个Region server</p><p>RegionServer接收到客户端的get/scan请求之后做了两件事情：</p><ol><li>首先构建scanner iterator体系；</li><li>然后执行next函数获取KeyValue，并对其进行条件过滤</li></ol><h6 id="322121-scanner-iterator体系">3.2.2.1.2.1. scanner iterator体系</h6><p>Scanner的核心体系包括三层Scanner：RegionScanner，StoreScanner，MemStoreScanner和StoreFileScanner。三者是层级的关系：</p><ul><li>一个RegionScanner由多个StoreScanner构成。一张表由多少个列簇组成，就有多少个StoreScanner，每个StoreScanner负责对应Store的数据查找。</li><li>一个StoreScanner由MemStoreScanner和StoreFileScanner构成。每个Store的数据由内存中的MemStore和磁盘上的StoreFile文件组成。相对应的，StoreScanner会为当前该Store中每个HFile构造一个StoreFileScanner，用于实际执行对应文件的检索。同时，会为对应MemStore构造一个MemStoreScanner，用于执行该Store中MemStore的数据检索。</li></ul><p>需要注意的是，RegionScanner以及StoreScanner并不负责实际查找操作，它们更多地承担组织调度任务，<strong>负责KeyValue最终查找操作的是StoreFileScanner和MemStoreScanner</strong>。三层Scanner体系可以用图表示。</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1743434475168-75a98825-be7f-4048-b747-4ab9507ee21a.png" alt="img" /></p><h5 id="32213-hbase过滤淘汰不符合条件的hfile">3.2.2.1.3. HBase过滤淘汰不符合条件的HFile</h5><p>通过不同的scanner进行查找，过滤HFile-&gt;根据Key在HFile进行读取数据，最终产出一个keyValue（Cell）的优先队列（小顶堆），next的查询模式就是基于这个优先队列（Key ValueScanner）进行keyValue的查找，并最终从HFile中读取到数据</p><p>其中keyValue的优先队列的生成过程如下</p><ol><li><p>过滤淘汰部分不满足查询条件的StoreScanner，可以先基于用户查询中的TimeRange，Rowkey Range过滤以及布隆过滤器过滤部份HFile</p></li><li><ol><li>TimeRange（时序数据）：StoreFile中元数据有一个关于该File的TimeRange属性[ miniTimestamp, maxTimestamp ]，如果待检索的TimeRange与该文件时间范围没有交集，就可以过滤掉该StoreFile；另外，如果该文件所有数据已经过期，也可以过滤淘汰</li><li>Rowkey Range：因为StoreFile中所有KeyValue数据都是有序排列的，所以如果待检索row范围[ startrow，stoprow ]与文件起始key范围[ f irstkey，lastkey ]没有交集，比如stoprow &lt; firstkey或者startrow &gt; lastkey，就可以过滤掉该StoreFile</li><li>布隆过滤器：主要根据Bloom Block，待检索的rowkey获取对应的Bloom Block并加载到内存（通常情况下，热点Bloom Block会常驻内存的），再用hash函数对待检索rowkey进行hash，根据hash后的结果在布隆过滤器数据中进行寻址，即可确定待检索rowkey是否一定不存在于该HFile</li></ol></li><li><p>每个Scanner seek到startKey。这个步骤在每个HFile文件中（或MemStore）中seek扫描起始点startKey（下文：从HFile中读取待查找Key）。如果HFile中没有找到starkKey，则seek下一个KeyValue地址，这个过程会比较复杂。</p></li><li><p>KeyValueScanner合并构建最小堆。将该Store中的所有StoreFileScanner和MemStoreScanner合并形成一个heap（最小堆）</p></li><li><p>最后，执行最终读取，例如执行next函数获取KeyValue并对其进行条件过滤</p></li><li><ol><li>检查该KeyValue的KeyType是否是Deleted/DeletedColumn/DeleteFamily等，如果是，则直接忽略该列所有其他版本，跳到下列（列簇）。</li><li>检查该KeyValue的Timestamp是否在用户设定的Timestamp Range范围，如果不在该范围，忽略。</li><li>检查该KeyValue是否满足用户设置的各种filter过滤器，如果不满足，忽略。</li><li>检查该KeyValue是否满足用户查询中设定的版本数，比如用户只查询最新版本，则忽略该列的其他版本；反之，如果用户查询所有版本，则还需要查询该cell的其他版本。</li></ol></li></ol><p>注意，其中一个KeyValue其实就是一个cell = {rowKey,colounm family,timeStamp,value}</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1743434492561-a009a141-7387-4568-be03-dbcd44201a97.png" alt="img" /></p><h6 id="322131-各种filter过滤器">3.2.2.1.3.1. 各种Filter过滤器</h6><p>在HBase里，<code>Scan</code> 的 <code>Filter</code> 支持多种过滤操作，以下为你详细介绍：</p><p>\1. 比较过滤器</p><ul><li><strong>SingleColumnValueFilter</strong>：此过滤器用于筛选特定列族和列限定符的值，它依据比较运算符和比较器来判定是否保留该行。示例代码如下：</li></ul><pre><code class="language-java">SingleColumnValueFilter filter = new SingleColumnValueFilter(    Bytes.toBytes(&quot;cf&quot;),    Bytes.toBytes(&quot;column&quot;),    CompareOperator.EQUAL,    new BinaryComparator(Bytes.toBytes(&quot;value&quot;)));scan.setFilter(filter);</code></pre><ul><li><strong>RowFilter</strong>：按照行键对数据进行过滤，同样借助比较运算符和比较器来实现。示例如下：</li></ul><pre><code class="language-java">RowFilter rowFilter = new RowFilter(    CompareOperator.EQUAL,    new BinaryComparator(Bytes.toBytes(&quot;rowkey&quot;)));scan.setFilter(rowFilter);</code></pre><ul><li><strong>FamilyFilter</strong>：用于筛选特定列族的数据，通过比较运算符和比较器达成。示例如下：</li></ul><pre><code class="language-java">FamilyFilter familyFilter = new FamilyFilter(    CompareOperator.EQUAL,    new BinaryComparator(Bytes.toBytes(&quot;cf&quot;)));scan.setFilter(familyFilter);</code></pre><ul><li><strong>QualifierFilter</strong>：该过滤器可筛选特定列限定符的数据，利用比较运算符和比较器完成。示例如下：</li></ul><pre><code class="language-java">QualifierFilter qualifierFilter = new QualifierFilter(    CompareOperator.EQUAL,    new BinaryComparator(Bytes.toBytes(&quot;column&quot;)));scan.setFilter(qualifierFilter);</code></pre><p>\2. 组合过滤器</p><ul><li><strong>FilterList</strong>：能够把多个过滤器组合起来使用，支持 <code>MUST_PASS_ALL</code>（所有过滤器都必须通过）和 <code>MUST_PASS_ONE</code>（只要有一个过滤器通过即可）两种组合方式。示例如下：</li></ul><pre><code class="language-java">FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);filterList.addFilter(filter1);filterList.addFilter(filter2);scan.setFilter(filterList);</code></pre><p>\3. 专用过滤器</p><ul><li><strong>PageFilter</strong>：用于实现分页功能，可指定每页返回的行数。示例如下：</li></ul><pre><code class="language-java">PageFilter pageFilter = new PageFilter(10); // 每页返回 10 行scan.setFilter(pageFilter);</code></pre><ul><li><strong>PrefixFilter</strong>：按照行键的前缀进行过滤，只返回行键以指定前缀开头的行。示例如下：</li></ul><pre><code class="language-java">PrefixFilter prefixFilter = new PrefixFilter(Bytes.toBytes(&quot;prefix&quot;));scan.setFilter(prefixFilter);</code></pre><ul><li><strong>ColumnPrefixFilter</strong>：根据列限定符的前缀来筛选数据，只返回列限定符以指定前缀开头的列。示例如下：</li></ul><pre><code class="language-java">ColumnPrefixFilter columnPrefixFilter = new ColumnPrefixFilter(Bytes.toBytes(&quot;prefix&quot;));scan.setFilter(columnPrefixFilter);</code></pre><ul><li><strong>FirstKeyOnlyFilter</strong>：仅返回每行的第一个键值对，常用于快速统计行数。示例如下：</li></ul><pre><code class="language-java">FirstKeyOnlyFilter firstKeyOnlyFilter = new FirstKeyOnlyFilter();scan.setFilter(firstKeyOnlyFilter);</code></pre><p>这些过滤器能让你在HBase中精准地筛选数据，以满足不同的业务需求。</p><h6 id="322132-其他api">3.2.2.1.3.2. 其他API</h6><p><strong>时间处理</strong>HBase的<code>Scan</code>操作中是有与时间相关的过滤器的，下面为你详细介绍：</p><p>\1. <code>TimestampsFilter</code></p><p><strong>该过滤器允许你指定一系列时间戳，只有当单元格的时间戳与指定的时间戳相匹配时，才会返回该单元格。以下是Java代码示例：</strong></p><pre><code class="language-java">import org.apache.hadoop.hbase.TableName;import org.apache.hadoop.hbase.client.*;import org.apache.hadoop.hbase.filter.TimestampsFilter;import org.apache.hadoop.hbase.util.Bytes;import java.io.IOException;import java.util.ArrayList;import java.util.List;public class TimeStampFilterExample {    public static void main(String[] args) throws IOException {        // 创建连接        Connection connection = ConnectionFactory.createConnection();        // 获取表        Table table = connection.getTable(TableName.valueOf(&quot;your_table_name&quot;));        // 创建Scan对象        Scan scan = new Scan();        // 指定时间戳列表        List&lt;Long&gt; timestamps = new ArrayList&lt;&gt;();        timestamps.add(1609459200000L);         timestamps.add(1609545600000L);         // 创建TimestampsFilter        TimestampsFilter filter = new TimestampsFilter(timestamps);        scan.setFilter(filter);        // 执行扫描        ResultScanner scanner = table.getScanner(scan);        for (Result result : scanner) {            // 处理结果            System.out.println(result);        }        // 关闭资源        scanner.close();        table.close();        connection.close();    }}</code></pre><p><strong>在这个示例中，我们创建了一个</strong><code>**TimestampsFilter**</code><strong>，并指定了两个时间戳。只有时间戳与这两个值相匹配的单元格才会被返回。</strong></p><p>\2. 在<code>Scan</code>对象中直接设置时间范围</p><p><strong>除了使用过滤器，你还可以在</strong><code>**Scan**</code><strong>对象中直接设置时间范围，只有时间戳在该范围内的单元格才会被返回。示例代码如下：</strong></p><pre><code class="language-java">import org.apache.hadoop.hbase.TableName;import org.apache.hadoop.hbase.client.*;import org.apache.hadoop.hbase.util.Bytes;import java.io.IOException;public class TimeRangeScanExample {    public static void main(String[] args) throws IOException {        // 创建连接        Connection connection = ConnectionFactory.createConnection();        // 获取表        Table table = connection.getTable(TableName.valueOf(&quot;your_table_name&quot;));        // 创建Scan对象        Scan scan = new Scan();        // 设置时间范围        long startTime = 1609459200000L;         long endTime = 1609545600000L;         scan.setTimeRange(startTime, endTime);        // 执行扫描        ResultScanner scanner = table.getScanner(scan);        for (Result result : scanner) {            // 处理结果            System.out.println(result);        }        // 关闭资源        scanner.close();        table.close();        connection.close();    }}</code></pre><p><strong>在上述代码里，我们使用</strong><code>**setTimeRange**</code><strong>方法设置了一个时间范围，只有时间戳在这个范围内的单元格才会被返回。</strong></p><h5 id="32214-从hfile中读取待查找key">3.2.2.1.4. <strong>从HFile中读取待查找Key</strong></h5><p>最后根据KevValue去HFIle中查找具体数据，查询流程如下：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1743435354513-57c1730b-e654-4471-b87e-d4abb081da64.png" alt="img" /></p><h3 id="323-memstore刷写">3.2.3. MemStore刷写</h3><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1743429765211-e84108bd-6328-4908-ba03-4921a1469615.png" alt="img" /></p><p>MemStore触发刷写的场景如下：</p><ol><li>单一MemStore超大小128M：某个MemStore的大小达到了hbase.hregion.memstore.flush.size（默认值 128M），其所在 region 的所有 memstore (对应的列簇)都会刷写，当达到128 * N 还没有刷写，此时会拒绝写入。</li></ol><p>两个相关参数的默认值如下：</p><p>hbase.hregion.memstore.flush.size=128M(默认)</p><p>hbase.hregion.memstore.block.multiplier=4(默认)</p><ol><li>memstore总大小超过堆内存：当 region server 中 memstore 的总大小达到java_heapsize(应用的堆内存)*hbase.regionserver.global.memstore.size时</li></ol><p>hbase.regionserver.global.memstore.size=0.4（默认值）</p><ol><li>定时刷写：到达自动刷写的时间，也会触发 memstore flush，默认时1h</li><li>WAL文件超大：WAL 文件的数量超过 hbase.regionserver.maxlogs，region 会按照时间顺序依次进行刷写</li></ol><h3 id="324-数据合并storefile-compaction">3.2.4. 数据合并（StoreFile Compaction）</h3><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1743430178982-8ca168ef-ce16-4a78-bdd4-587e74ba03b3.png" alt="img" /></p><p>为什么需要执行数据合并？</p><p>由于MemStore每次刷写都会生成一个新的HFile，同一个字段的不同版本(timestamp)和不同类型(Put/Delete)有可能分布在不同的HFile中，因此查询时需要遍历所有的HFile。为了减少HFile的个数，以及清除掉过期和删除的数据，会进行StoreFile Compaction</p><p>Compaction分为两种</p><ol><li>Minor Compaction：会将临时的若干较小的HFile合并成一个较大的HFile，但不会清理过期和删除的数据</li><li>Major Compaction：会将一个Store下的所有HFile合并为一个大HFile，并且会清理掉过期和删除的数据</li></ol><h3 id="325-数据拆分">3.2.5. 数据拆分</h3><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1743430356371-5012b2b7-f04b-4493-b3c9-7e2f2b828a1f.png" alt="img" /></p><p>默认情况下，每个 Table 起初只有一个 Region，随着数据的不断写入，Region 会自动进行拆分。刚拆分时，两个子 Region 都位于当前的 Region Server，但处于负载均衡的考虑，HMaster 有可能会将某个 Region 转移给其他的 Region Server</p><p>拆分时机：</p><ol><li>Region中总Store大小超限制：hbase.hregion.max.filesize，该 Region 就会进行拆分（0.94 版本之前）。</li><li>Region中某个Store下所有StoreFile大小超限</li></ol><h1 id="4-hbase的扩展性和高可用">4. HBase的扩展性和高可用</h1><p>HBase 的架构设计使其具备良好的扩展性和高可用性。</p><table><thead><tr><th>特性</th><th>描述</th></tr></thead><tbody><tr><td>扩展性</td><td>HBase 可以通过增加 RegionServer 节点来实现水平扩展。当数据量增长时，HMaster 可以将 Region 划分为更小的 Region，并将其分配到新的 RegionServer 上。</td></tr><tr><td>高可用性</td><td>通过 Zookeeper 监控集群中的各个节点，HBase 实现了自动故障恢复机制。当一个 RegionServer 发生故障时，HMaster 会将其管理的 Region 重新分配给其他健康的 RegionServer。</td></tr></tbody></table><p>相关文档：</p><p><a href="https://cloud.tencent.com/developer/article/2184702">https://cloud.tencent.com/developer/article/2184702</a></p><p><a href="https://cloud.tencent.com/developer/article/2448014">https://cloud.tencent.com/developer/article/2448014</a></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[ES架构图]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/e-s-jia-gou-tu" />
                <id>tag:https://0522-isniceday.top,2025-03-01:e-s-jia-gou-tu</id>
                <published>2025-03-01T16:36:38+08:00</published>
                <updated>2025-03-01T16:36:38+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke//image_1740818175617.png" alt="image.png" /></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[OpenAPI面临的签名、加密、证书等问题]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/o-p-e-n-a-p-i-mian-lin-de-qian-ming--jia-mi--zheng-shu-deng-wen-ti" />
                <id>tag:https://0522-isniceday.top,2025-01-27:o-p-e-n-a-p-i-mian-lin-de-qian-ming--jia-mi--zheng-shu-deng-wen-ti</id>
                <published>2025-01-27T11:22:02+08:00</published>
                <updated>2025-02-18T17:18:00+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="1openapi可能面临的问题">1.OpenAPI可能面临的问题</h1><ul><li>信息保密问题：网络报文中的信息可能会泄露，例如身份证等信息 - 加密</li><li>信息篡改问题：网络报文被篡改，例如转账接受人 - 签名</li><li>通信对象认证问题：和你通信的人可能是被劫持的对象 - 签名</li></ul><h1 id="2加密对称加密--非对称加密">2.加密：对称加密 / 非对称加密</h1><h2 id="20什么是加密">2.0.什么是加密</h2><p>注意加密也一定需要能解密出来。</p><p>组成：一般密码系统通常由 明文 + 密钥 + 算法组成，明文 + 密钥是原材料，算法是配方，结果就是产出密文</p><p>密码算法要考虑的关键因素是让破解者难以通过明文、密文、以及其他信息推断出密钥或者缩小密钥的范围，从而暴力破解出密钥</p><h2 id="21对称加密">2.1.对称加密</h2><p><strong>对称密码</strong>指加密和解密使用同样的密钥，就是<strong>加密方和解密方都要持有同样的密钥</strong>，性能高</p><p>由于对称加密需要双方持有同一密钥，因此就会引发出如何确定同一密钥，也就是<strong>密钥配送问题</strong>，如何协商出一致的秘钥</p><p>解决方案：</p><ul><li>事先共享密钥（邮件等安全通道）</li><li><a href="https://zhida.zhihu.com/search?content_id=185119483&amp;content_type=Article&amp;match_order=1&amp;q=密钥分配中心&amp;zhida_source=entity">密钥分配中心</a></li><li>Diffie-Hellman<a href="https://zhida.zhihu.com/search?content_id=185119483&amp;content_type=Article&amp;match_order=1&amp;q=密钥交换&amp;zhida_source=entity">密钥交换</a></li><li><a href="https://zhida.zhihu.com/search?content_id=185119483&amp;content_type=Article&amp;match_order=1&amp;q=非对称加密&amp;zhida_source=entity">非对称加密</a></li></ul><h2 id="22非对称加密">2.2.非对称加密</h2><p><strong>非对称密码</strong>指加密和解密使用不同密钥。</p><p>非对称加密需要4个密钥。通信双方各自准备一对公钥和私钥。其中公钥是公开的，由信息接受方提供给信息发送方。公钥用来对信息加密。私钥由信息接受方保留，用来解密。既然公钥是公开的，就不存在保密问题。也就是说非对称加密完全不存在密钥配送问题！你看，是不是完美解决了密钥配送问题？</p><p>但是，非对称加密过程容易产生<strong>中间人问题</strong>，中间人攻击指的是在通信双方的通道上，混入攻击者。他对接收方伪装成发送者，对放送放伪装成接收者。</p><p>他监听到双方发送公钥时，偷偷将消息篡改，发送自己的公钥给双方。然后自己则保存下来双方的公钥。</p><p>如此操作后，双方加密使用的都是攻击者的公钥，那么后面所有的通信，攻击者都可以在拦截后进行解密，并且篡改信息内容再用接收方公钥加密。而接收方拿到的将会是篡改后的信息。实际上，发送和接收方都是在和中间人通信</p><p>和对称加密相比较，非对称加密有如下特点：</p><p>1、非对称加密解决了密码配送问题</p><p>2、非对称加密的处理速度只有对称加密的几百分之一。不适合对很长的消息做加密。</p><h3 id="221如何解决公钥的传输解决中间人问题---证书解决">2.2.1.如何解决公钥的传输解决中间人问题 - 证书解决</h3><p>那么实际在通信双边的公钥传输过程中，如何明确拿到的公钥就是正确的呢，因此我们是不是就需要验证对端的身份合法？因此引出了证书</p><h2 id="23证书">2.3.证书</h2><p>证书是能够证明你身份的文件，类似于身份证，证书包括如下几个部分：</p><ul><li>身份证姓名：对应证书的公钥所有者</li><li>身份证号：通信方公钥</li><li>身份证签发机关：证书颁发者</li><li>身份证有效期限：证书有效期</li><li>身份证防伪条纹：认证机构对公钥的签名</li></ul><p>工作机制</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/v2-6beb29e9d47f7e3898797ab0a0eaab10_r.jpg" alt="img" /></p><p>大致流程可以概述为：</p><ol><li>对端在认证机构（CA）注册自己的公钥</li><li>CA使用自己的机构私钥对证书进行签名，生成证书</li><li>接收端使用CA的公钥对证书进行验签</li></ol><p>其中又会涉及到机构的合法性，此时又会引入证书链，也就是有更高级的证书机构对证书机构进行背书，最终会到根CA</p><h1 id="3签名">3.签名</h1><p>我们通过加密来保障了内容的隐私性，那假设我们发出去的报文被劫持方劫持并篡改，再使用公钥进行发送，此时会发生篡改的问题。接收人是无法识别的。这里就需要使用电子签名来保证信息的完整性（来源方正确+信息没有被篡改，例如我能知道我收到的这个信息是肯德基、麦当劳商户发起的）</p><p>如何确保来源方正确：对端用自己的私钥加签，服务端用对端的公钥进行签名的验证，如果能通过则验证对端是来源合法。（当然此时也会出现中间人问题，一样用证书解决）</p><p>如何确保报文没有被篡改：当然我们可以直接对原报文进行加密生成签名，但是由于考虑到性能，我们基于报文生成一个较短的单向散列函数值，并基于单向散列值进行签名的生成，由于散列函数存在同样的报文生成的散列函数唯一的特性，因此其实散列函数就能代表一段报文。</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/v2-a63303809e39f43250f07296c6561fc1_r.jpg" alt="img" /></p><p>散列函数：</p><ol><li>计算出的散列值是固定的，并且短。</li><li>不同的消息，散列值不同。</li><li>计算的速度要快。信息很长的时候，也要保证快速计算。</li></ol>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Redis学习（二十）：代码实战之缓存异常场景]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/r-e-d-i-s-xue-xi--er-shi---dai-ma-shi-zhan-zhi-huan-cun-yi-chang-chang-jing" />
                <id>tag:https://0522-isniceday.top,2024-12-20:r-e-d-i-s-xue-xi--er-shi---dai-ma-shi-zhan-zhi-huan-cun-yi-chang-chang-jing</id>
                <published>2024-12-20T10:47:01+08:00</published>
                <updated>2024-12-20T10:47:04+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<ul><li><a href="#1缓存和数据库中数据不一致">1.缓存和数据库中数据不一致</a><ul><li><a href="#11发生场景">1.1.发生场景</a></li><li><a href="#12如何解决数据不一致">1.2.如何解决数据不一致</a></li><li><a href="#13先更新库还是先更新缓存的取舍">1.3.先更新库还是先更新缓存的取舍</a></li></ul></li><li><a href="#2缓存雪崩">2.缓存雪崩</a><ul><li><a href="#21概念">2.1.概念：</a></li><li><a href="#22发生场景及解决措施">2.2.发生场景及解决措施</a></li></ul></li><li><a href="#3缓存击穿">3.缓存击穿</a><ul><li><a href="#31概念">3.1.概念</a></li><li><a href="#32解决措施">3.2.解决措施：</a></li></ul></li><li><a href="#4缓存穿透">4.缓存穿透</a><ul><li><a href="#41概念">4.1.概念</a></li><li><a href="#42解决措施">4.2.解决措施</a></li></ul></li></ul><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/缓存异常场景.png" alt="缓存异常场景" /></p><p>应用redis缓存时，会发生如下问题：</p><ul><li>缓存和数据库中数据不一致</li><li>缓存雪崩</li><li>缓存击穿</li><li>缓存穿透</li></ul><h1 id="1缓存和数据库中数据不一致">1.缓存和数据库中数据不一致</h1><p>数据一致性包含两种情况：</p><ul><li>第一种：缓存中有数据，且数据和数据库中一致</li><li>第二种：缓存中没有数据，且数据库的数据是最新的</li></ul><h2 id="11发生场景">1.1.发生场景</h2><p>根据缓存的三种类型，来详细讲解下不一致可能出现的情形</p><p>（1）读缓存</p><ul><li><p>新增操作：新增数据，数据会直接写入到数据库，不涉及缓存操作，因此不会出现不一致的情形，且数据符合一致性的第一种情形</p></li><li><p>删改操作：既要更新数据库，也需要删除缓存中的数据。两个操作如果无法保住原子性，则会出现数据不一致的问题</p><p>如下图：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/2c376b536aff9d14d8606499f401cdac.jpg" alt="img" /></p></li><li><p>并发场景下的数据不一致</p><ul><li><p>情况一：先删除缓存，再更新数据库</p><p>写+读：线程A删除缓存后，由于网络延迟还没有更新到库，此时线程B读取数据发现缺失，从库读出再同步入缓存，此时A才写入库，造成了缓存存在数据，但是数据不一致的情形</p><p><img src="https://static001.geekbang.org/resource/image/85/12/857c2b5449d9a04de6fe93yy1e355c12.jpg" alt="img" /></p></li><li><p>情况二：先更新数据库值，再删除缓存</p><p>写+读:线程A删完库和删除缓存之间，存在其他线程发出读请求，这个时候的读请求由于缓存还没删除，所以读到了值并返回，但是这个场景对业务影响较小</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/a1c66ee114yyc9f37f2a35f21b46010b.jpg" alt="img" /></p></li></ul><p>（2）读写缓存-直写场景</p><p>针对这种缓存类型的数据不一致的问题，大致与读场景相同，其实二者的区别也就是写请求的处理，读写缓存针对写请求是同步缓存，而读缓存是删除缓存，因此读写缓存场景下，新增的不一致的类型包括：新增请求，并发下的写写（执行顺序导致）请求</p><ul><li><p>新增操作：新增数据库和新增缓存需要保证原子性，因此可以用<strong>重试解决</strong></p></li><li><p>并发场景下的数据不一致：在读缓存的两个情形上多了个写写类型</p><ul><li><p>情况一：先更新缓存，再更新数据库</p><p>写+读：不会影响业务，更新了缓存之后，其余请求会直接读缓存，对业务无影响</p><p>写+写：线程A与线程B同时写数据D，缓存中的写顺序是AB、数据库写顺序是BA，导致不一致</p></li><li><p>情况二：先更新数据库值，再删除缓存</p><p>写+读：有部分请求会读到缓存的老数据，但是对业务系统短暂影响</p><p>写+写：与情况一的写+写情形一致</p></li></ul></li></ul></li></ul><p>写+读场景下，如果缓存不存在怎么办？</p><p>可能的问题：假设写请求W，读请求R</p><table><thead><tr><th>时间</th><th>W</th><th>R</th></tr></thead><tbody><tr><td>T1</td><td> </td><td>读取缓存无数据，读取DB</td></tr><tr><td>T2</td><td>更新DB</td><td> </td></tr><tr><td>T3</td><td>删除缓存/更新缓存</td><td> </td></tr><tr><td>T4</td><td> </td><td>写入读取的数据入缓存</td></tr></tbody></table><p>虽然这种场景会出现问题，不过由于读取缓存+写入读取的数据入缓存的耗时比更新DB的耗时小的多，因此发生的概率极低，不过不排除R线程由于GC导致STW中断</p><h2 id="12如何解决数据不一致">1.2.如何解决数据不一致</h2><p><strong>（1）重试机制</strong></p><p>通过引入消息队列或者代码中手写重试代码，如果重试之后仍然失败则抛错</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/74a66b9ce185d7c5b53986fc522dfcab.jpg" alt="img" /></p><p><strong>（2）针对并发场景下的读缓存的</strong></p><p>先删除缓存，再更新数据库：</p><ul><li>（写+读）延迟双删：线程A再更新完数据库之后，再sleep一会再删除缓存一次，具体sleep时间更新线程读数据+写缓存的时间（其实就是上述线程B的耗时）</li></ul><p>先更新数据库，再更新缓存：</p><ul><li>（写+读）对业务系统短暂影响，无需特殊处理</li></ul><p><strong>（3）针对并发场景下的读写缓存的</strong></p><p>先更新缓存，再更新数据库：</p><ul><li>（读+写）由于对业务的影响都不大，因此无需特殊处理</li><li>（写+写）写请求进来时，针对同一个资源的修改操作，先加分布式锁，这样同一时间只允许一个线程去更新数据库和缓存，没有拿到锁的线程把操作放入到队列中，延时处理</li></ul><p>先更新数据库，再更新缓存</p><ul><li>（读+写）由于对业务的影响都不大，因此无需特殊处理</li><li>（写+写）同上写+写</li></ul><p>写+写其实就是更新覆盖的场景，上述解决措施采取的悲观锁，这里其实也可以采取乐观锁的方式</p><h2 id="13先更新库还是先更新缓存的取舍">1.3.先更新库还是先更新缓存的取舍</h2><p>先总结下表：</p><p><img src="https://static001.geekbang.org/resource/image/11/6f/11ae5e620c63de76448bc658fe6a496f.jpg" alt="img" /></p><p>建议优先更新库再删除缓存，好处如下：</p><ul><li>先删除缓存再更新库，可能会导致数据缺失给数据库带来压力</li><li>先删除缓存再删库解决办法的延迟双删的sleep时间不好把握</li></ul><h1 id="2缓存雪崩">2.缓存雪崩</h1><h2 id="21概念">2.1.概念：</h2><p>大量的请求无法在缓存中进行处理，进而请求向数据库，导致数据库压力激增</p><h2 id="22发生场景及解决措施">2.2.发生场景及解决措施</h2><ul><li><p>第一种场景：缓存中大量数据同时失效，导致大量请求无法处理直接请求数据库</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/74bb1aa4b2213e3ff29e2ee701e8f72e.jpg" alt="img" /></p><p>解决措施：</p><p>（1）过期时间设置一个浮动值，当然这是得在业务场景运行的情况下</p><p>（2）服务降级：服务降级是指针对不同类型的数据采取不同的处理方式，针对请求？。</p><ul><li><p>当业务应用访问的是非核心数据（例如电商商品属性）时，暂时停止从缓存中查询这些数据，而是直接返回预定义信息、空值或是错误信息；</p></li><li><p>当业务应用访问的是核心数据（例如电商商品库存）时，仍然允许查询缓存，如果缓存缺失，也可以继续通过数据库读取。</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/4ab3be5ba24cf172879e6b2cff649ca8.jpg" alt="img" /></p></li><li><p>第二种场景：redis实例发生宕机，无法处理请求，会导致大量请求打向数据库</p><p>解决措施：</p><p>（1）业务系统中实现服务熔断：就是发生缓存雪崩的时候，为了方式业务系统崩溃，业务系统调用缓存接口时，缓存客户端发现redis实例宕机了，则拒绝所有的请求并返回直至恢复。虽然这样可以保证数据库的正常运行，但是这样对业务影响比较大。</p><p>（2）限流：针对熔断的改进就是限流了，允许少量请求进入，减轻数据库的压力</p><p>（3）提前预防：构建redis的高可用集群，方式不可用的风险</p></li></ul></li></ul><h1 id="3缓存击穿">3.缓存击穿</h1><h2 id="31概念">3.1.概念</h2><p>热点数据失效后，针对该热点数据的大量请求打向数据库，与缓存雪崩的区别在于雪崩是大量请求失效，而击穿是热点数据缓存失效。</p><h2 id="32解决措施">3.2.解决措施：</h2><p>1.设置key永不过期</p><p>2.数据缺失从数据库同步入缓存的过程上分布式（非阻塞）锁，保证同步过程中只有一个请求去同步，其余请求直接拒绝</p><h1 id="4缓存穿透">4.缓存穿透</h1><h2 id="41概念">4.1.概念</h2><p>要访问的数据既不在缓存中也不在数据库中，此时大量请求会同时请求缓存及数据库，这个时候会同时给缓存和数据库带来压力</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/46c49dd155665579c5204a66da8ffc2e.jpg" alt="img" /></p><h2 id="42解决措施">4.2.解决措施</h2><p>（1）缓存空值或缺省值：针对数据缺失的数据存储空值或缺省值入redis</p><p>（2）前端预防，针对请求进行校验</p><p>（3）使用布隆过滤器快速判断数据是否存在，避免从数据库中查询数据是否存在，减轻数据库压力</p><p><strong>布隆过滤器</strong>：</p><p>布隆过滤器由一个初始值都为0的bit数组和N个哈希函数组成，用于快速判断数据是否存在。当我们想快速判断数据是否存在是，可以通过判断布隆过滤器的标记进行判断，标记流程如下：</p><ul><li>数据针对N个哈希函数进行计算得到N个哈希值</li><li>再将N个哈希值对bit数据的长度取模。将取模后的值对应的下标置为1</li></ul><p>如果数据存在，判断时再走一遍标记的流程，查看bit数组中这N个位置上的bit值。只要这N个bit值有一个不为1，这就表明布隆过滤器没有对该数据做过标记，则代表数据不存在，无需查找后端缓存和数据库</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/98f7d32499e4386b40aebc3622aa7268.jpg" alt="img" /></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[流量投放：推荐召回策略]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/liu-liang-tou-fang--tui-jian-zhao-hui-ce-lve" />
                <id>tag:https://0522-isniceday.top,2023-03-27:liu-liang-tou-fang--tui-jian-zhao-hui-ce-lve</id>
                <published>2023-03-27T10:20:01+08:00</published>
                <updated>2023-03-27T10:20:01+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="流量投放推荐召回策略">流量投放：推荐召回策略</h1><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1678417356792-da8afe35-071a-4e68-8768-689e57c4347a.jpeg" alt="img" /></p><h1 id="协同策略">协同策略</h1><h3 id="概念">概念</h3><ul><li>u-user：用户</li><li>i-item：内容</li><li>g-tag：标签</li></ul><p><strong>各种概念：</strong></p><ul><li>2i：计算item-item相似度，用于相似推荐、相关推荐、关联推荐；</li><li>u2i：基于矩阵分解、协同过滤的结果，直接给u推荐i；</li><li>u2u2i：基于用户的协同过滤，先找相似用户，再推荐相似用户喜欢的item；</li><li>u2i2i：基于物品的协同过滤，先统计用户喜爱的物品，再推荐他喜欢的物品；</li><li>u2tag2i：基于标签的泛化推荐，先统计用户偏好的tag向量，然后匹配所有的Item，这个tag一般是item的标签、分类、关键词等tag；</li></ul><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1678354056981-0b868525-6a4f-44b3-90e4-f3b820f91892.png" alt="img" /></p><h2 id="比较">比较</h2><h3 id="基于用户特征协同过滤2u">基于用户（特征）协同过滤2u</h3><p><strong>基本思想</strong>：当召回用户A的候选集时，可以先找到和他有相似兴趣的其他用户，然后把那些用户喜欢的、而用户A没有未交互的物品作为候选集</p><p>**其中相似度的计算公式可以概括为：**用户u和用户v，令N(u)表示用户u曾经有过正反馈（例如点击）的物品集合，令N(v) 为用户v曾经有过正反馈的物品集合，相似度=uv用户相似的物品集合/uv所有的物品集合和</p><p>例如香港首页feed为你推荐场景：正反馈可能为 -&gt;<img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/1678368077321-919b7353-97cb-4119-97c0-a043fc49a3ad.png" alt="img" /></p><p>除了针对候选集进行0 1有无的推荐外，更应该进一步优化的是针对每个候选集的物品进行score（价值）计算，换句话说，两个用户对冷门物品采取过同样的行为更能说明他们兴趣的相似度，因此，我们可以基于物品的流行度对热门物品进行一定的惩罚，价值可以为自定义，例如售卖券的价值=10，小程序的价值=20</p><p><strong>基于余弦相似度</strong></p><p><strong>大概步骤：</strong></p><ol><li>先统计当前用户相似度的用户后</li><li>算法会给用户推荐和他兴趣最相似的<strong>K个用户</strong>喜欢的</li></ol><h3 id="基于物品协同过滤2i">基于物品协同过滤2i</h3><p>**基本思想：**给用户推荐那些和他们之前喜欢的物品相似的物品。比如，该算法会因为你购买过《数据挖掘导论》而给你推荐《机器学习》。不过，ItemCF算法并不利用物品的内容属性计算物品之间的相似度，它主要通过分析用户的行为记录计算物品之间的相似度。该算法认为，物品A和物品B具有很大的相似度是因为喜欢物品A的用户大都也喜欢物品 B。一切都是基于用户行为去推荐item</p><p>**其中相似度的计算公式可以概括为：**喜欢item1并且喜欢item2的人数占了喜欢item1的百分比</p><p>相似度 =（ N（Item1）交N（item2））/N（Item1）</p><p>这里我们按照喜欢吃这个行为计算相似度举例，例如喜欢西瓜=100w，喜欢吃菠萝并喜欢吃西瓜的人数=50W，那么喜欢吃西瓜和喜欢吃菠萝的相似度=50/100=0.5，值越接近1相似度越高</p><p><strong>大概步骤：</strong></p><ol><li>计算物品之间的相似度</li><li>根据物品的相似度和用户的历史行为给用户生成召回候选集</li></ol><p>该算法也会有一个问题，就是针对热门item，假设按照喜欢这个行为作为相似度的考虑，假设有一个热门商品大多数用户都喜欢，这样会导致这个热门商品和大部分商品相似度都很高，因此这里会导致热门商品会经常被推荐。</p><p>那么我们是不是可以不让每个用户的行为的贡献度都一样呢？例如：</p><p>假设有这么一个用户，他是开书店的，并且买了当当网上80%的书准备用来自己卖。那么，  他的购物车里包含当当网80%的书。假设当当网有100万本书，也就是说他买了80万本。从前面  对ItemCF的讨论可以看到，这意味着因为存在这么一个用户，有80万本书两两之间就产生了相似度。这个用户虽然活跃，但是买这些书并非都是出于自身的兴趣，而且这些书覆   盖了当当网图书的很多领域，所以这个用户对于他所购买书的两两相似度的贡献应该远远小于一个只买了十几本自己喜欢的书的文学青年。因此，我们要对这样的用户进行一定的惩罚，John  S. Breese在论文1中提出了一个称为IUF(Inverse User Frequence)，即用户活跃度对数的  倒数的参数，他也认为活跃用户对物品相似度的贡献应该小于不活跃的用户</p><h3 id="结论">结论</h3><table><thead><tr><th> </th><th>UserCF</th><th>ItemCF</th></tr></thead><tbody><tr><td>推荐原理</td><td>给用户推荐那些和他有共同兴  趣爱好的用户喜欢的物品，UserCF的推荐结果着重于反映和用户兴趣相似的小群体的热点，UserCF的推荐更社会化，反映了用户所在的小型兴趣群体中物品的热门程度</td><td>用户推荐那些和他之前喜欢的物品类似的物品，ItemCF  的推荐结果着重于维系用户的历史兴趣，而ItemCF的推荐更加个性化，反映了用户自己的兴趣传承</td></tr><tr><td>业务场景</td><td>例如新闻网站，擅长抓热点</td><td>例如图书，电商网站，用户兴趣比较持久固定，对热门程度没有那么敏感，例如买书，更多的是看相似性，不一定注重热点</td></tr><tr><td>时效性（新加入item）</td><td>时效性要求较高，个性化不太明显的场景</td><td>长尾物品丰富，用户个性化需求强烈</td></tr><tr><td>上新物品</td><td>无需更新离线数据，只要用户看到，就可能推荐给用户</td><td>依赖离线更新item之间的相似度关系</td></tr><tr><td>存储</td><td>如果用户很多，那么维护用户兴趣相似度矩阵需要很大的空间</td><td>同理，如果物品很多，那么维护物品相似度矩阵代价较大</td></tr></tbody></table><p><img src="https://intranetproxy.alipay.com/skylark/lark/0/2023/png/35856706/1678365662792-1fbaec34-5c33-4516-90fc-1d75da61fdfa.png" alt="img" /></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Netty（三）：源码解析之keepalive和三种Idle检测的支持]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/n-e-t-t-y--san---yuan-ma-jie-xi-zhi-k-e-e-p-a-l-i-v-e-he-san-zhong-i-d-l-e-jian-ce-de-zhi-chi" />
                <id>tag:https://0522-isniceday.top,2022-11-18:n-e-t-t-y--san---yuan-ma-jie-xi-zhi-k-e-e-p-a-l-i-v-e-he-san-zhong-i-d-l-e-jian-ce-de-zhi-chi</id>
                <published>2022-11-18T23:25:24+08:00</published>
                <updated>2022-11-18T23:25:24+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="1什么是keepalive和为什么需要keepalive">1.什么是keepalive和为什么需要keepalive</h1><p><strong>为什么需要keepalive？</strong></p><p>如果建立TCP连接之后，如果由于某些异常原因，导致连接已经损坏。但是此时双端仍然在维持该连接，此时则会浪费资源，已经在使用时产生报错</p><p><strong>连接损坏如何定义？</strong></p><ol><li>对端异常“崩溃”</li><li>对端在，但是处理不过来</li><li>对端在，但是网络请求不可达</li></ol><h1 id="2如何设计keepalive">2.如何设计keepalive</h1><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210922210030164.png" alt="image-20210922210030164" /></p><h1 id="3为什么应用层需要keepalive">3.为什么应用层需要keepalive</h1><p>原因如下：</p><ol><li>协议分层，各层关注点不同：传输层关注是否通，应用层关注是否可服务，例如虽然连接是ok的，但是可能对端应用无法提供正常服务</li><li>TCP 层的 keepalive 默认关闭，且经过路由等中转设备 keepalive 包可能会被丢弃，传输层的调整会影响全部应用</li><li>TCP的keepalive时间太长</li></ol><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210922211148513.png" alt="image-20210922211148513" /></p><h1 id="4什么是idle检测">4.什么是Idle检测</h1><p>Idle检测只是负责对连接的诊断、分析并作出不同的行为，主要用于判断连接是否空闲or连接是否异常，从而来决定是否采取keepalive，有助于减少keepalive的次数</p><p>其中Idle状态分为如下三种:</p><ol><li>readerIdleTime：读超时时间，当前端一段时间未收到对端消息</li><li>writerIdleTime：写超时时间，当前端一段时间未向对端发送消息</li><li>allIdeTime：所有类型（当前端既没有传也没有收到数据）的超时时间</li></ol><pre><code class="language-java">/** * An {@link Enum} that represents the idle state of a {@link Channel}. */public enum IdleState {    /**     * No data was received for a while.     */    READER_IDLE,    /**     * No data was sent for a while.     */    WRITER_IDLE,    /**     * No data was either received or sent for a while.     */    ALL_IDLE}</code></pre><h1 id="5如何使用tcp-keepalive和idle检测">5.如何使用TCP keepalive和Idle检测</h1><p>Server 端开启 TCP keepalive</p><pre><code class="language-java">bootstrap.childOption(ChannelOption.SO_KEEPALIVE,true) bootstrap.childOption(NioChannelOption.of(StandardSocketOptions.SO_KEEPALIVE), true)</code></pre><p>提示：.option(ChannelOption.SO_KEEPALIVE,true) 存在但是无效</p><p>开启不同的 Idle 检测:</p><pre><code class="language-java">ch.pipeline().addLast(&quot;idleCheckHandler&quot;, new IdleStateHandler(0, 20, 0, TimeUnit.SECONDS)); </code></pre><h1 id="6idle检测类包的功能概览">6.Idle检测类包的功能概览</h1><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210923102733067.png" alt="image-20210923102733067" /></p><h1 id="4读idle检测的原理">4.读Idle检测的原理</h1><p>io.netty.handler.timeout.IdleStateEvent：ReaderIdleTimeoutTask</p><h1 id="5写idle检测原理和observeroutput用途">5.写Idle检测原理和ObserverOutput用途</h1><p>io.netty.handler.timeout.IdleStateHandler：WriterIdleTimeoutTask</p><p>其中observeOutput的用处如下，用于写Idle检测判断条件的选择，true-写空闲判断中的写是指有写意向则算写 false-写空闲的判断中的写是指写成功</p><pre><code class="language-java">private boolean hasOutputChanged(ChannelHandlerContext ctx, boolean first) {    if (observeOutput) {        //正常情况下，false，即写空闲的判断中的写是指写成功，但是实际上，有可能遇到几种情况：        //（1）写了，但是缓存区满了，写不出去；（2）写了一个大“数据”，写确实在“动”，但是没有完成。        //所以这个参数，判断是否有“写的意图”，而不是判断“是否写成功”。        // We can take this shortcut if the ChannelPromises that got passed into write()        // appear to complete. It indicates &quot;change&quot; on message level and we simply assume        // that there's change happening on byte level. If the user doesn't observe channel        // writability events then they'll eventually OOME and there's clearly a different        // problem and idleness is least of their concerns.        if (lastChangeCheckTimeStamp != lastWriteTime) {            lastChangeCheckTimeStamp = lastWriteTime;            // But this applies only if it's the non-first call.            if (!first) {                return true;            }        }        Channel channel = ctx.channel();        Unsafe unsafe = channel.unsafe();        ChannelOutboundBuffer buf = unsafe.outboundBuffer();        if (buf != null) {            int messageHashCode = System.identityHashCode(buf.current());            long pendingWriteBytes = buf.totalPendingWriteBytes();            if (messageHashCode != lastMessageHashCode || pendingWriteBytes != lastPendingWriteBytes) {                lastMessageHashCode = messageHashCode;                lastPendingWriteBytes = pendingWriteBytes;                if (!first) {                    return true;                }            }            long flushProgress = buf.currentProgress();            if (flushProgress != lastFlushProgress) {                lastFlushProgress = flushProgress;                if (!first) {                    return true;                }            }        }    }    return false;}</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Netty（二）：源码解析之粘包、半包和编解码]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/n-e-t-t-y--er---yuan-ma-jie-xi-zhi-zhan-bao--ban-bao-he-bian-jie-ma" />
                <id>tag:https://0522-isniceday.top,2022-11-18:n-e-t-t-y--er---yuan-ma-jie-xi-zhi-zhan-bao--ban-bao-he-bian-jie-ma</id>
                <published>2022-11-18T23:24:37+08:00</published>
                <updated>2022-11-18T23:24:37+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<h1 id="1什么是粘包和半包">1.什么是粘包和半包</h1><p>假设我发送一个消息ABC  DEF两块消息，此时接收方不一定是通过两个包就接收到两块消息，可能产生的情况有下述几种</p><p>例如：</p><ol><li>一个包内包含ABCDEF的信息，这种叫做粘包</li><li>也可能分为三个包AB、CD、EF，这种叫做半包</li></ol><p>换个角度理解</p><p>收发角度来看：一个发送可能会被多次接收，多个发送可能会被一次接收</p><p>传输角度来看：一个发送可能占用多个传输包，多个发送可能共用一个传输包</p><p>那么会什么会出现粘包和半包现象呢？</p><h1 id="2为什么tcp会出现粘包和半包">2.为什么TCP会出现粘包和半包</h1><blockquote><p>总结为一句话：TCP是流式协议，消息无边界</p><p>UDP 像邮寄的包裹，虽然一次运输多个，但每个包裹都有“界限”，一个一个签收，所以无粘包、半包问题</p></blockquote><p><strong>粘包的主要原因</strong>：</p><ul><li>发送方每次写入数据 &lt; 套接字缓冲区大小</li><li>接收方读取套接字缓冲区数据不够及时</li></ul><p><strong>半包的主要原因</strong>：</p><ul><li>发送方每次写入数据 &gt; 套接字缓冲区大小</li><li>发送的数据大于协议的 MTU（Maximum Transmission Unit，最大传输单元），必须拆包</li></ul><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210922163825472.png" alt="image-20210922163825472" /></p><h1 id="3解决粘包和半包问题的几种常见方法">3.解决粘包和半包问题的几种常见方法</h1><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210922164056495.png" alt="image-20210922164056495" /></p><h1 id="4netty对三种常用封帧方式的支持">4.Netty对三种常用封帧方式的支持</h1><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210922164351512.png" alt="image-20210922164351512" /></p><p>其中固定长度和分割符的方式编码比较简单，因此netty并没有创建具体的类去实现</p><h1 id="5netty处理粘包半包的源码">5.Netty处理粘包、半包的源码</h1><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210922171345367.png" alt="image-20210922171345367" /></p><h2 id="51解码核心工作流程">5.1.解码核心工作流程</h2><h2 id="52解码中两种数据积累器的区别">5.2.解码中两种数据积累器的区别</h2><h2 id="53三种解码器的常用额外控制参数有哪些">5.3.三种解码器的常用额外控制参数有哪些</h2><h1 id="6二次解码">6.二次解码</h1><h2 id="61什么是二次解码">6.1.什么是二次解码</h2><p>一次解码负责通过新增长度信息等方式解决粘包、半包的问题，二次解码负责将数据按照一定的编码方式（JSON、XML等）进行序列化及反序列化（例如将报文数据反序列化为Java对象）</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210922173415451.png" alt="image-20210922173415451" /></p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210922175434413.png" alt="image-20210922175434413" /></p><h2 id="62选择编解码方式的要点">6.2.选择编解码方式的要点</h2><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210922180222822.png" alt="image-20210922180222822" /></p><h2 id="63protobuf-简介与使用">6.3.Protobuf 简介与使用</h2><p>Protobuf 是一个灵活的、高效的用于序列化数据的协议。</p><p>相比较 XML 和 JSON 格式，Protobuf 更小、更快、更便捷。</p><p>Protobuf 是跨语言的，并且自带了一个编译器（protoc），只需要用它进行编译，可</p><p>以自动生成 Java、python、C++ 等代码，不需要再写其他代码。</p><h2 id="64netty二次解码的源码">6.4.netty二次解码的源码</h2><p>使用示例：io.netty.example.worldclock.WorldClockClientInitializer</p><pre><code class="language-java">/* * Copyright 2012 The Netty Project * * The Netty Project licenses this file to you under the Apache License, * version 2.0 (the &quot;License&quot;); you may not use this file except in compliance * with the License. You may obtain a copy of the License at: * *   http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an &quot;AS IS&quot; BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */package io.netty.example.worldclock;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelPipeline;import io.netty.channel.socket.SocketChannel;import io.netty.handler.codec.protobuf.ProtobufDecoder;import io.netty.handler.codec.protobuf.ProtobufEncoder;import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;import io.netty.handler.codec.protobuf.ProtobufVarint32LengthFieldPrepender;import io.netty.handler.ssl.SslContext;public class WorldClockClientInitializer extends ChannelInitializer&lt;SocketChannel&gt; {    private final SslContext sslCtx;    public WorldClockClientInitializer(SslContext sslCtx) {        this.sslCtx = sslCtx;    }    @Override    public void initChannel(SocketChannel ch) {        ChannelPipeline p = ch.pipeline();        if (sslCtx != null) {            p.addLast(sslCtx.newHandler(ch.alloc(), WorldClockClient.HOST, WorldClockClient.PORT));        }        p.addLast(new ProtobufVarint32FrameDecoder());        p.addLast(new ProtobufDecoder(WorldClockProtocol.LocalTimes.getDefaultInstance()));        p.addLast(new ProtobufVarint32LengthFieldPrepender());        p.addLast(new ProtobufEncoder());        p.addLast(new WorldClockClientHandler());    }}</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Netty（一）：源码解析之三种IO支持]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/n-e-t-t-y--yi---yuan-ma-jie-xi-zhi-san-zhong-i-o-zhi-chi" />
                <id>tag:https://0522-isniceday.top,2022-11-18:n-e-t-t-y--yi---yuan-ma-jie-xi-zhi-san-zhong-i-o-zhi-chi</id>
                <published>2022-11-18T23:23:24+08:00</published>
                <updated>2022-11-18T23:24:48+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<ul><li><a href="#1三种经典io">1.三种经典IO</a></li><li><a href="#2netty对三种io的支持">2.Netty对三种IO的支持</a></li><li><a href="#3netty为什么仅仅支持nio">3.Netty为什么仅仅支持NIO</a></li><li><a href="#4三种io分别采取的开发模式">4.三种IO分别采取的开发模式</a></li><li><a href="#5nio的三种reactor模型的实现">5.NIO的三种Reactor模型的实现</a><ul><li><a href="#51reactor是什么">5.1.Reactor是什么</a></li><li><a href="#52reactor模型详细设计">5.2.Reactor模型详细设计</a></li><li><a href="#53reactor模型的三种实现">5.3.Reactor模型的三种实现</a><ul><li><a href="#531单线程版本的reactor">5.3.1.单线程版本的Reactor</a></li><li><a href="#532主从版本的reactor">5.3.2.主从版本的Reactor</a></li><li><a href="#533单线程的reactor线程池多线程">5.3.3.单线程的Reactor+线程池（多线程）</a></li><li><a href="#534主从的reactor线程池多线程">5.3.4.主从的Reactor+线程池（多线程）</a></li></ul></li></ul></li><li><a href="#5netty关于reactor的实现">5.Netty关于Reactor的实现</a></li></ul><h1 id="1三种经典io">1.三种经典IO</h1><ul><li>阻塞同步IO（BIO）</li><li>非阻塞同步IO（NIO）</li><li>非阻塞异步IO（AIO）</li></ul><p>详细可看：<a href="https://0522-isniceday.top/archives/ji-suan-ji-wang-luo--i-o-mo-xing#more">https://0522-isniceday.top/archives/ji-suan-ji-wang-luo--i-o-mo-xing#more</a></p><h1 id="2netty对三种io的支持">2.Netty对三种IO的支持</h1><p>相关api如下：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210920161809448.png" alt="image-20210920161809448" /></p><h1 id="3netty为什么仅仅支持nio">3.Netty为什么仅仅支持NIO</h1><ul><li><p>为什么不建议（deprecate）阻塞 I/O（BIO/OIO）?</p><p>连接数高的情况下：阻塞 -&gt; 耗资源、效率低</p></li><li><p>为什么删掉已经做好的 AIO 支持？</p><ul><li>Windows 实现成熟，但是很少用来做服务器</li><li>Linux 常用来做服务器，但是 AIO 实现不够成熟</li><li>Linux 下 AIO 相比较 NIO 的性能提升不明显</li></ul></li></ul><h1 id="4三种io分别采取的开发模式">4.三种IO分别采取的开发模式</h1><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210921153548798.png" alt="image-20210921153548798" /></p><p>其中NIO采取的就是我们下文所要详细讲述的Reactor模式</p><blockquote><p>Thread-Per-Connection相当于就是为每一个连接分配一个线程去处理，但是这里会受到<code>C10K</code>的约束</p><p>代码如下：</p><pre><code class="language-java">package com.linwuee.netty.reactor;import java.net.ServerSocket;import java.net.Socket;/** * @Auther: zhang_yx * @Date: 2021/9/21 15:04 */public class ThreadPerConnection {    public static void main(String[] args) {        new Thread(new Server()).start();    }}class Server implements Runnable{    @Override    public void run() {        try {            ServerSocket serverSocket = new ServerSocket(8080);            while (!Thread.interrupted()){                //这里会阻塞                final Socket socket = serverSocket.accept();                new Thread(new Runnable() {                    @Override                    public void run() {                        System.out.println(Thread.currentThread().getId());                        //TODO 获得已连接套接字，去处理请求                    }                }).start();            }        }catch (Exception e){            System.out.println(e);        }    }}</code></pre></blockquote><h1 id="5nio的三种reactor模型的实现">5.NIO的三种Reactor模型的实现</h1><h2 id="51reactor是什么">5.1.Reactor是什么</h2><p>Reactor可以看做一种设计模式，它要求主线程（IO处理单元）只负责监听文件描述符（fd）是否有事件发生，有的话就立即将该事件通知工作线程（handler）。主线程不做除了监听事件之外的任何操作，读写数据，客户端的请求等均在工作线程中完成。其中主线程往往是阻塞在某一个调用上，并监听。</p><p>其中Reactor模式，是处理并发IO比较常见的模式，使用同步非阻塞IO实现。其会将要处理的IO事件放到一个IO多路复用器中，同时主线程阻塞在该多路复用器（select、poll、epoll）上，直到有IO事件准备就绪了，多路解复用器从阻塞处返回，将准备好的IO事件分发到事件处理器（工作线程handler）中</p><p>Reactor是基于非阻塞同步IO+事件驱动技术（或+线程池）的一种并发模型</p><p>Reactor模式通过poll、epoll等IO分发技术实现的一个无限循环的事件分发程序。获取到一个已连接事件之后，会触发添加监听更多的读写事件</p><blockquote><p>其中可总结为：注册感兴趣的事件 -&gt; 扫描是否有感兴趣的事件发生 -&gt; 事件发生后做出相应的处理</p><p>其中事件如下，其中</p><ul><li>客户端的套接字：连接、读、写事件</li><li>服务端的监听套接字：获得连接事件</li><li>服务端的已连接套接字：读、写事件</li></ul><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210921194308472.png" alt="image-20210921194308472" /></p></blockquote><h2 id="52reactor模型详细设计">5.2.Reactor模型详细设计</h2><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20201209004038532-a.png-itzhai" alt="image-20201209004038532" /></p><ul><li>Handle：描述符，或者文件句柄，代表操作的资源，例如socketfd。其中会注册到<code>Synchronous Event Demultiplexer</code>(同步事件多路解复用器)中，进行监听Handle上发生的事件，如：Connect，Read，Write，Close等事件（一个handle会有多个事件）；</li><li>Synchronous Event Demultiplexer(同步事件多路解复用器)：该组件阻塞等待被监听的事件集，等待事件发生，一般使用IO复用技术实现，如select、poll、epoll；</li><li>Reactor：类似于程序门面，持有Event Handler，包含注册、删除Event Handler，其中也持有了Synchronous Event Demultiplexer去执行事件的监听，然后根据发生事件的文件描述符和事件类型交给不同的Event Handler去处理</li><li>Event Handler：事件处理器，处理具体的IO事件，对IO事件作出具体的响应</li></ul><p>其中通过类图也大概能够看出流程很简单，大概如下：</p><ol><li>注册Event Handler（如果是网络IO框架，这一步对应<strong>创建监听套接字</strong>，并且把监听套接字事件处理器注册到Reactor中），其实相当于创建Acceptor，单线程下的Reactor，多路复用器承担了Acceptor的职责</li><li>Reactor调用多路复用器中的监听方法，阻塞获取handles（例如已连接套接字），此时可将handle关联Event Handler并注册入Reactor中，方便该handle产生事件时能够找到handler执行（所以也说handler是回调方法）</li><li>获得监听事件并将事件交给对应的Event Handler去执行（每个Handle都关联了一个Handler）</li></ol><p>现在针对上述流程，也会产生一个问题，就是说当handler执行事件的处理逻辑时，会阻塞多路复用器中的监听方法，因此此时可将事件的处理丢入线程池中取异步执行。</p><h2 id="53reactor模型的三种实现">5.3.Reactor模型的三种实现</h2><p>上面我们说了，事件的再handler中的处理流程可能会阻塞多路复用器的调用，因此可将事件处理丢入线程池去异步执行</p><h3 id="531单线程版本的reactor">5.3.1.单线程版本的Reactor</h3><p>上述我们所讲的Reactor流程就是单线程版本的实现，其中多路复用器承担了Acceptor的职责，大概流程如下：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20201218000043160-a.png-itzhai" alt="image-20201218000043160" /></p><ul><li>Accept接收新连接，把新连接IO读写事件注册到同步事件<strong>多路解复用器</strong>；</li><li>执行dispatch调用多路解复用器阻塞等待IO事件；</li><li>分发事件到特定的Handler中处理，测试会阻塞dispatch流程；</li></ul><p><strong>当描述符可读的时候，才会去执行read，想要往socket写数据的时候，统一先写到输出缓冲区</strong>，等到感知到可写事件的时候，再统一把输出缓冲区的数据写到网卡即可，从而避免了读和写的阻塞。</p><blockquote><p>我们需要明白，多路复用器本质就是将往内核的多次IO操作，通过channel一次完成</p></blockquote><p><strong>单线程版本中的Handler处理会阻塞Reactor的Event Loop线程的IO轮训</strong>，为此，我们<strong>可以把Handler丢到单独的线程池中进行处理</strong></p><h3 id="532主从版本的reactor">5.3.2.主从版本的Reactor</h3><p>单线程版本存在的问题主要是Reactor可能因为Event Handler处理速度太占用CPU时间，并且会阻塞dispatch流程，因此可能会影响连接套接字不能及时通过Reactor注册到<code>Synchronous Event Demultiplexer</code>中进行IO轮训</p><p>因此我们可以将监听套接字（handle）与已连接套接字（handle）交给不同的Reactor模型去监听，因此就有了主从模式</p><p>主Reactor线程只负责执行dispatch阻塞等待监听套接字的连接事件，当有套接字连接之后，随机选择一个Sub-Reactor，把已连接套接字的IO读写事件注册到选择的sub-Reactor线程中</p><p>Sub-Reactor线程负责阻塞等待已连接套接字的IO事件的到来，并且调用IO事件的Event Handler处理IO读写事件。这样就实现了IO的高效分发</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20201217230206165-a.png-itzhai" alt="image-20201217230206165" /></p><p>但是，不管主从Reactor，其中内部对于事件的处理仍然会阻塞dispatch的执行，因此我们还有单线程+主从模式结合线程池的实现</p><h3 id="533单线程的reactor线程池多线程">5.3.3.单线程的Reactor+线程池（多线程）</h3><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20201218000146145-a.png-itzhai" alt="image-20201218000146145" /></p><p>在read到了数据之后，compute这些操作都是业务需要去处理的，因此可将这些内容交给线程池去具体执行，从而加快了时间处理速度</p><h3 id="534主从的reactor线程池多线程">5.3.4.主从的Reactor+线程池（多线程）</h3><p>同理</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20201218000305993-a.png-itzhai" alt="image-20201218000305993" /></p><h1 id="5netty关于reactor的实现">5.Netty关于Reactor的实现</h1><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210921204201212.png" alt="image-20210921204201212" /></p><blockquote><p>我们始终需要明白，单线程，指的是针对socketfd具体事件的处理是单线程的，存在阻塞dispatch的情况</p></blockquote>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[java-卫语（提前退出）]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/j-a-v-a---wei-yu--ti-qian-tui-chu-" />
                <id>tag:https://0522-isniceday.top,2022-07-20:j-a-v-a---wei-yu--ti-qian-tui-chu-</id>
                <published>2022-07-20T11:56:53+08:00</published>
                <updated>2022-07-20T11:56:53+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>函数中的条件逻辑使人难以看清正常的执行途径。使用卫语句表现所有特殊情况。</p><p>动机：条件表达式通常有2种表现形式。</p><p>第一：所有分支都属于正常行为。</p><p>第二：条件表达式提供的答案中只有一种是正常行为，其他都是不常见的情况</p><p>这2类条件表达式有不同的用途。如果2条分支都是正常行为，就应该使用形如if-else的条件表达式；</p><p>如果某个条件极其罕见，就应该单独检查该条件，并在该条件为真时立刻从函数中返回。这种单独检查也称“卫语句”。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[设计思想随笔]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/she-ji-si-xiang-sui-bi" />
                <id>tag:https://0522-isniceday.top,2022-07-20:she-ji-si-xiang-sui-bi</id>
                <published>2022-07-20T11:54:26+08:00</published>
                <updated>2022-07-20T11:54:26+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="秒杀场景">秒杀场景</h2><p>针对秒杀场景，除了将库存等相关信息通过缓存增大吞吐之外，还可以采取将库存记录进行分片到不同的库表，来减少单条记录串行导致吞吐很低的场景</p><h2 id="状态机">状态机</h2><p>为什么要有状态机去梳理状态</p><ol><li>清晰知道状态的变更流程</li><li>状态机的每个状态可以用来标识某个阶段的一致性状态，例如：预申请状态</li></ol><p>开发：</p><ul><li>在分布式事物的场景下，存有状态的流水记录需要在本地事物中去进行插入，并且流水状态变更所在的本地事物中，执行的内容就是与流水记录相关的数据的一致性状态变更为流水状态变更后的另一个一致性状态，这样的好处在于，当通过流水记录的状态，我们能够明确知道与流水相关的业务记录的数据处于何种阶段</li></ul><h2 id="操作流水">操作流水</h2><p>在操作之前先起本地事物插入流水数据</p><p>目的：为了保证数据不丢失，类似于Zk的事务请求先落log、habase、es的WAL</p><h2 id="关于核对">关于核对</h2><p>核对分类</p><ol><li>一致性核对</li><li>准确性核对</li></ol><p>核对场景</p><ol><li>跨团队协作，一个业务场景涉及到多个库表</li><li>库表进行了字段或者枚举的调整</li></ol><p>举例：</p><p>在用户初次绑卡发奖场景，绑卡记录和信息存储在会员库，发奖存储在营销库，此时核对就需要监控营销发奖了，用户绑卡库中的卡号是否是只有一条且是刚绑定的卡（卡号是否匹配肯定有流水进行记录）</p><h2 id="资损分析">资损分析</h2><h3 id="场景识别">场景识别</h3><p>**一致性：**站点体系内部系统之间、系统内部多表之间，站点与商户，站点与渠道的数据一致性；包括状态、金额、币种、条数等。</p><p><strong>正确性：<strong>资损相关业务正确性：资损五大类内</strong>部逻辑计算产出</strong>的，不能借由一致性核对的，均需要做正确性核对。包含：<strong>业务准入（涉及资损风险的错误码） 、业务决策（涉及资损风险的逻辑分支）、专家经验（根据业务特性总结，非代码逻辑）</strong></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[业务开发中的时区问题]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/ye-wu-kai-fa-zhong-de-shi-qu-wen-ti" />
                <id>tag:https://0522-isniceday.top,2022-01-12:ye-wu-kai-fa-zhong-de-shi-qu-wen-ti</id>
                <published>2022-01-12T17:50:01+08:00</published>
                <updated>2022-01-12T17:50:01+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<h2 id="时区划分">时区划分</h2><ul><li>UTC：Coordinated Universal Time－ 世界协调时间，零时区</li><li>CST：4个不同的时区时间<ol><li>Central Standard Time (USA) UT-6:00</li><li>Central Standard Time (Australia) UT+9:30</li><li>China Standard Time UT+8:00</li><li>Cuba Standard Time UT-4:00</li></ol></li><li>GMT：<strong>「格林威治标准时间」(Greenwich Mean Time，简称G.M.T.)</strong></li><li>PST：太平洋标准时间</li></ul><p>关系：</p><p><strong>UTC=GMT</strong></p><p><strong>CST=UTC/GMT +8=PST+8+8</strong></p><p><strong>CST=CET+7</strong></p><p><strong>UTC=PST+8</strong></p><blockquote><p>目前阿里的服务器统一采取的UTC时区，不带有任何地区标识</p></blockquote><h2 id="地时">地时</h2><p>时间由两个维度进行标识：时间值+时区</p><p>地球处于某一自转状态时对应的时间为地时，地时由时间值和时区组成，即(时间值，时区)二维坐标。地球某一自转状态时，不同时区的时间值不相同，但地时为同一个。地时有多种表达方式，每一个时区都对应一种表达方式，但都是等价的。<br />例如，(8:00,UTC+01),(9:00,UTC+02),(10:00,UTC+03),(11:00,UTC+04)<br />这4个地时为等价地时，同一地时在不同时区的转换称为地时的等价转换或恒定转换，将地时转化为相同时区称为地时的共区化。地时只能在相同时区下才能比较，这称为地时的同区比较</p><blockquote><p>因此，对于时间的判断或传递。需要传入时区以及时间值，创建时间也需要包含时区信息，如果未包含，则时间信息无效</p></blockquote><h2 id="业务可能涉及的时区分类">业务可能涉及的时区分类：</h2><h3 id="服务器的系统时间">服务器的系统时间</h3><p>它影响应用中new Date的值和date toString展示的时区标志，注意Date自身不包含时区标志，它本质上只是个时间戳，toString时仅仅取服务器系统时区作为date的时区标志</p><h3 id="sessiontimezone">sessionTimeZone</h3><p>它为应用与db会话时所定的时区，db根据这个时区来判断所接收到date的时区。</p><pre><code class="language-xml ">&lt;bean id=&quot;db_config_shareipayment&quot; class=&quot;com.alipay.zdatasource.LocalTxDataSourceDO&quot;&gt;        &lt;property name=&quot;jndiName&quot; value=&quot;ShareipaymentDataSource&quot;/&gt;        &lt;property name=&quot;connectionURL&quot; value=&quot;jdbc:mysql://ipaymydev.devdb.alipay.net:3306/share_ipayment&quot;/&gt;        &lt;property name=&quot;driverClass&quot; value=&quot;com.mysql.jdbc.Driver&quot;/&gt;        &lt;property name=&quot;connectionProperties&quot;&gt;            &lt;map&gt;                &lt;entry key=&quot;sessionTimeZone&quot; value=&quot;PST&quot;/&gt;            &lt;/map&gt;        &lt;/property&gt;    &lt;/bean&gt;</code></pre><h3 id="数据库的系统时区">数据库的系统时区</h3><p>sql生成的时间的出生时区。如sql中使用set gmt_modified=sysdate()</p><h2 id="时区问题解决">时区问题解决</h2><ol><li><p>采取时间戳，因为不同时区的时间戳值一致，缺点主要是时间戳只能用到204X年</p></li><li><p>确定时区，恒定转换</p><p>在进行时间比较，都需要确定两者的时区一致，或者进行时间的返回渲染页面，也需要确保返回的时间和端的时区一致，以及时间的传递需要保证时间值+时区都穿</p></li></ol><h2 id="洛杉矶时间">洛杉矶时间</h2><p>洛杉矶时间会涉及冬令时和夏令时的变动，需要注意</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[关于博客最近没有更新的那些事儿！！]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/guan-yu-bo-ke-jing-qi-mei-you-geng-xin-de-na-xie-shi-er--ma-yi-nei-tui-zhao-wo--" />
                <id>tag:https://0522-isniceday.top,2022-01-10:guan-yu-bo-ke-jing-qi-mei-you-geng-xin-de-na-xie-shi-er--ma-yi-nei-tui-zhao-wo--</id>
                <published>2022-01-10T22:43:27+08:00</published>
                <updated>2023-03-21T10:26:22+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>从10月份入职蚂蚁金服已经3个月了，然后在上周也成功的进行了转正，近期没有更新博客，也不是没有在学习，而是一直都在学习蚂蚁的相关中间件，例如微服务架构SOFA、SOFABoot、分布式事物XTS、消息队列MsgBroker、AntQ、SOFAMQ、数据源ZDAL、参数中心、DRM等～</p><p>其中蚂蚁的SOFA的技术栈也可以了解一下，主要也是应用于金融领域：<a href="https://www.sofastack.tech/">https://www.sofastack.tech/</a></p><p>最后，内网的技术大佬和优质的技术文章实在是太多了，因此给我学习其他技术并进行梳理的时间越来越少了（主要是公司的内网的很多知识不能传到博客 T.T）</p><p>2023-03-21更新</p><p>状态良好，初代机继续前进，鸟哥的linux私房菜快点撸完啊，不想只会写业务呀。。。</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[关于21年从长沙跳到深圳的面经记录]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/guan-yu-2-1-nian-cong-chang-sha-tiao-dao-shen-zhen-de-mian-jing-ji-lu" />
                <id>tag:https://0522-isniceday.top,2021-12-06:guan-yu-2-1-nian-cong-chang-sha-tiao-dao-shen-zhen-de-mian-jing-ji-lu</id>
                <published>2021-12-06T21:56:24+08:00</published>
                <updated>2023-03-21T10:22:33+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<p>2021-06-02:<br />湖南亚信：<br />1.自我介绍（感觉还是有点乱+紧张）<br />2.项目巴拉巴拉<br />3.Redis的的缓存方案<br />这里没有说好<br />4.读写缓存的写场景该先修改数据库还是缓存<br />修改缓存，没有说好<br />5.如何保证并发下的写场景的数据库和redis的一致性<br />分布式锁<br />6.Redis的分布式锁</p><p>7.公司Redis的部署模式<br />8.Mybatis的dao是否可以重载<br />不能进行重载。。。。</p><p>2021-06-08<br />深圳灰度科技<br />一面：<br />1.自我介绍（这一次要好多了）<br />2.全篇八股文。。。我都不想吐槽了，结果还和我说大小周、994<br />。。。<br />反正全部都是八股文，重点是还是他妈面了一个多小时</p><p>二面：<br />1.基本就是聊天了，谈谈开发啥的<br />人非常nice，聊起来很轻松</p><p>2021-06-08<br />湖南拓维<br />1.自我介绍<br />2.项目介绍<br />3.最近解决的两个问题：死锁的排查及解决、慢接口查询方案<br />4.ArrayList的相关实现<br />比上一家靠谱多了。。。至少不会问你八股文。。。。</p><p>2021-06-15<br />芒果tv（挂了）<br />1.自我介绍什么的<br />2.排序算法有哪些<br />3.快速排序是不是稳定的<br />4.java的Compator.compara底层采取的排序算法是什么<br />5.平时用过哪些数据库，MySQL的索引有哪些分类<br />6.讲下linux的grep的使用<br />这里直接就没让我过。。。。不知道为什么，问的问题也不太多，难道是我开头就说我不想转golang？</p><p>2021-09-03<br />深圳知学云科技<br />1.MySQL排序如果不加order by会怎么样<br />2.MySQL调优的一些问题，也就explain、stack_trace去整就好<br />3.spring的bean的生命周期<br />4.spring的父子容器的关系<br />5.B+和B-树的区别<br />无语，面试挺顺利但是没下文。。</p><p>2021-09-06<br />阿里-蚂蚁<br />技术面-&gt;交叉部门主管面-&gt;主管面-&gt;hr<br />1h40min<br />一面<br />算法题：<br />1.实现一个队列<br />2.队列可能存在哪些并发问题<br />3.能不能通过代码来使其线程安全</p><p>1.自我介绍。。<br />2.基本没有问八股文，全部都是围绕你的项目发起提问，主要是问你项目相关的问题，<br />例如<br />健康检查如果避免手动配置，有没有办法能够做到自动配置<br />threadLocal在mock组件里面如何使用的，你认为你写的mock组件会不会有什么问题，以及如何优化等等等相关问题<br />如果让你实现一个从发起支付到结束的请求的全链路监控，你会如何设计等等很多设计的问题</p><p>二面：<br />50min<br />这轮面试也是基本讨论你的项目，然后八股文的内容很少问，大部分是给场景让你实现的过程或者讨论你的项目细节的时候，会问到一些相关知识<br />1.实现一个线程池，你会怎么做？讲下思路<br />2.用户重复发起支付请求，如何处理？要求采取分布式缓存（分布式锁）、或者不采取分布式缓存使用底层db的方式实现（其实就是幂等性，当时我也没看过幂等性的解决方案，就采取第一种是将互斥量存入db，采取effectrow是否等于1的形式来取锁，还有就是唯一索引+ON DUPLICATE KEY UPDATE的方式）<br />3.还聊了很多项目细节巴拉巴拉，这块偏向于聊天了感觉，感觉这轮更像主管面</p><p>三面：<br />50min<br />这轮基本全是八股文，估计前面两轮没有考察八股文，放在这里考察了<br />1.JVM运行时内存的划分<br />2.一个对象从创建到销毁的内存变化<br />3.JVM一个对象什么时候会从新生代晋升到老年代<br />4.Hashmap讲一下<br />5.hash的扩容机制，如果hashmap扩容之后还是不够怎么办（mmp，JDK集合类包底下偏偏hashmap的源码没有全部都看完，直接不敢回答这个）<br />6.数据库的隔离级别<br />7.讲下幻读并讲下如何解决<br />8.倒排索引讲一下<br />9.动态代理的实现讲一下<br />10.消息队列针对消费端的pull模式和push模式各有什么优缺点<br />大概就这些吧，也不记得漏了没，说我普通话不好，扎心了。。。</p><p>2021-09-08<br />浩鲸<br />45min<br />一面：<br />这个面经我很多就不记得了，因为也是很多八股文，记一下我还记得的<br />1.如何排查连接池中异常的连接池信息（我说通过服务端grep端口，看下那个傻逼ip连的多）<br />2.锁相关的问题，还是那样，老东西（JUC底下的几个工具类和源码、锁相关的管程模型、互斥量、忙等待和阻塞巴拉巴拉够你说20分钟）<br />3.JVM的相关问题，不记得了，反正也是基本概念</p><p>二面：<br />人事面，纯聊天，傻逼公司不建议面</p><p>2021-09-09<br />京东（深圳）<br />50min<br />一面：<br />1.JVM一个对象占用的空间大概多大？<br />2.JVM是否有过调优经验？<br />3.用过哪些工具类或linux上的工具？说下用法和好处<br />4.JVM什么时候会发生内存泄漏<br />5.Redis如何保证高可用<br />6.聊了下项目和实现细节，并问了下mock组件可能会存在内存泄漏的风险点<br />7.JVM的垃圾回收器<br />8.redis的源码有没有看过？讲下redis的skiplist，会不会写c++代码<br />9.网络IO、多路复用相关<br />10.JUC锁相关，问了下对于锁的理解</p><p>二面:</p><p>记录这个文档的时候还没有二面。。。因为已经拿了蚂蚁的offer了，不过京东最后也是过了，深圳的一个中间件团队，不知道最后裁员没有。。。薪资说在我蚂蚁的基础上+2k，不过还是拒绝了</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[计算机网络：总览]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/ji-suan-ji-wang-luo--zong-lan" />
                <id>tag:https://0522-isniceday.top,2021-09-20:ji-suan-ji-wang-luo--zong-lan</id>
                <published>2021-09-20T15:49:42+08:00</published>
                <updated>2021-09-20T15:49:42+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<ul><li><a href="#1网络分层">1.网络分层</a><ul><li><a href="#11osi7层模型">1.1.OSI7层模型</a></li><li><a href="#12tcpip四层模型">1.2.TCP/IP四层模型</a></li><li><a href="#13数据包的封装与分用">1.3.数据包的封装与分用</a></li><li><a href="#14协议簇">1.4.协议簇</a></li></ul></li><li><a href="#2物理层">2.物理层</a></li><li><a href="#3数据链路层">3.数据链路层</a><ul><li><a href="#31数据帧格式">3.1.数据帧格式</a></li><li><a href="#32arp">3.2.ARP</a></li><li><a href="#33交换机">3.3.交换机</a></li></ul></li><li><a href="#4网络层">4.网络层</a><ul><li><a href="#41icmp协议">4.1.ICMP协议</a><ul><li><a href="#411格式">4.1.1.格式</a></li><li><a href="#412查询报文">4.1.2.查询报文</a><ul><li><a href="#4121ping">4.1.2.1.PING</a></li></ul></li><li><a href="#413差错报文">4.1.3.差错报文</a></li></ul></li><li><a href="#42traceroute程序">4.2.traceroute程序</a></li><li><a href="#43网关和路由器">4.3.网关和路由器</a><ul><li><a href="#431ip协议">4.3.1.IP协议</a><ul><li><a href="#4311ip协议特点">4.3.1.1.IP协议特点</a></li></ul></li><li><a href="#4312ip数据报格式">4.3.1.2.IP数据报格式</a></li><li><a href="#432路由器">4.3.2.路由器</a><ul><li><a href="#4321转发网关">4.3.2.1.转发网关</a></li><li><a href="#3222nat网关">3.2.2.2.NAT网关</a></li></ul></li></ul></li><li><a href="#33路由策略">3.3.路由策略</a><ul><li><a href="#331静态路由">3.3.1.静态路由</a></li><li><a href="#332动态路由">3.3.2.动态路由</a><ul><li><a href="#内网路由协议">内网路由协议</a></li><li><a href="#外网路由协议">外网路由协议</a></li></ul></li></ul></li></ul></li><li><a href="#4传输层">4.传输层</a><ul><li><a href="#41udp">4.1.UDP</a><ul><li><a href="#412udp的特点">4.1.2.UDP的特点</a></li><li><a href="#413udp使用场景">4.1.3.UDP使用场景</a></li></ul></li><li><a href="#42tcp">4.2.TCP</a><ul><li><a href="#421tcp的简述">4.2.1.TCP的简述</a></li><li><a href="#422tcp的数据报格式">4.2.2.TCP的数据报格式</a></li><li><a href="#423tcp的特点">4.2.3.TCP的特点</a></li><li><a href="#423连接管理">4.2.3.连接管理</a><ul><li><a href="#4231tcp三次握手">4.2.3.1.TCP三次握手</a><ul><li><a href="#为什么是三次握手而不是两次或者四次">为什么是三次握手，而不是两次或者四次？</a></li></ul></li><li><a href="#4232tcp的四次挥手">4.2.3.2.TCP的四次挥手</a></li></ul></li><li><a href="#424数据传输">4.2.4.数据传输</a></li><li><a href="#4241如何保证可靠传输ack序列号">4.2.4.1.如何保证可靠传输：ACK+序列号</a><ul><li><a href="#4242窗口管理">4.2.4.2.窗口管理</a></li><li><a href="#4243超时重传机制">4.2.4.3.超时重传机制</a></li><li><a href="#4245流量控制">4.2.4.5.流量控制</a></li><li><a href="#4255拥塞控制">4.2.5.5.拥塞控制</a></li></ul></li></ul></li><li><a href="#43socket编程">4.3.Socket编程</a><ul><li><a href="#431是什么">4.3.1.是什么</a></li><li><a href="#432基于tcp的socket通信交互">4.3.2.基于TCP的Socket通信交互</a></li><li><a href="#433基于udp协议的socket通信交互">4.3.3.基于UDP协议的Socket通信交互</a></li><li><a href="#434服务器如何处理更多的请求">4.3.4.服务器如何处理更多的请求</a></li></ul></li></ul></li><li><a href="#5应用层">5.应用层</a><ul><li><a href="#51dns解析">5.1.DNS解析</a><ul><li><a href="#511dns解析流程">5.1.1.DNS解析流程</a></li></ul></li><li><a href="#52http协议">5.2.http协议</a><ul><li><a href="#521http请求的构建">5.2.1.http请求的构建</a></li><li><a href="#522http-返回的构建">5.2.2.HTTP 返回的构建</a></li></ul></li><li><a href="#53https">5.3.https</a><ul><li><a href="#531数字证书">5.3.1.数字证书</a></li><li><a href="#532https的工作模式">5.3.2.https的工作模式</a></li></ul></li></ul></li></ul><h1 id="1网络分层">1.网络分层</h1><h2 id="11osi7层模型">1.1.OSI7层模型</h2><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200708085822940-a.png-itzhai" alt="image-20200708085822940" /></p><h2 id="12tcpip四层模型">1.2.TCP/IP四层模型</h2><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200708234353206-a.png-itzhai" alt="image-20200708234353206" /></p><p>可以发现，TCP/IP体系少了表示层和会话层，数据链路层和物理层用链路层取代。</p><ul><li><code>应用层</code>：<strong>将数据传输给具体的应用</strong>：最高层，应用层的任务是通过应用进程间交互来实现特定网络应用。主要负责把应用程序中的用户数据传达给另一台主机或同一主机上的其他应用程序。这是所有应用程序协议的运行层，如SMTP、FTP、SSH、HTTP等；</li><li><code>传输层</code>：<strong>通用的数据传输服务</strong>：负责向两个主机中的进程之间的通信提供通用的数据传输服务。UDP是基本的传输层协议，提供了不可靠的无连接数据报传输服务；</li><li><code>网络层</code>：<strong>寻址和路由功能</strong>：负责为分组交换网上的不同主机提供通信服务。该层定义了寻址和路由功能，主要协议是IP协议(Internet Protocol)，它定义了IP地址，它在路由中的功能是将数据报传输到充当IP路由器的下一个主机，该主机更接近最终数据目的地；</li><li><code>链路层</code>：<strong>负责处理与传输媒介的相关细节</strong>：也称为数据链路层或者网络接口层，通常包括操作系统中设备驱动程序和计算机对应的网络接口卡</li></ul><h2 id="13数据包的封装与分用">1.3.数据包的封装与分用</h2><p>​<img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200726223316970-a.png-itzhai" alt="image-20200726223316970" /></p><h2 id="14协议簇">1.4.协议簇</h2><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200726204748249-a.png-itzhai" alt="image-20200726204748249" /></p><h1 id="2物理层">2.物理层</h1><p>有哪些通信交互方式？单工、半双工通信、全双工通信？</p><p><strong>单工通信</strong>，又称为单向通信，只有一个方向的通信，如无线电广播，电视广播；</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200712122203747-a.png-itzhai" alt="image-20200712122203747" /></p><p><strong>半双工通信</strong>，又称为双向交替通信，双方都可以收发信息，只能交替进行；</p><p><strong>全双工通信</strong>，又称为双向同时通信，双方可以同时发送和接收数据。</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200712122339778-a.png-itzhai" alt="image-20200712122339778" /></p><h1 id="3数据链路层">3.数据链路层</h1><h2 id="31数据帧格式">3.1.数据帧格式</h2><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200715085955337-a.png-itzhai" alt="image-20200715085955337" /></p><p><code>前导</code>：用在发送方和接收方之间同步时钟和bit流；</p><p><code>SFD</code>：帧开始界定符，只有一个byte，内容固定为：10101011 (0xAB)；</p><p><code>DST</code>：目标MAC地址；</p><p><code>SRC</code>：源MAC地址；</p><p><code>长度或类型</code>：0800时，表示IP数据报，0806表示ARP请求/应答，0835表示RARP请求/应答；</p><p><code>FCS</code>：帧检验序列，用于数据帧的差错检测；</p><h2 id="32arp">3.2.ARP</h2><p>通过ip地址获取目标的mac地址</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200713231114251-a.png-itzhai" alt="image-20200713231114251" /></p><h2 id="33交换机">3.3.交换机</h2><p><strong>为什么需要交换机？</strong></p><p>集线器（hub）：局域网中，一台机器发送信息，会通过hub将消息以广播的形式给其他局域网内的机器，</p><p>交换机：但是hub会有一个缺点，就是会把不属于某台机器的消息也发送给他，因此能不能只给需要的主机发送消息呢？</p><p>因此有了交换机，当一台电脑A向交换机发送数据时，交换机会把电脑A的IP和MAC地址记住，保存到一个<code>转发表</code>中，如果<code>转发表</code>中暂时找不到目标IP地址的MAC地址，那么首先还是会广播消息，最终转发表会记录所有请求过交换机的电脑IP和MAC。当然，转发表也是有过期时间的</p><p><strong>IP地址与MAC</strong>：</p><p>IP地址相当于是收货地址，是会改变的，mac地址相当于网卡的唯一标识，也就是你的主机的身份证，因此网络上发送数据不可能只指定身份证，而需要执行邮寄地址</p><h1 id="4网络层">4.网络层</h1><p>前面我们将的数据链路层，其实只能在局域网内进行通信，因为都是通过MAC地址进行传达信息的，要想跨局域网，那么就得用到IP地址了，这就是网络层要做的事情了。</p><p>首先我们来介绍下网络的一个协议：ICMP协议。</p><h2 id="41icmp协议">4.1.ICMP协议</h2><p>ICMP：用于获取包发送途中的异常探测，诊断信息，以及发送途中经过的路由器、往返时间。IP协议是无法支持这些的</p><p>ICMP并不为IP网络提供可靠性，它只是用于反馈各种故障和配置信息。丢包不会触发ICMP</p><h3 id="411格式">4.1.1.格式</h3><p>ICMP报文是在IP数据报内部传输的，格式如下：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200715084813447-a.png-itzhai" alt="image-20200715084813447" /></p><p>而ICMP报文的格式如下：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200715085522615-a.png-itzhai" alt="image-20200715085522615" /></p><ul><li>类型有15个不同的值，描述特定类型的ICMP报文；</li><li>某些ICMP报文还是用代码字段的值来进一步描述不同的条件；</li><li>校验和字段用于ICMP报文的差错检查。</li></ul><h3 id="412查询报文">4.1.2.查询报文</h3><p>有关信息采集和配置的ICMP报文，ping就是采取的ICMP查询报文</p><h4 id="4121ping">4.1.2.1.PING</h4><p>ping请求的处理流程：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200716085927626-a.png-itzhai" alt="image-20200716085927626" /></p><p>ping程序是用到了网络层的ICMP协议，不经过传输层</p><p><strong>ping失败的原因</strong>：</p><ol><li>IP输入错误</li><li>网络配置不正确，如错误的子网掩码</li><li>防火墙阻止了ping</li><li>硬件故障</li></ol><h3 id="413差错报文">4.1.3.差错报文</h3><p>差错报文是有关IP数据报传递的ICMP报文，要是发送IP数据包中途产生了异常，那么就会响应ICMP报文</p><p>但是不是所有情况都会响应ICMP差错报文，如以下场景：</p><ul><li>ICMP差错报文不会产生另一个ICMP差错报文；</li><li>目的地址是广播地址或者多播地址的IP数据报不会产生差错报文；</li><li>作为链路层广播的数据报不会产生差错报文；</li><li>源地址不是单个主机(源地址为零地址、环回地址、广播地址或者多波地址)的数据报不会产生差错报文；</li></ul><h2 id="42traceroute程序">4.2.traceroute程序</h2><p>traceroute工具用于确定从发送者到目的地路径上的路由器。</p><p>traceroute主要是通过故意设置特殊的TTL，来达到追踪目的地路径上的路由器的功能。</p><blockquote><p><strong>TTL运行原理</strong></p><p>TTL：是 Time To Live的缩写，该字段指定IP包被路由器丢弃之前允许通过的最大网段数量。每经过一个路由器，TTL就会减一，然后再把IP包转发出去，如果TTL减到0了，路由器就会丢弃收到的TTL=0的IP包，并向IP包的发送者发送一个ICMP差错报文，类型为11，代码为0：传输期间生存时间为0。</p></blockquote><h2 id="43网关和路由器">4.3.网关和路由器</h2><h3 id="431ip协议">4.3.1.IP协议</h3><p>IP是TCP/IP协议簇中最核心的协议，所有TCP、UDP、ICMP等数据都以IP数据包格式传输</p><h4 id="4311ip协议特点">4.3.1.1.IP协议特点</h4><ol><li>IP协议是不可靠的传输协议：传输出现异常时，IP层都会丢弃数据包，并且可能会响应一个ICMP差错消息给发送端，而任<strong>何要求的可靠性必须由上层如TCP协议来提供</strong>；</li><li>IP协议是无连接的：每个数据报相互独立，可以不按照发送顺序接受，不用维护连接状态等</li></ol><h3 id="4312ip数据报格式">4.3.1.2.IP数据报格式</h3><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200719221922958-a.png-itzhai" alt="image-20200719221922958" /></p><ul><li>版本：协议版本号，指明IPv4还是IPv6；</li><li>头部长度：最长60个字节；</li><li>服务类型：包含3bit优先权子字段(已被忽略)，4bit TOS子字段(分别代表最小时延、最大吞吐量、最高可靠性和最小费用)和1bit未用位但必须置0；</li><li>总长度：指的是整个IP数据报的长度，单位字节；</li><li>标识符：唯一地标识主机发送的每一份数据报，通常每发送一个数据报就+1；</li><li>标志：主要用于IP分片；</li><li>分片偏移：主要用于IP分片；</li><li>生存期：设置数据报可以经过最多的路由器数；</li><li>协议：主要表明IP数据是什么协议，用于对数据报进行分用；</li><li>头部校验和：校验数据报是否正确；</li><li>源IP地址：发送IP数据报的IP地址；</li><li>目的IP地址：IP数据报目的IP地址；</li><li>选项：可选数据；</li><li>IP数据：具体的IP数据；</li></ul><h3 id="432路由器">4.3.2.路由器</h3><p>路由器一般充当一个网关，属于三层设备。其会把MAC头和IP头取下来根据内容进行处理。路由器有5个网口，分别可以连接5个局域网，每个网口有和局域网的IP地址相同的网段，每个网口都是对应局域网的网关，5个网口中一般包含一个外网网口，外网网口用于连接到WAN上。</p><p>路由器除了具有交换机的功能外，更拥有路由表<strong>作为发送数据包时的依据，在有多种选择的路径中选择最佳的路径。</strong></p><blockquote><p><strong>一层设备、二层设备、三层设备分别有什么区别？</strong></p><p>路由器是属于OSI第三层的产品，交換机是OSI第二层的产品。</p><p>第二层的产品功能在于，将网络上各个电脑的MAC地址记在<code>MAC地址表</code>中，当局域网中的电脑要经过交换机去交换传递数据时，就查询交换机上的MAC地址表中的信息，将数据包发送给指定的电脑，而不会像第一层的产品（如集线器）每台在网络中的电脑都发送。</p><p>而路由器除了有交换机的功能外，更拥有<code>路由表</code>作为发送数据包时的依据，在有多种选择的路径中选择最佳的路径。此外，并可以连<code>接两个以上不同网段的网络</code>，而交换机只能连接两个。路由表存储了（向前往）某一网络的最佳路径、该路径的“路由度量值”以及下一个（跳路由器）</p></blockquote><p>网关地址一般是网段的第一个或者第二个，如192.168.23.0/24这个网段，网关地址可能是192.168.23.1/24或者192.168.23.2/24。</p><p><strong>NAT网关</strong>：在不同的局域网中，私有IP地址是会重复的，而我们要访问公网的时候，一定要分配一个共有IP地址，所以，我们在访问公网的时候，需要路由器帮忙把私有IP变为共有IP，这种叫做NAT网关，普通内网之间的通信用到的称为转发网关。</p><h4 id="4321转发网关">4.3.2.1.转发网关</h4><p><img src="https://cdn.itzhai.com/image-20200719171542654-a.png-itzhai" alt="image-20200719171542654" /></p><p>主机A要访问主机B，流程如下：</p><ul><li><p>主机A发现要访问的主机B不是在同一个网段（是一个网段则直接通过ARP获得目标id的mac地址，将包发送出去即可，怎么判断同一个网段呢？需要 CIDR 和子网掩码），准备先找到<code>网关</code>，把消息发给网关，网关地址是192.168.1.1，主机A通过<code>ARP</code>获取到了网关的MAC地址，然后发送如下数据包：</p><ul><li><pre><code>SRC MAC: 主机A的MACDST MAC: 路由器A的192.168.1.1网口的MACSRC IP : 192.168.1.3DST IP : 192.168.3.4</code></pre></li></ul></li><li><p>路由器A的<code>192.168.1.1</code>网口接收包之后，准备把包转发出去。而路由器A中的路由表中匹配到了，要想发送给<code>192.168.3.4/24</code>，需要从<code>192.168.2.1</code>这个网口出去，下一跳地址为<code>192.168.2.2/24</code>。路由器通过ARP拿到了下一跳<code>192.168.2.2/24</code>d的MAC地址，然后发送如下数据包：</p><ul><li><pre><code>SRC MAC: 路由器A的192.168.2.1网口的MACDST MAC: 路由器B的192.168.2.2网口的MACSRC IP : 192.168.1.3DST IP : 192.168.3.4</code></pre></li></ul></li><li><p>路由器B的<code>192.168.2.2</code>网口接收包之后，准备把包转发出去。路由器B中判断到目标IP在<code>192.168.3.1</code>这个网口所在的局域网，于是通过ARP拿到了<code>192.168.3.4</code>的MAC地址，然后发送如下数据包：</p><ul><li><pre><code>SRC MAC: 路由器B的192.168.3.1网口的MACDST MAC: 主机192.168.3.4网口的MACSRC IP : 192.168.1.3DST IP : 192.168.3.4</code></pre></li></ul></li></ul><p>最终，主机B收到数据包。</p><p>可以发现在转发网关中，<strong>源IP和目的IP地址都是不会变的，因为整个内网不可能有冲突的IP</strong>。</p><p>但是，假如我们要访问外网，情况就不一样了，最终可能会请到到另一个局域网，另一个局域网的私有IP是可能跟我们所在的局域网一样的，为了避免冲突，于是就有了NAT网关。专门在把数据包发送出去之前，把IP改为公网IP。</p><h4 id="3222nat网关">3.2.2.2.NAT网关</h4><p>现在假设主机A要访问另一个城市的主机B，这里为了演示NAT，我们把模型简化一下，假设路由器出去之后就是公网IP了，如下：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200719184500782-a.png-itzhai" alt="image-20200719184500782" /></p><p>假设路由器A和路由器B都直接接入了互联网。</p><p>现在主机A想访问主机B：</p><ul><li><p>由于是不同的局域网，主机A不会知道主机B的IP的，而主机B接入互联网的之后，领取到了一个互联网的IP，就是上图路由器WAN口的IP：<code>203.0.113.103</code>，所以主机B会把这个IP作为主机B的IP，最终发出如下IP数据包：</p><ul><li><pre><code>SRC MAC: 主机A的MACDST MAC: 路由器A的192.168.1.1网口的MACSRC IP : 192.168.1.3DST IP : 203.0.113.103</code></pre></li></ul></li><li><p>192.168.1.1网口接收到包之后，发现要想访问<code>203.0.113.103</code>，就要从<code>203.0.113.102</code>这个网口出去，发给路由器B，路由器B中判断到目标IP就是<code>203.0.113.103</code>这个网口，于是通过ARP拿到了<code>203.0.113.103</code>的MAC地址，然后发送如下数据包：</p><ul><li><pre><code>SRC MAC: 路由器A的203.0.113.102网口的MACDST MAC: 路由器B的203.0.113.103网口的MACSRC IP : 203.0.113.102DST IP : 203.0.113.103</code></pre></li><li><p>因为消息是要发到公网的，最终SRC IP会被NAT转化为公网的IP 203.0.113.102；</p></li></ul></li><li><p>最终路由器B接收到消息，通过NAPT得到最终接收数据报的IP为当前局域网的192.168.1.3/24，最终把消息转发给了这个IP所在的主机B。</p></li></ul><blockquote><p><strong>NAPT是如何把一个公网IP翻译为局域网IP的？</strong></p><p>传统的NAT(traditional NAT)包括基本NAT(basic NAT)和网络地址端口转换(Network Address Port Translation, NAPT)。<strong>基本NAT只执行IP地址的重写</strong>，本质上是将私有地址改写为一个公共地址，这往往取自于一个由ISP提供的地址池或共有地址范围，这种NAT不是最流行的，因为无助于减少需要使用的IP地址数量。</p><p>比较流行的做法是使用NAPT，NAPT使用传输层标识符如TCP或者UDP端口，或者ICMP查询标识符来确定一个特定的数据报到底和NAT内部哪台私有主机相关联。</p><p>如果局域网两个端口号一样，那么<strong>NAPT会重写端口号</strong>，保证不一致。如下图，三个局域网的IP需要转换为公网IP，由于有两个的端口重复了，于是NAPT进行了端口重写：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200719184357681-a.png-itzhai" alt="image-20200719184357681" /></p></blockquote><p><strong>两者主要的区别在于 IP 地址是否改变。不改变 IP 地址的网关，我们称为转发网关；改变 IP 地址的网关，我们称为NAT 网关。</strong></p><h2 id="33路由策略">3.3.路由策略</h2><h3 id="331静态路由">3.3.1.静态路由</h3><p>**静态路由，其实就是在路由器上，配置一条一条规则。**这些规则包括：想访问 B站（它肯定有个网段），从 2 号口出去，下一跳是 IP2；想访问教学视频站（它也有个自己的网段），从 3 号口出去，下一跳是 IP3，然后保存在路由器里。</p><p>每当要选择从哪只手抛出去的时候，就一条一条的匹配规则，找到符合的规则，就按规则中设置的那样，从某个口抛出去，找下一跳 IPX。</p><h3 id="332动态路由">3.3.2.动态路由</h3><h4 id="内网路由协议">内网路由协议</h4><p>基于<strong>链路状态算法</strong>实现的OSPF协议(Open Shortest Path First, 开放式最短路径优先)：主要用于数据中心内部，因此也成为<code>内网路由协议</code>(Interior Gateway Protocol，IGP)，关键是找到最短的路径。</p><p>OSPF是一种链路状态路由协议。可以将其视为网络的分布式地图。</p><h4 id="外网路由协议">外网路由协议</h4><p>基于距离矢量算法实现的BGP协议(Border Gateway Protocol，外网路由协议)：距离矢量，就是每个路由器都保存一个路由表，路由表每行保存了下一跳的路由器，以及距离下一跳路由器的距离。也成为边界网关协议。</p><p>在BGP的世界中，每个路由域都称为自治系统或AS。BGP所做的工作通常是通过选择遍历最少自治系统的路由：最短的AS路径来帮助选择通过Internet的路径。</p><p>我们会把重点放在传输层以上，所以动态路由协议这部分我们暂时不做不深入研究。</p><h1 id="4传输层">4.传输层</h1><h2 id="41udp">4.1.UDP</h2><p>UDP协议处理的事情比较少，类似于IP协议，数据可能丢失，包的顺序无法得到保证，UDP和后边介绍的TCP不一样，是无状态的。</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200719222327297-a.png-itzhai" alt="image-20200719222327297" /></p><ul><li>源端口号：发送数据报方使用的端口号，用于标识发送进程；</li><li>目的端口号：接收数据包方使用的端口号，用于标识接收进程；</li><li>UDP长度：UDP头部和UDP负载数据的字节长度；</li><li>UDP校验和：UDP校验和覆盖UDP头部和UDP数据和一个伪头部(区别：IP头部校验和只覆盖IP头部)，伪头部衍生子IPv4头部字段的12个字节，或者衍生子IPv6头部字段的一个40字节的伪头部；</li><li>负载数据：具体的UDP数据。</li></ul><blockquote><p>UDP和TCP的端口号主要拿来区分哪个进程的数据包，TCP和UDP的端口号相同也没关系，因为两者是相互独立的。每个请求都有源IP、目标IP、源端口号、目标端口、协议五个元素来标识的，每个协议的端口池是完全独立的。在UDP/TCP协议中源端口和目的端口都只有16位，也就是说端口的取值范围为0~65535</p></blockquote><h3 id="412udp的特点">4.1.2.UDP的特点</h3><ol><li>数据可能丢失</li><li>无状态，不需要像TCP那样建立连接</li><li>没有拥塞机制。来一包就发一个</li></ol><h3 id="413udp使用场景">4.1.3.UDP使用场景</h3><ol><li>需要资源少，在网络情况比较好的内网，或者对对包不敏感的场合。如DHCP和TFTP就是基于UDP的</li><li>广播场景，不需要一对一建立连接，如DHCP</li><li>需要时延低，允许丢包，不关注网络拥塞的场景，如视频直播这种流媒体，实时游戏，通信，物联网等领域</li></ol><h2 id="42tcp">4.2.TCP</h2><h3 id="421tcp的简述">4.2.1.TCP的简述</h3><ol><li>TCP是面向连接的可靠的服务，面向连接是指TCP的两个应用程序必须在他们可交换数据之前，相互建立一个TCP连接</li><li>TCP提供了一种面向字节流的抽象概念，TCP不会自动插入记录标志或者消息边界，这意味着TCP没有限制应用程序的写范围。发送端分两次发10字节和30字节，接收端可能会以两个20字节的方式写入</li></ol><h3 id="422tcp的数据报格式">4.2.2.TCP的数据报格式</h3><p><img src="C:/Users/98347/Desktop/%E5%AD%A6%E4%B9%A0%E5%8A%A0%E6%B2%B9%E5%95%8A/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/image/image-20200721222551224-a.png-itzhai" alt="image-20200721222551224" /></p><p>如上图，头部深黄色部分为TCP特有的重点字段，后面TCP相关功能基本都是靠这些特有的字段来实现的。</p><ul><li>源端口号和目的端口号：同UDP一样，主要用于区分数据应该转发给哪个应用；</li><li>序列号：这个序号为了解决乱序的问题，32位无符号数，满了后再从0开始</li><li>确认号：确认已经接收到了哪里，该值表示确认号的发送方期待接受的下一个序列号，该字段只有在ACK位字段被启用的情况下才有效，所以也成为ACK号或者ACK段</li><li>状态位：<ul><li>ACK：回复状态，启用该状态的情况下，确认号有效，连接建立之后一般都是启用状态</li><li>SYN：发起一个连接</li><li>RST：重置连接，经常因为错误导致</li><li>FIN：结束连接，表示该报文的发送方已经结束向对方发送数据</li><li>CWR：拥塞窗口减小，发送方降低发送速率</li><li>ECE：ECN回显，发送方接收到了一个更早的拥塞通告；</li><li>URG：紧急，表示紧急指针字段有效，很少用到；</li><li>PSH：推送，表示接收方应该尽快给应用程序传送这个数据——没有被可靠的实现或用到；</li></ul></li><li>窗口大小：流量的窗口大小，用于流量控制，通信双方各声明一个窗口，这个大小表明了自己当前的处理能力</li><li>校验和：覆盖了TCP的头部和数据，以及伪头部数据(与UDP使用的相似的伪头部进行计算)；</li><li>紧急指针：只有在UGE位启用的是偶才有效</li><li>选项：如最大段大小等其他的可选项；</li><li>数据：TCP数据报的数据内容。</li></ul><h3 id="423tcp的特点">4.2.3.TCP的特点</h3><ol><li>保证数据顺序传输</li><li>丢包重传，消息可靠</li><li>连接维护</li><li>流量控制，保证稳定</li><li>拥塞控制，及时调整，最大程度保证传输正常进行</li></ol><h3 id="423连接管理">4.2.3.连接管理</h3><h4 id="4231tcp三次握手">4.2.3.1.TCP三次握手</h4><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200723224535183-a.png-itzhai" alt="image-20200723224535183" /></p><ul><li>第一次握手：主动连接方发送一个SYN报文段指明自己想要连接的端口号，以及客户端消息的初始化序列化ISN(c)；</li><li>第二次握手：服务器接收到消息后，也发送自己的SYN报文，包含了服务端的初始化序列号ISN(s)，并设置确认号ack=客户端序列号+1；</li><li>第三次握手：客户端应答服务器的SYN，将服务端的序列号+1作为ack返回给服务端。</li></ul><p>总结一下：<strong>客户端与服务端利用SYN报文交换彼此的初始化序列号。在我们熟悉的Socket编程中，三次握手在执行connect的时候触发。</strong></p><blockquote><p>其中的ACK应答和递增的序列化是可靠性的保证。</p></blockquote><h5 id="为什么是三次握手而不是两次或者四次">为什么是三次握手，而不是两次或者四次？</h5><p>如果是两次：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200723001805421-a.png-itzhai" alt="image-20200723001805421" /></p><p>如果去除了客户端的第三次ACK报文，那么服务端就无法知道服务端发送给客户端的ACK报文是否得到相应，可能服务端就直接结束了请求，这个时候再传消息网络层就会收到一个ICMP目的不可达的差错报文。</p><blockquote><p>如果客户端第一次SYN请求服务端没有ACK应答，那么会重发SYN，此时服务端可能会接收到两个SYN报文，但是并不会建立两个连接，因为会对SYN报文的序列号进行去重，</p></blockquote><p>如果是四次：</p><p>因为如果建立连接的双方所发起的SYN报文都得到了响应，双方都知道对方接受了自己的请求，因此没有比较继续发包去确认了</p><h4 id="4232tcp的四次挥手">4.2.3.2.TCP的四次挥手</h4><p><strong>连接的任何一方都可以发起关闭操作，此外，也支持双方同时关闭连接</strong>。在传统的情况下，负责发起关闭连接请求的通常是客户端。</p><p>这个流程又被称为四次挥手：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210829135606775.png" alt="image-20210829135606775" /></p><ul><li>连接的主动关闭者发送一个FIN段请求关闭连接，携带了序列号seq=p，表明自己的当前序列号，并进入FIN_WAIT_1 状态</li><li>连接的被动关闭者进行了ACK回应，并发送ack=p+1表明自己已经接受到了FIN的请求，进入ClOSED-WAIT状态。主动关闭者接收到ACK之后，就进入FIN_WAIT_2 状态（此时如果被动关闭者不继续发送FIN连接，则主动关闭者会一直处于FIN_WAIT_2 状态，但是linux可以通过tcp_fin_timeout 设置一个超时时间）</li><li>连接的被动关闭者也发送一个FIN段请求关闭连接，携带了序列号seq=q，ack=p+1（ack和第二次挥手的一样），告诉主动关闭者已经准备好关闭连接，并进入LAST-ACK状态</li><li>最后连接的主动关闭者接收到了对方的FIN请求，也回应了一个ACK，表明自己已经成功接收到了被动关闭者发送的FIN，并进入TIME-WAIT等待2MSL之后才进入CLOSED状态</li></ul><p><strong>为什么要有TIME-WAIT?</strong></p><p>因为可能主动关闭者可能对于被动关闭者的FIN请求的ACK包对方收不到，此时被动关闭者就会进行重试，而TIME-WAIT就是留了对方重试的时间，如果没有TIME-WAIT，由于端口复用，那么其重试的包可能发送给了另外一个程序，由于序列号的存在，新的程序肯定不会消费该包，并且会发送一个RET给对方，此时对方就明白早已断开连接。</p><blockquote><p>等待的时间设为 2MSL，<strong>MSL</strong>是<strong>Maximum Segment Lifetime</strong>，<strong>报文最大生存时间</strong>，它是任何报文在网络上存在的最长时间，超过这个时间报文将被丢弃</p><p><strong>序列号</strong></p><p>每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的，可以看成一个 32 位的计数器，每 4ms 加一，如果计算一下，如果到重复，需要 4 个多小时，那个绕路的包早就死翘翘了，因为我们都知道 IP 包头里面有个 TTL，也即生存时间</p><p>A 要告诉 B，我这面发起的包的序号起始是从哪个号开始的，B 同样也要告诉 A，B 发起的包的序号起始是从哪个号开始的。为什么序号不能都从 1 开始呢？因为这样往往会出现冲突。</p><p>例如，A 连上 B 之后，发送了 1、2、3 三个包，但是发送 3 的时候，中间丢了，或者绕路了，于是重新发送，后来 A 掉线了，重新连上 B 后，序号又从 1 开始，然后发送 2，但是压根没想发送 3，但是上次绕路的那个 3 又回来了，发给了 B，B 自然认为，这就是下一个包，于是发生了错误。</p></blockquote><h3 id="424数据传输">4.2.4.数据传输</h3><h3 id="4241如何保证可靠传输ack序列号">4.2.4.1.如何保证可靠传输：ACK+序列号</h3><p>假设主机A通过TCP向主机B发送数据，当主机A的数据到达主机B时，主机B会发送一个确认应答消息ACK。主机A收到ACK之后，就知道自己的数据已经被对方接收了</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200724081253191-a.png-itzhai" alt="image-20200724081253191" /></p><h4 id="4242窗口管理">4.2.4.2.窗口管理</h4><p>TCP头部中，为了保证包的<strong>顺序问题</strong>以及<strong>丢包问题</strong>，我们重点可以关注了如下三个字段：</p><ol><li>序列号</li><li>确认号</li><li>窗口：表明自己的处理能力，代表着可用缓存的大小，以字节为单位</li></ol><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200725114826908-a.png-itzhai" alt="image-20200725114826908" /></p><p>滑动窗口：TCP每个端的数据收发量都是通过滑动窗口来实现的，其中包括发送窗口和接收窗口</p><p><strong>发送窗口</strong>：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200725125106588-a.png-itzhai" alt="image-20200725125106588" /></p><ul><li>SND.WND：提供窗口的大小是由返回的ACK中的窗口大小字段来规定的</li><li>SND.UNA：窗口左边界的值</li><li>SND.UNA + SND.WND：记录窗口右边界的值</li><li>SND.NEXT：记录下次发送的数据</li></ul><p>窗口包含下面三个操作：</p><ol><li>关闭：窗口左边界左移，当已发送的数据得到ACK的时候，就会进行关闭</li><li>打开：窗口右边界右移，当<strong>接收端</strong>处理了确认的数据之后，其窗口值就会变大，这个时候通过打开操作让提供窗口大小变大；</li><li>收缩：窗口右边界左移，使得提供窗口大小减小</li></ol><p><strong>接收窗口结构</strong>：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200725143723481-a.png-itzhai" alt="image-20200725143723481" /></p><p>如何保证包的顺序问题：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200725222605426-a.png-itzhai" alt="image-20200725222605426" /></p><h4 id="4243超时重传机制">4.2.4.3.超时重传机制</h4><p>关于如何解决包的丢失问题，有如下三种方案</p><ol><li>基于计时器的重传超时机制(Retransmission Ttimeout, RTO)</li><li>基于反馈信息的快速重传机制</li><li>带选择确认的重传SACK</li></ol><p><strong>基于计时器的重传超时机制(Retransmission Ttimeout, RTO)</strong>：</p><p>TCP发送数据时有一个重传计时器，如果计时器超时仍然没有接收到ACK信息，那么会进行重传操作</p><p>超时触发重传存在的问题是，超时周期可能相对较长。那是不是可以有更快的方式呢</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200725230608270-a.png-itzhai" alt="image-20200725230608270" /></p><blockquote><p>关于重传时间间隔的问题：</p><p>而TCP的基于计时器的重传策略是如果发生重试，可以有两种处理方式：</p><ul><li>一种是基于拥塞控制机制，减小发送窗口大小；</li><li>另一种是超时时间间隔会一直加倍：<strong>每当遇到一次超时重传的时候，都会将下一次超时时间间隔设为先前值的两倍</strong></li></ul><p>重传时间需要讲到<code>自适应重传算法</code>，一种计算重传时间的算法，大致流程：</p><p>TCP通过采样RTT的时间，进行加权平均，算出一个值，最终得到一个估计的重传时间。</p><pre><code>&gt; 初始值：原始值&gt; 测量之后：RTO = RTTs + 4*RTTd&gt; (RTTs：加权平均值，RTTd：偏差值)&gt;</code></pre></blockquote><p><strong>基于反馈信息的快速重传机制</strong>：</p><p>当接收方收到一个序号大于下一个所期望的报文段时，就检测到了数据流中的一个间格，于是发送三个冗余的 ACK，客户端收到后，就在定时器过期之前，重传丢失的报文段。</p><p>例如，接收方发现 6、8、9 都已经接收了，就是 7 没来，那肯定是丢了，于是发送三个 6 的 ACK，要求下一个是 7。客户端收到 3 个，就会发现 7 的确又丢了，不等超时，马上重发。</p><p><strong>带选择确认的重传SACK</strong>：</p><p>这种方式需要在 TCP 头里加一个 SACK 的东西，可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9，有了地图，发送方一下子就能看出来是 7 丢了</p><h4 id="4245流量控制">4.2.4.5.流量控制</h4><p>流量控制总结为一句话就是，控制发送方发送窗口的窗口大小（该字段通过接收方的ACK包返回），如果接收方的消息处理不过来，可以减小发送方的窗口，如果接收方比较空闲，则可以增大发送方的窗口大小</p><p>如果接收方的包应用程序一直没有处理，最终会导致接收端没有更多空间来存储达到的数据，那么窗口右边界可能就不会打开了，最终接收的窗口大小变为0</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200726121531246-a.png-itzhai" alt="image-20200726121531246" /></p><p>此时，接收方会发送一个零窗口通告（TCP ZeroWindow），告知发送端不要再发送数据了，我已经处理不过来了，于是发送方就暂停发送数据了，等待接收端的窗口更新（TCP Window Update）通知</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200726133315644-a.png-itzhai" alt="image-20200726133315644" /></p><p>接收方为了防止发送方通知窗口更新的ACK消息丢失，会定时方式（TCP ZeroWindowProbe）请求，要求接收端返回<code>TCP ZeroWindowProbeAck</code>，看看是否窗口是否已经增加了</p><h4 id="4255拥塞控制">4.2.5.5.拥塞控制</h4><p>流量控制通过滑动窗口实现，而拥塞控制则是通过拥塞窗口了，其本质也是为了避免丢包和超时重传</p><p><strong>反映网络传输能力的变量称为拥塞窗口(congestion window)，记为cwnd。</strong></p><p>可以理解为**滑动窗口是为接收方服务的，而拥塞窗口是为整个网络通道服务的，拥塞窗口大小又会受制于接收方滑动窗口大小，并且会因为网络原因进行调整。**因为网络通道中的任何一个环节都有可能影响整体的传输效率。</p><p><strong>实际可用窗口大小</strong>：W为接收端滑动窗口awnd和拥塞窗口cwnd的较小者，W = min(cwnd, awnd)</p><blockquote><p>那么发送方如何判断网络是不是满了呢？</p><p>对于TCP而言，整个网络路径对其都是一个黑盒，而TCP的拥塞控制主要用来避免<strong>丢包</strong>和<strong>超时重传</strong>，如果发生了这两种现象，就说明发送速度太快，要慢一点</p><p>那么发送方如何控制拥塞窗口的变化呢？</p><p>答案就是慢启动：</p><p>当TCP建立连接后，cwnd设置为1，当接收到1个ACK之后，cwnd加1，两个ACK就加2，可以看出这是指数级增长，涨到什么时候是个头呢？其中有一个阈值ssthresh （slow start threshhold）为65535 ，当超过这个值时，我们可能就需要调整我们cwnd的增长策略了，此时cwnd会调整为收到一个ack，增长1/8，收到8个ACK，cwnd才加1，此时增长的速度明显放缓</p><p>如果碰到了丢包和超时重传，拥塞窗口cwnd会如何变化？</p><p>两种方案：</p><ol><li>此时代表出现了拥塞，这个时候会将ssthresh 设为cwnd/2，cwnd重新设置为1，重新开始慢启动。这真是一旦超时重传，马上回到解放前。但是这种方式太激进了，将一个高速的传输速度一下子停了下来，会造成网络卡顿</li><li>前面讲的快速重传算法，接收端发现丢了包时，，发送三次前一个包的 ACK，于是发送端就会快速的重传，不必等待超时再重传。TCP 认为这种情况不严重，因为大部分没丢，只丢了一小部分。此时，cwnd减半为cwnd/2，shthresh = cwnd，并且此时一个ACK，cwnd还是增长1，然后往复。</li></ol><p>但是单独依靠丢包和超时重传来判断拥塞也不太准确！？：</p><p><strong>第一个问题</strong>是丢包并不代表着通道满了，也可能是管子本来就漏水。例如公网上带宽不满也会丢包，这个时候就认为拥塞了，退缩了，其实是不对的。</p><p><strong>第二个问题</strong>是 TCP 的拥塞控制要等到将中间设备都填充满了，才发生丢包，从而降低速度，这时候已经晚了。其实 TCP 只要填满管道就可以了，不应该接着填，直到连缓存也填满。</p><p>为了优化这两个问题，后来有了<strong>TCP BBR 拥塞算法</strong>。它企图找到一个平衡点，就是通过不断的加快发送速度，将管道填满，但是不要填满中间设备的缓存，因为这样时延会增加，在这个平衡点可以很好的达到高带宽和低时延的平衡。</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210829162358023.png" alt="image-20210829162358023" /></p></blockquote><h2 id="43socket编程">4.3.Socket编程</h2><h3 id="431是什么">4.3.1.是什么</h3><p>Socket是一个抽象层，主要是把TCP/IP复杂的操作抽象成几个简单的接口提供给应用层调用，进而实现应用进程在网络中通信。Socket主要是端到端之间的传输协议(网络层之上的协议)。</p><blockquote><p><strong>Socket是一种高层的抽象网络API，是一种端到端的通信，只能访问到端到端协议之上的网络层和传输层</strong></p></blockquote><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200726175908288-a.png-itzhai" alt="image-20200726175908288" /></p><h3 id="432基于tcp的socket通信交互">4.3.2.基于TCP的Socket通信交互</h3><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210829165335199.png" alt="image-20210829165335199" /></p><ul><li><p>bind（）：TCP服务需要监听IP和端口：</p><p>接受到的网络包，需要根据端口来找到应用程序，而ip是因为存在多个网卡，就会有多个ip，因此只能读取监听的ip上网卡的数据</p></li><li><p>listen（）：当服务端有了 IP 和端口号，就可以调用 listen 函数进行监听，当调用这个函数之后，服务端就进入了这个状态，这个时候客户端就可以发起连接了（connect()），通过socket创建的套接字为主动套接字，在执行listen函数之后，指示内核应该接收指向该套接字的连接请求，并返回一个监听套接字</p><p>以下是listen()[<a href="https://www.itzhai.com/articles/necessary-knowledge-of-network-programming-graphic-socket-core-insider-and-five-io-models.html#fn3">3]</a>函数定义：</p><pre><code class="language-c">#include &lt;sys/types.h&gt;          /* See NOTES */#include &lt;sys/socket.h&gt;int listen(int sockfd, int backlog);</code></pre><p>backlog是已连接队列和未完成连接队列的和</p></li><li><p>accept（）：</p><p>内核中，每个Socket维护两个队列：</p><ol><li>已建立连接的队列，这时候连接三次握手已经完毕，处于 established 状态</li><li>还没有完全建立连接的队列，这个时候三次握手还没完成，处于 syn_rcvd 的状态</li></ol><p>服务端调用 accept 函数，拿出一个已经完成的连接进行处理。如果还没有完成已完成的连接，此时就需要服务器进入阻塞等待状态，直到获取客户端的已连接套接字并返回</p><blockquote><p>如下图，在调用accept()之后，阻塞等待客户连接到达，然后获取一个已连接套接字：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20201025165112038-a.png-itzhai" alt="image-20201025165112038" /></p></blockquote></li><li><p>connect（）：服务端等待的时候，客户端可以通过connect发起连接，内核会给客户端分配一个临时端口，一旦握手成功，服务端的accept就会返回另外一个socket（连接Socket，后续数据都是通过这个Socket进行传输）</p></li></ul><blockquote><p>监听的Socket和真正用来传输数据的Socket是两个，一个叫做监听Socket，一个叫做连接Socket</p><p><strong>关于监听套接字和已连接套接字</strong></p><p>注意，这里要区分好服务端的监听套接字和已连接套接字，服务端调用socket()返回的是<code>监听套接字</code>，bind()和listen()函数入参也是监听套接字。</p><p>一旦有客户端请求过来了于是产生了一个<code>已连接套接字</code>，后续和客户端的交互是通过这个已连接套接字进行的。监听套接字<strong>只负责监听客户端请求并获取和客户端的已连接套接字</strong>，其中每建立一个请求都会产生一个已连接套接字，但是监听套接字只会有一个。</p></blockquote><p><strong>Socket的本质</strong>：</p><p>内核中，Socket是一个文件，对应的就有文件描述符，每一个进程都有一个数据结构taskStruct，其中存储了进程级的文件描述符列表的指针file_struce，而文件描述符fd就是一个非负整数，是这个数组的下标，而数组存储了该fd指向系统级文件描述表（file_table）表项的指针，该表项就实际存储了inode的地址，但是该inode并不像其他文件一样存储在磁盘，而是存储在内存中，在这个 inode 中，指向了 Socket 在内核中的 Socket 结构</p><p>Socket结构中主要是两个队列：队列中存储的是一个缓存sk_buff，这个缓存中能够看到完整的包的结构（该结构就是TCP中的两个滑动窗口吗？），</p><ol><li>发送队列</li><li>等待队列</li></ol><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210829172155297.png" alt="image-20210829172155297" /></p><h3 id="433基于udp协议的socket通信交互">4.3.3.基于UDP协议的Socket通信交互</h3><p>UDP不需要listen以及accept，但是需要bind IP和端口，UDP 是没有维护连接状态的，因而不需要每对连接建立一组 Socket，而是只要有一个 Socket，就能够和多个客户端通信。也正是因为没有连接状态，每次通信的时候，都调用 sendto 和 recvfrom，都可以传入 IP 地址和端口。</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210829172341089.png" alt="image-20210829172341089" /></p><h3 id="434服务器如何处理更多的请求">4.3.4.服务器如何处理更多的请求</h3><p>一个Socket处理的请求受限于什么呢？</p><ol><li><p>理论上的最大连接数</p><p>{本机 IP, 本机端口, 对端 IP, 对端端口}其中只有对端ip和对端端口可以发生改变</p><p>最大 TCP 连接数 = 客户端 IP 数×客户端端口数。对 IPv4，客户端的 IP 数最多为 2 的 32 次方，客户端的端口数最多为 2 的 16 次方，也就是服务端单机最大 TCP 连接数，约为 2 的 48 次方。</p></li><li><p>文件描述符的限制</p></li><li><p>内存的限制</p></li></ol><p>因此针对该进程记录所有连接套接字socket的fd而言，如果单线程去处理所有的socket（fd list），那么可能会非常的慢</p><p>因此在资源有限的情况下，想要处理更多的请求，就需要降低每个项目所占用的资源</p><ol><li><p>方法一：将项目外包给其他公司（多进程方式）</p><p>主要通过fork的方式实现，因为fork会复制父进程的进程级别的fd列表，因此fork出的子进程也可以去处理已建立连接的socket请求，但是采取fork方式过于麻烦</p></li><li><p>方法二：将项目转包给独立的项目组（多线程方式）</p><p>针对进程级别的fd列表，该方法会创建多个线程去处理socket的数据，例如一个连接socket就创建一个线程去处理，但是这种方式会受限于计算机的性能，一台机器无法创建很多进程或者线程。有个<strong>C10K</strong>，它的意思是一台机器要维护 1 万个连接，就要创建 1 万个进程或者线程，那么操作系统是无法承受的。如果维持 1 亿用户在线需要 10 万台服务器，成本也太高了</p></li><li><p>方法三：一个项目组支撑（IO多路复用，一个线程维护多个Socket）</p><p>将进程级别的文件描述符列表（也就是socket列表）的数据放在一个文件描述符集合fd_set中，然后通过select函数去监听这个集合是否发生了变化，一旦有变化那么就会依次查看每个文件描述符。那些有变化的文件描述符在fd_set对应的位设置为1，表示socket可读或者可写，从而可以进行读写操作，然后再调用 select，接着盯着下一轮的变化。但是这种方式采取的是轮询的方式去查看fd_set的改变情况，大大限制的socket的数量（ 通过FD_SETSIZE 限制）</p></li><li><p>方法四：一个项目组支撑多个项目（IO 多路复用，从“派人盯着”到“有事通知”）</p><p>将select的轮询改为事件通知机制会好很多，当socket发生变化的时候，主动通知项目组，然后项目组再根据然后项目组再根据项目进展情况做相应的操作。</p><p>epoll函数：通过注册callback函数的方式，当某个fd发生变化时，就会主动通知</p></li></ol><h1 id="5应用层">5.应用层</h1><h2 id="51dns解析">5.1.DNS解析</h2><p>DNS服务器：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210831142351297.png" alt="image-20210831142351297" /></p><ul><li>根DNS服务器：返回顶级域 DNS 服务器的 IP 地址</li><li>顶级域DNS服务器：返回权威 DNS 服务器的 IP 地址</li><li>权威DNS服务器：返回相应主机的 IP 地址</li></ul><h3 id="511dns解析流程">5.1.1.DNS解析流程</h3><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210831142633906.png" alt="image-20210831142633906" /></p><p><strong>DNS 递归查询过程</strong>如下：</p><ol><li>先去本地缓存host文件查找</li><li>如果没有再去请求本地DNS服务器，也就是网络服务商（ISP）的服务器</li><li>本地DNS如果找不到则去请求根域名服务器，根域名服务器全球共有13套，不用于直接解析</li><li>然后根域名服务器根据一级域名（.com、.cn等）转向顶级域名服务器，然后根据二级域名，例如163.com，让本地DNS服务器去寻找权威DNS域名服务器</li><li>此时权威DNS域名服务器会将域名对应的IP地址告诉本地DNS服务器</li><li>本地DNS服务器再将IP信息返回给客户端，再去建立连接</li></ol><h2 id="52http协议">5.2.http协议</h2><p>http是基于TCP协议的，因此需要先建立TCP连接，</p><blockquote><p>目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面，默认是开启了 Keep-Alive 的，这样建立的 TCP 连接，就可以在多次请求中复用</p></blockquote><h3 id="521http请求的构建">5.2.1.http请求的构建</h3><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210831150312910.png" alt="image-20210831150312910" /></p><p>请求行：</p><ul><li>URL：例如 <a href="http://www.163.com">http://www.163.com</a></li><li>版本：HTTP 1.1</li><li>方法：<ol><li>GET</li><li>POST</li><li>PUT</li><li>DELETE</li></ol></li></ul><p>首部（header）：首部是 key value，通过冒号分隔。这里面，往往保存了一些非常重要的字段。</p><ul><li>Accept-Charset：客户端可以接收的字符集</li><li>Content-Type：正文的格式，例如JSON</li><li>Cache-control：当客户端发送的请求中包含 max-age 指令时，如果判定缓存层中，资源的缓存时间数值比指定时间的数值小，那么客户端可以接受缓存的资源；当指定 max-age 值为 0，那么缓存层通常需要将请求转发给应用集群</li><li>If-Modified-Since：如果服务器的资源在某个时间之后更新了，那么客户端就应该下载最新的资源；如果没有更新，服务端会返回“304 Not Modified”的响应，那客户端就不用下载了，也会节省带宽</li></ul><h3 id="522http-返回的构建">5.2.2.HTTP 返回的构建</h3><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210831151500469.png" alt="image-20210831151500469" /></p><p>状态行：HTTP请求的结果</p><p>首部（header）：</p><p>实体：</p><h2 id="53https">5.3.https</h2><p>http协议会有报文被截取的风险，因此我们需要针对报文进行加密，加密方式分为如下两种：</p><ol><li><p>对称加密</p><p>双方都持有一个秘钥，然后双方都通过此秘钥进行加密解密，但是秘钥也需要在互联网上传播，因此也有被截取的风险，因此这个方法也不可靠</p></li><li><p>非对称加密</p><p>非对称加密是指私钥放在服务端，公钥放在客户端，客户端通过公钥加密的报文只能通过私钥去解密，但是服务端返回给客户端的报文也需要加密，此时只能够客户端也去持有一个公钥私钥，然后服务端通过客户端的秘钥去加密报文并返回，此时又会有一个问题，那就是客户端和服务端如何去获取对方的公钥呢？这也是<strong>数字证书</strong>解决的问题</p></li></ol><h3 id="531数字证书">5.3.1.数字证书</h3><p>如果我创建了一个公钥私钥，服务端或者客户端如何能保证这个公钥是对的，会不会有黑客冒充给我公钥呢？</p><p>这个时候就需要权威部门的介入了，就是说每个人都能有自己的秘钥，但是只有权威部门（CA）盖章的公钥才是准确的，而这个我们就称为证书。</p><blockquote><p><strong>证书有什么？</strong></p><ol><li>公钥</li><li>证书所有者，</li><li>发布机构和有效期</li></ol><p><strong>证书如何生成呢？会不会有人冒充生成证书呢？</strong></p><p>生成证书需要发起一个请求，然后将这个请求发给一个权威机构去认证，这个权威机构我们称为<strong>CA</strong>（ <strong>Certificate Authority</strong>）。</p><p>证书请求可以通过这个命令生成。</p><pre><code>openssl req -key cliu8siteprivate.key -new -out cliu8sitecertificate.req</code></pre><p>请求CA机构对证书盖章时候，会同时根据请求内容生成一个签名，同时发送给CA机构，相当于CA机构给服务端背书，CA会针对这个签名进行加密（使用CA的密匙）</p><p>此时客户端如果想拿服务端的公钥，就需要通过证书去获取（服务端会将证书传给客户端），客户端可以通过CA的公钥去解密外卖证书的签名（该签名在服务端CA签名的时候会发给CA机构，CA机构会根据CA机构的私钥去对签名进行加密），客户端通过CA公钥解密成功，并且验证签名无误之后，就说明这个证书是正确的</p><p><strong>客户端的CA公钥哪里来的？</strong></p><p>默认安装在操作系统中，并且有一个证书管理器去管理</p><p><strong>客户端的CA公钥可不可能也不可靠呢?</strong></p><p>会出现，但是证书以证书链的形式存在，就是说，CA 的公钥也需要更牛的 CA 给它签名，然后形成 CA 的证书。要想知道某个 CA 的证书是否可靠，要看 CA 的上级证书的公钥，能不能解开这个 CA 的签名。样层层上去，直到全球皆知的几个著名大 CA，称为<strong>root CA</strong>，做最后的背书。通过这种<strong>层层授信背书</strong>的方式，从而保证了非对称加密模式的正常运转。</p></blockquote><h3 id="532https的工作模式">5.3.2.https的工作模式</h3><p>非对称加密在性能上不如堆成加密，因此https协议实际是将两者结合，公钥私钥主要用于传输对称加密的秘钥，而真正的双方大数据量的通信都是通过对称加密进行的</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210831161135767.png" alt="image-20210831161135767" /></p><p>参考链接：</p><p><a href="https://www.itzhai.com/articles/comprehend-the-underlying-principles-of-network-programming.html">https://www.itzhai.com/articles/comprehend-the-underlying-principles-of-network-programming.html</a></p><p><a href="https://time.geekbang.org/column/intro/100007101">https://time.geekbang.org/column/intro/100007101</a></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[计算及网络：IO模型]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/ji-suan-ji-wang-luo--i-o-mo-xing" />
                <id>tag:https://0522-isniceday.top,2021-09-20:ji-suan-ji-wang-luo--i-o-mo-xing</id>
                <published>2021-09-20T15:48:42+08:00</published>
                <updated>2021-09-20T15:48:42+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<ul><li><a href="#1为什么io会阻塞">1.为什么IO会阻塞</a><ul><li><a href="#11tcp中的发送缓冲区和接收缓冲区">1.1.TCP中的发送缓冲区和接收缓冲区</a></li><li><a href="#12udp的阻塞情况">1.2.UDP的阻塞情况</a></li><li><a href="#13accept阻塞">1.3.ACCEPT阻塞</a></li><li><a href="#14connect阻塞">1.4.Connect阻塞</a></li></ul></li><li><a href="#2五种io模型">2.五种IO模型</a><ul><li><a href="#21阻塞io模型">2.1.阻塞I/O模型</a></li><li><a href="#22非阻塞io模型">2.2.非阻塞I/O模型</a></li><li><a href="#23io复用模型">2.3.IO复用模型</a></li><li><a href="#24信号驱动式io模型">2.4.信号驱动式IO模型</a></li><li><a href="#25异步io模型">2.5.异步IO模型</a></li></ul></li><li><a href="#3io复用模型">3.IO复用模型</a><ul><li><a href="#31select">3.1.select</a></li><li><a href="#32poll">3.2.poll</a></li><li><a href="#33epoll">3.3.epoll</a></li></ul></li></ul><p>转载链接：<a href="https://www.itzhai.com/articles/necessary-knowledge-of-network-programming-graphic-socket-core-insider-and-five-io-models.html">https://www.itzhai.com/articles/necessary-knowledge-of-network-programming-graphic-socket-core-insider-and-five-io-models.html</a></p><h1 id="1为什么io会阻塞">1.为什么IO会阻塞</h1><h2 id="11tcp中的发送缓冲区和接收缓冲区">1.1.TCP中的发送缓冲区和接收缓冲区</h2><p><strong>TCP的发送缓冲区</strong>：TCP的接收队列的数据没有应用程序读取或者读取的速度比较慢，发送缓冲区已发送未确认的数据越来越多，最终导致输出操作阻塞</p><p><strong>TCP的接收缓冲区</strong>：同样的，接收端接收数据后放入到接收缓冲区中，然后对接收的数据返回一个ack确认信息，如果接收端还没有发送ack信息出去，或者应用进程一直没来取接受缓冲区已接受已确认的数据，那么就不会清除接收缓冲区的数据。</p><h2 id="12udp的阻塞情况">1.2.UDP的阻塞情况</h2><p>UDP的输入操作：如果UDP套接字没有一个完整的数据报可读，那么就会导致输入操作阻塞；</p><p>UDP的输出操作：UDP没有真正的发送缓冲区，内核只是复制应用进程数据并把它沿着协议向下传送，所以对于一个设置为阻塞的UDP套接字，输出函数不会因为与TCP同样的原因导致阻塞，但是可能因为其他原因而阻塞。</p><h2 id="13accept阻塞">1.3.ACCEPT阻塞</h2><p>如果针对一个阻塞的套接字调用accept函数，并且连接请求还未到达，那么进程会进入睡眠状态，阻塞代表的是当前进程需要等待该函数执行完毕才可以进行下一步的处理（因此Redis的IO模型中listen返回的监听套接字都是非阻塞模式）。对于一个非阻塞的套接字，这种情况，会立刻返回一个<code>EWOULDBLOCK</code>错误。</p><h2 id="14connect阻塞">1.4.Connect阻塞</h2><p>如果对一个阻塞的套接字调用connect函数，一直要等到收到发出的SYN的ACK为止才返回，所以每个connect至少会阻塞进程至少一个到服务器的RTT时间。对于非阻塞的套接字，这种情况会返回一个<code>EINPROGRESS</code>错误。</p><p><strong>为了避免阻塞导致IO性能下降，所以衍生出了几种IO模型，下面我们来介绍下。</strong></p><h1 id="2五种io模型">2.五种IO模型</h1><blockquote><p>下述的IO模型可以看做从已连接套接字中读取TCP数据这一IO过程，因为socketfd的数据都存储在内核中</p></blockquote><p>一般而言，一个输入操作一般会经历如下过程：</p><ol><li>等待数据准备好</li><li>从内核复制到进程：这里的数据复制，一般是应用进程调用了某个IO方法之后，陷入系统调用，在内核态完成的</li></ol><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20201028084848560-a.png-itzhai" alt="image-20201028084848560" /></p><p>下面我们通过UDP的<code>recvfrom()</code>函数来说明具体的IO模型。</p><h2 id="21阻塞io模型">2.1.阻塞I/O模型</h2><p>阻塞主进程：会等待数据准备好，例如使用了阻塞的已连接套接字，因为会导致send（）阻塞读取tcp的数据</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20201029085102627-a.png-itzhai" alt="image-20201029085102627" /></p><p>如上图，在应用进程调用 recvfrom()之后，陷入内核态，直到数据报到达并且复制到应用进程缓冲区之后才返回到用户态。</p><p>或者在系统调用期间发生错误，也会立刻返回。</p><p>这种I/O称为阻塞I/O。</p><h2 id="22非阻塞io模型">2.2.非阻塞I/O模型</h2><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20201029085245393-a.png-itzhai" alt="image-20201029085245393" /></p><p>轮询：当我们将套接字设置为<strong>非阻塞模式</strong>的时候，内核会这样处理IO操作，原本IO操作需要阻塞主进程，当非阻塞时，是直接报错，还不是阻塞主进程。上面的例子中，调用recvfrom()之后，因为数据没有准备好，所以内核直接返回一个EWOULDBLOCK错误，直到数据准备好了，才复制数据到进程空间，并返回系统调用继续处理进程逻辑。</p><p>而应用进程会不断轮询调用<strong>recvfrom()函数，这种处理方式我们称为<code>polling</code>(轮训)</strong>，持续到轮训内核，查看数据是否准备好。<strong>这种方式的缺点是会消耗大量的CPU时间</strong>。</p><h2 id="23io复用模型">2.3.IO复用模型</h2><p>IO复用：通过一个支持同时感知多个描述符的函数系统调用，阻塞在这个系统调用。等待某一个或某几个描述符准备就绪，再去读写。例如select，poll，epoll系统调用可以实现此类功能功能。这种模型不用阻塞在真正的I/O系统调用上。</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20201029085514237-a.png-itzhai" alt="image-20201029085514237" /></p><p>与非阻塞IO相比，其实就是将轮询每一个fd抽象为了select函数调用来实现</p><blockquote><p>在多线中使用阻塞I/O，即每个文件描述符一个线程，与I/O复用模型很类似，每个线程可以自由调用阻塞式I/O系统调用。</p></blockquote><h2 id="24信号驱动式io模型">2.4.信号驱动式IO模型</h2><p>所谓信号驱动式I/O(signal-driven I/O)，就是指在描述符准备就绪的时候，让内核发送一个SIGIO信号通知应用进程进行后续的数据读取等处理。工作原理如下图所示：</p><p><img src="https://cdn.itzhai.com/image-20201101160607451-a.png-itzhai" alt="image-20201101160607451" /></p><p>注册了SIGIO信号处理函数，开启了信号驱动式IO之后，就可以继续执行程序了，等到数据报准备好之后，内核会发送一个SIGIO信号给应用进程，然后应用进程在信号处理函数中调用recvfrom读取数据报。</p><p>这种模型在内核等待数据报达到期间<strong>进程不会被阻塞</strong>，可以继续执行</p><h2 id="25异步io模型">2.5.异步IO模型</h2><p>可以发现，上面所有的I/O模型都会在某一个执行点阻塞，并不是真正的异步的。其实就是通知的时间不同</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20201031102929395-a.png-itzhai" alt="image-20201031102929395" /></p><p>通过异步处理函数如aio_read告知内核启动某个动作，并且让内核在整个操作完成之后再通知应用进程，内核会在把数据复制到用户空间缓冲区之后再进行通知。整个IO过程应用进程都不会被阻塞</p><h1 id="3io复用模型">3.IO复用模型</h1><h2 id="31select">3.1.select</h2><h2 id="32poll">3.2.poll</h2><h2 id="33epoll">3.3.epoll</h2>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Spring（一）：IoC原理及源码解析.md]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/s-p-r-i-n-g--yi---i-o-c-yuan-li-ji-yuan-ma-jie-xi--m-d" />
                <id>tag:https://0522-isniceday.top,2021-08-30:s-p-r-i-n-g--yi---i-o-c-yuan-li-ji-yuan-ma-jie-xi--m-d</id>
                <published>2021-08-30T23:16:10+08:00</published>
                <updated>2021-08-30T23:16:10+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<ul><li><a href="#1ioc容器是什么">1.IoC容器是什么</a></li><li><a href="#2spring-ioc设计原理">2.Spring IoC设计原理</a></li><li><a href="#3ioc的设计与实现beanfactory和applicationcontext">3.IoC的设计与实现：BeanFactory和ApplicationContext</a><ul><li><a href="#31beanfactory容器设计">3.1.BeanFactory容器设计</a></li><li><a href="#32applicationcontext">3.2.ApplicationContext</a><ul><li><a href="#321applicationcontext的实现">3.2.1.ApplicationContext的实现</a></li></ul></li><li><a href="#33applicaitioncontextioc容器的创建过程">3.3.ApplicaitionContext：IoC容器的创建过程</a><ul><li><a href="#331构造方法">3.3.1.构造方法</a><ul><li><a href="#3311获得当前的配置文件位置将classpathxmlapplicationcontext作为resourceloader存入成员变量">3.3.1.1.获得当前的配置文件位置+将ClassPathXmlApplicationContext作为ResourceLoader存入成员变量</a></li></ul></li><li><a href="#332preparerefresh">3.3.2.prepareRefresh()</a><ul><li><a href="#3321生成environment实例并存入成员变量">3.3.2.1.生成Environment实例并存入成员变量</a></li></ul></li><li><a href="#333configurablelistablebeanfactory-beanfactory--obtainfreshbeanfactory">3.3.3.ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();</a><ul><li><a href="#3331创建defaultlistablebeanfactory">3.3.3.1.创建DefaultListableBeanFactory</a></li><li><a href="#3332defaultlistablebeanfactory定制参数">3.3.3.2.DefaultListableBeanFactory定制参数</a></li><li><a href="#3333核心流程-bean加载入defaultlistablebeanfactoryabstractxmlapplicationcontextloadbeandefinitionsbeanfactory">3.3.3.3.核心流程-Bean加载入DefaultListableBeanFactory：AbstractXmlApplicationContext.loadBeanDefinitions(beanFactory);</a><ul><li><a href="#33331为defaultlistablebeanfactory创建xmlbeandefinitionreader">3.3.3.3.1.为DefaultListableBeanFactory创建XmlBeanDefinitionReader</a></li><li><a href="#33332xmlbeandefinitionreaderloadbeandefinitions创建beandefiniton并存储">3.3.3.3.2.XmlBeanDefinitionReader.loadBeanDefinitions()创建BeanDefiniton并存储</a><ul><li><a href="#333321abstractapplicationcontextgetresourceslocation加载xml配置文件">3.3.3.3.2.1.AbstractApplicationContext.getResources(location)加载xml配置文件</a></li><li><a href="#333222xmlbeandefinitionreaderloadbeandefinitionsresources解析文件并生成beandefinitions">3.3.3.2.2.2.XmlBeanDefinitionReader.loadBeanDefinitions(resources)解析文件并生成BeanDefinitions</a></li></ul></li></ul></li></ul></li><li><a href="#334preparebeanfactorybeanfactory">3.3.4.prepareBeanFactory(beanFactory)</a></li><li><a href="#335postprocessbeanfactorybeanfactory">3.3.5.postProcessBeanFactory(beanFactory)</a></li><li><a href="#336invokebeanfactorypostprocessorsbeanfactory">3.3.6.invokeBeanFactoryPostProcessors(beanFactory)</a></li><li><a href="#337registerbeanpostprocessorsbeanfactory">3.3.7.registerBeanPostProcessors(beanFactory)</a></li><li><a href="#338initmessagesource">3.3.8.initMessageSource()</a></li><li><a href="#339initapplicationeventmulticaster">3.3.9.initApplicationEventMulticaster()</a></li><li><a href="#3310onrefresh">3.3.10.onRefresh()</a></li><li><a href="#3311registerlisteners">3.3.11.registerListeners()</a></li><li><a href="#3312finishbeanfactoryinitializationbeanfactory">3.3.12.finishBeanFactoryInitialization(beanFactory)</a></li><li><a href="#3313finishrefresh">3.3.13.finishRefresh()</a></li></ul></li></ul></li></ul><h1 id="1ioc容器是什么">1.IoC容器是什么</h1><p>举个例子：</p><p>例如有一天，一个农民，他需要一把铲子去种田，因此这个农民就需要自己去造一把铲子去种田，但是后面农民感觉自己造铲子太麻烦了，然后他就去找一个能够帮忙造铲子的工厂（IoC容器）帮忙造，农夫就只需要从工厂去取（<code>getBean</code>）然后使用就好了，这里我们知道了农夫从工厂通过<code>getBean</code>拿到铲子，那么铲子又是如何造的呢？</p><p>造铲子：</p><p>工厂内有个员工叫做<code>RES（Resource）</code>负责接收‘取铲子’的需求，需求包括铲子的所需创建零部件（xml参数）和图纸（Class），以及依赖的零部件（依赖的bean）</p><p>另外一个员工<code>BDR（BeanDefinitionReader）</code>负责分析需求，并且把需求整理为工厂流水线生产铲子的规范格式BeanDefinition</p><p>还有员工<code>BF(BeanFactory)</code>拿到BDR员工提供的BeanDefinition开始制造产品</p><p>三个员工同时负责外部交易，通过向客户提供getBean方法让客户来这里拿产品</p><pre><code class="language-java">ClassPathResource res = new ClassPathResource(&quot;spring.xml&quot;);DefaultListableBeanFactory factory = new DefaultListableBeanFactory();XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);reader.loadBeanDefinitions(res);</code></pre><p>但是厂长觉得三个员工配合的流程过于复杂并且没有一个带头的门面大哥去指挥，因此厂长招了个项目经理对三个员工进行管理以及指挥，使得农民拿铲子的流程看起来更简单，并且农民可以直接找项目经理提需求，而不需要直接去找员工，除此之外，项目经理还在之前生产铲子功能之外，扩展了如下的功能：</p><ul><li>从ApplicationEventPublisher那里学会了事件监听机制，可以让工厂里面的产品之间发送接收消息；</li><li>从MessageSource学会了外语交流能力；</li><li>从InitializingBean那里学会了售后服务，在卖出产品后，接收用户的需求调整；</li><li>从BeanPostProcessor那里学会了售前售后</li></ul><h1 id="2spring-ioc设计原理">2.Spring IoC设计原理</h1><p>从宏观角度看：</p><p>其本质就是把程序的类和配置元数据组装起来，然后就可以通过ApplicationContext创建并初始化好之后，通过IoC容易就能直接获得装配好的实例</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-201910131714373650-a.png-itzhai" alt="image-20191013171437365.png-itzhai" /></p><h1 id="3ioc的设计与实现beanfactory和applicationcontext">3.IoC的设计与实现：BeanFactory和ApplicationContext</h1><h2 id="31beanfactory容器设计">3.1.BeanFactory容器设计</h2><blockquote><p>其中的BeanFactory定义了基本的IoC容器的规范</p></blockquote><p>用XmlBeanFactory的实现为例子来简单说明下IoC容器的设计理念，下图是其一个简单的类图:</p><ul><li>BeanFactory实现是IoC容器的基本形式，各种ApplicationContext的实现是IoC容器的高级表现形式。</li><li>DefaultListableBeanFactory作为一个默认的功能完整的IoC容器来使用</li><li>Resource是Spring用来封装I/O操作的类</li></ul><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210825112944311.png" alt="image-20210825112944311" /></p><h2 id="32applicationcontext">3.2.ApplicationContext</h2><p>前面我们说了ApplicaitonContext，其类似于工厂的项目经理，一个生产的需求提过来，ApplicationContext能够很好并且很简单的完成。对于农民而言无需关注生产过程中过多的细节，并且其相较于BeanFactory也扩展了很多功能，可以这样说：</p><ul><li>ApplicationContext是一个高级形态的IoC容器</li><li>支持不同的信息源：继承接口Messagesource；</li><li>访问资源：继承接口ResourceLoader；</li><li>支持应用事件</li><li>在ApplicationContext中提供附加功能；</li></ul><h3 id="321applicationcontext的实现">3.2.1.ApplicationContext的实现</h3><p>ApplicationContext的实现类主要有两个：</p><ol><li>FileSystemXmlApplicationContext</li><li>ClassPathXmlApplicationContext</li></ol><p>继承关系如下：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210824150344843.png" alt="image-20210824150344843" /></p><p>其中ApplicationContext的主要功能在AbstractRefreshableApplicationContext中实现了，接下来我们通ClassPathXmlApplicationContext来讲解下如下几个问题：</p><ol><li>IoC容器的创建过程</li><li>bean的创建及其赋值过程</li><li>bean的初始化过程</li></ol><p>接下来，我们用如下代码段作为入口，去深入探析如上的问题：</p><pre><code class="language-java">public static void main(String[] args) {    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(&quot;config.xml&quot;);    SimpleBean simpleBean = context.getBean(SimpleBean.class);    simpleBean.invoke();}</code></pre><p>其中SimpleBean只是一个很普通的bean，代码如下：</p><pre><code class="language-java">/** * @Auther: zhang_yx * @Date: 2021/3/24 15:23 */public class SimpleBean {    private String desc;    public void invoke(){        System.out.println(&quot;我的描述：&quot;+desc);    }    public String getDesc() {        return desc;    }    public void setDesc(String desc) {        this.desc = desc;    }}</code></pre><p>config.xml配置文件如下：</p><pre><code class="language-xml">&lt;bean class=&quot;com.lingwuee.zhang.springlearn.IoC.SimpleBean&quot;&gt;    &lt;property name=&quot;desc&quot; value=&quot;冲起来&quot;&gt;&lt;/property&gt;&lt;/bean&gt;</code></pre><h2 id="33applicaitioncontextioc容器的创建过程">3.3.ApplicaitionContext：IoC容器的创建过程</h2><h3 id="331构造方法">3.3.1.构造方法</h3><h4 id="3311获得当前的配置文件位置将classpathxmlapplicationcontext作为resourceloader存入成员变量">3.3.1.1.获得当前的配置文件位置+将ClassPathXmlApplicationContext作为ResourceLoader存入成员变量</h4><p>执行上述代码，首先进入的是构造方法：</p><pre><code class="language-java">public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)      throws BeansException {   //执行父类的构造方法，会一直执行到AbstractApplicationContext的构造方法，代码如下   super(parent);   //设置配置文件位置，并存储到成员变量configLocations中，其中会经过一段文本处理的代码   setConfigLocations(configLocations);   //默认为true   if (refresh) {      //Spring bean解析就在此方法，非常重要      refresh();   }}</code></pre><p>AbstractApplicationContext构造方法，其实只是将ClassPathXmlApplicationContext作为ResourceLoader传入了AbstractApplicationContext成员变量<code>resourcePatternResolver</code>中</p><pre><code class="language-java">/** ResourcePatternResolver used by this context */private ResourcePatternResolver resourcePatternResolver;public AbstractApplicationContext(ApplicationContext parent) {   this();   setParent(parent);}public AbstractApplicationContext() {    this.resourcePatternResolver = getResourcePatternResolver();}</code></pre><p><code>AbstractApplicationContext的refresh()</code>源码如下：</p><pre><code class="language-java">@Overridepublic void refresh() throws BeansException, IllegalStateException {   synchronized (this.startupShutdownMonitor) {      // Prepare this context for refreshing.      prepareRefresh();      // Tell the subclass to refresh the internal bean factory.      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();      // Prepare the bean factory for use in this context.      prepareBeanFactory(beanFactory);      try {         // Allows post-processing of the bean factory in context subclasses.         postProcessBeanFactory(beanFactory);         // Invoke factory processors registered as beans in the context.         invokeBeanFactoryPostProcessors(beanFactory);         // Register bean processors that intercept bean creation.         registerBeanPostProcessors(beanFactory);         // Initialize message source for this context.         initMessageSource();         // Initialize event multicaster for this context.         initApplicationEventMulticaster();         // Initialize other special beans in specific context subclasses.         onRefresh();         // Check for listener beans and register them.         registerListeners();         // Instantiate all remaining (non-lazy-init) singletons.         finishBeanFactoryInitialization(beanFactory);         // Last step: publish corresponding event.         finishRefresh();      }      catch (BeansException ex) {         if (logger.isWarnEnabled()) {            logger.warn(&quot;Exception encountered during context initialization - &quot; +                  &quot;cancelling refresh attempt: &quot; + ex);         }         // Destroy already created singletons to avoid dangling resources.         destroyBeans();         // Reset 'active' flag.         cancelRefresh(ex);         // Propagate exception to caller.         throw ex;      }      finally {         // Reset common introspection caches in Spring's core, since we         // might not ever need metadata for singleton beans anymore...         resetCommonCaches();      }   }}</code></pre><h3 id="332preparerefresh">3.3.2.prepareRefresh()</h3><p>准备上下文环境，其中只关注重要的方法：</p><pre><code class="language-java">protected void prepareRefresh() {   //空实现   initPropertySources();//AbstractApplicationContext.getEnvironment(),获得当前的环境信息，其中包括JVM的参数，操作系统的环境变量，spring中配置的config参数   getEnvironment().validateRequiredProperties();   this.earlyApplicationEvents = new LinkedHashSet&lt;ApplicationEvent&gt;();}</code></pre><h5 id="3321生成environment实例并存入成员变量">3.3.2.1.生成Environment实例并存入成员变量</h5><p>getEnvironment方法源自于ConfigurableApplicationContext.createEnvironment(),源码很简单，其中存储的内容是当前系统的环境变量（如果能获取到）、JVM参数、spring的property、profile信息，其中Environment的继承体系如下图：</p><blockquote><p>Spring Profile特性是从3.1开始的，其主要是为了解决这样一种问题: 线上环境和测试环境使用不同的配置或是数据库或是其它。有了Profile便可以在 不同环境之间无缝切换。**Spring容器管理的所有bean都是和一个profile绑定在一起的。**使用了Profile的配置文件示例:</p><pre><code class="language-xml">&lt;beans profile=&quot;develop&quot;&gt;   &lt;context:property-placeholder location=&quot;classpath*:jdbc-develop.properties&quot;/&gt;  &lt;/beans&gt;  &lt;beans profile=&quot;production&quot;&gt;   &lt;context:property-placeholder location=&quot;classpath*:jdbc-production.properties&quot;/&gt;  &lt;/beans&gt;  &lt;beans profile=&quot;test&quot;&gt;   &lt;context:property-placeholder location=&quot;classpath*:jdbc-test.properties&quot;/&gt;  &lt;/beans&gt;</code></pre><p>在启动代码中可以用如下代码设置活跃(当前使用的)Profile:</p><pre><code class="language-java">context.getEnvironment().setActiveProfiles(&quot;dev&quot;);</code></pre></blockquote><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210825100106289.png" alt="image-20210825100106289" /></p><p>其中创建的environment持有一个MutablePropertySources对象，其中就使用了一个CopyOnWriteArrayList存储环境变量和JVM参数。PropertySource接口代表了键值对的Property来源，继承体系如下图：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210825101747391.png" alt="image-20210825101747391" /></p><h3 id="333configurablelistablebeanfactory-beanfactory--obtainfreshbeanfactory">3.3.3.ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();</h3><p>源码如下：</p><p><code>AbstractApplicationContext.class</code></p><pre><code class="language-java">protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {   //初始化beanFactory   refreshBeanFactory();   ConfigurableListableBeanFactory beanFactory = getBeanFactory();   if (logger.isDebugEnabled()) {      logger.debug(&quot;Bean factory for &quot; + getDisplayName() + &quot;: &quot; + beanFactory);   }   return beanFactory;}</code></pre><h4 id="3331创建defaultlistablebeanfactory">3.3.3.1.创建DefaultListableBeanFactory</h4><p>此处由于并没有传一个ConfigurableApplicationContext的子类进来，传了则会将该值赋值给成员变量：<code>AbstractBeanFactory.parentBeanFactory</code></p><pre><code class="language-java">protected final void refreshBeanFactory() throws BeansException {   try { //创建了一个DefaultListableBeanFactory对象      DefaultListableBeanFactory beanFactory = createBeanFactory();       //定制参数      customizeBeanFactory(beanFactory);      loadBeanDefinitions(beanFactory);      synchronized (this.beanFactoryMonitor) {         this.beanFactory = beanFactory;      }   }   catch (IOException ex) {   }}</code></pre><h4 id="3332defaultlistablebeanfactory定制参数">3.3.3.2.DefaultListableBeanFactory定制参数</h4><p>AbstractRefreshableApplicationContext.customizeBeanFactory方法用于给子类提供一个自由配置的机会，默认实现:</p><pre><code class="language-java">protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {    if (this.allowBeanDefinitionOverriding != null) {        //默认false，不允许覆盖        beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);    }    if (this.allowCircularReferences != null) {        //默认false，不允许循环引用        beanFactory.setAllowCircularReferences(this.allowCircularReferences);    }}</code></pre><h4 id="3333核心流程-bean加载入defaultlistablebeanfactoryabstractxmlapplicationcontextloadbeandefinitionsbeanfactory">3.3.3.3.核心流程-Bean加载入DefaultListableBeanFactory：AbstractXmlApplicationContext.loadBeanDefinitions(beanFactory);</h4><p>AbstractXmlApplicationContext.loadBeanDefinitions(beanFactory)，这个就是核心bean的加载了，此处的执行步骤</p><pre><code class="language-java">@Overrideprotected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {    // 1.为给定的beanFactory创建一个XmlBeanDefinitionReader对象   XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);   // 2.设置必要参数   beanDefinitionReader.setEnvironment(this.getEnvironment());   beanDefinitionReader.setResourceLoader(this);   beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));   //创建BeanDefiniton并存储   loadBeanDefinitions(beanDefinitionReader);}</code></pre><h5 id="33331为defaultlistablebeanfactory创建xmlbeandefinitionreader">3.3.3.3.1.为DefaultListableBeanFactory创建XmlBeanDefinitionReader</h5><p>其中BeanDefineReader继承关系如下图，其职责就是根据ResourceLoader读取到的xml解析的信息（图纸），生成bean的规范BeanDefiniton（工厂生产东西的规范格式）：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210825120110850.png" alt="image-20210825120110850" /></p><h5 id="33332xmlbeandefinitionreaderloadbeandefinitions创建beandefiniton并存储">3.3.3.3.2.XmlBeanDefinitionReader.loadBeanDefinitions()创建BeanDefiniton并存储</h5><ol><li>getConfigLocations()：获得配置文件的具体位置</li><li>loadBeanDefinitions(configLocations)：生成BeanDefinition</li></ol><pre><code class="language-java">protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {   Resource[] configResources = getConfigResources();   if (configResources != null) {      reader.loadBeanDefinitions(configResources);   }   String[] configLocations = getConfigLocations();   //重点生成的代码   if (configLocations != null) {      reader.loadBeanDefinitions(configLocations);   }}</code></pre><p>其中通过ResourceLoader获得bean配置并且生成BeanDefinitions的核心代码如下：</p><p>AbstractBeanDefinitionReader.loadBeanDefinitions 206行：</p><pre><code class="language-java">//第二个参数为nullpublic int loadBeanDefinitions(String location, Set&lt;Resource&gt; actualResources) throws BeanDefinitionStoreException {   ResourceLoader resourceLoader = getResourceLoader();   //ApplicationContext都实现了ResourceLoader相关的接口   if (resourceLoader instanceof ResourcePatternResolver) {      try {         Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);         int loadCount = loadBeanDefinitions(resources);         return loadCount;      }      catch (IOException ex) {         throw new BeanDefinitionStoreException(               &quot;Could not resolve bean definition resource pattern [&quot; + location + &quot;]&quot;, ex);      }   }   else {      // Can only load single resources by absolute URL.      Resource resource = resourceLoader.getResource(location);      int loadCount = loadBeanDefinitions(resource);      return loadCount;   }}</code></pre><p>其中有两个核心的调用：</p><pre><code class="language-java">//加载Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);//解析int loadCount = loadBeanDefinitions(resources);</code></pre><h6 id="333321abstractapplicationcontextgetresourceslocation加载xml配置文件">3.3.3.3.2.1.AbstractApplicationContext.getResources(location)加载xml配置文件</h6><p>代码如下：</p><pre><code class="language-java">/** 其值再实例化ApplicaitionContext的时候会赋值好，其类内部持有ResourceLoad也就是ApplicaitionContext的引用 */private ResourcePatternResolver resourcePatternResolver;//读取文件，并返回Resource对象public Resource[] getResources(String locationPattern) throws IOException {   return this.resourcePatternResolver.getResources(locationPattern);}</code></pre><h6 id="333222xmlbeandefinitionreaderloadbeandefinitionsresources解析文件并生成beandefinitions">3.3.3.2.2.2.XmlBeanDefinitionReader.loadBeanDefinitions(resources)解析文件并生成BeanDefinitions</h6><p>其中底层调用的方法是XmlBeanDefinitionReader.loadBeanDefinitions（EncodedResource encodedResource），代码如下，其中关键的源码只有两行：</p><pre><code class="language-java">public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {    InputStream inputStream = encodedResource.getResource().getInputStream();    InputSource inputSource = new InputSource(inputStream);    //源码如下    return doLoadBeanDefinitions(inputSource, encodedResource.getResource());}</code></pre><p>Resource是一种代表资源的接口，其中EncodedResource扮演的是一个装饰器模式，为InputStreamSource添加了字符编码(虽然默认为null)。这样为我们自定义xml配置文件的编码方式提供了机会</p><p>XmlBeanDefinitionReader.doLoadBeanDefinitions：</p><pre><code class="language-java">protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) {    Document doc = doLoadDocument(inputSource, resource);    return registerBeanDefinitions(doc, resource);}</code></pre><p>其中doLoadDocument返回的就是解析之后的结果，spring也是采取了dom的方式解析，即一次全部load到内存当中</p><pre><code class="language-java">public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {   BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();    //解析Resource对象并生成BeanDefinition对象存入map交由ApplicationContext持有   documentReader.registerBeanDefinitions(doc, createReaderContext(resource));}</code></pre><p>XmlBeanDefinitionReader.createBeanDefinitionDocumentReader：</p><p><strong>这里为什么用了转型呢？因为这里使用到了策略模式，spring的使用方可以通过set的方式去修改这个BeanDefinitionDocumentReader策略实现，实现了IoC容器针对BeanDefinitionDocument也就是xml信息读取之后的操作的可扩展，调用方只需要初始化XmlBeanDefinitionReader时set入自己的BeanDefinitionDocument类就好了</strong></p><pre><code class="language-java">//默认值是DefaultBeanDefinitionDocumentReader类private Class&lt;?&gt; documentReaderClass = DefaultBeanDefinitionDocumentReader.class;public void setDocumentReaderClass(Class&lt;?&gt; documentReaderClass) {    this.documentReaderClass = documentReaderClass;}protected BeanDefinitionDocumentReader createBeanDefinitionDocumentReader() {   return BeanDefinitionDocumentReader.class.cast(BeanUtils.instantiateClass(this.documentReaderClass));}</code></pre><p>DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(Document doc, XmlReaderContext readerContext) ：</p><pre><code class="language-java">@Overridepublic void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {   this.readerContext = readerContext;   Element root = doc.getDocumentElement();   doRegisterBeanDefinitions(root);}</code></pre><p>DefaultBeanDefinitionDocumentReader.doRegisterBeanDefinitions(Element root)：</p><pre><code class="language-java">protected void doRegisterBeanDefinitions(Element root) {  BeanDefinitionParserDelegate parent = this.delegate;    this.delegate = createDelegate(getReaderContext(), root, parent);    //默认的命名空间即    //http://www.springframework.org/schema/beans    if (this.delegate.isDefaultNamespace(root)) {        //检查profile属性        String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);        if (StringUtils.hasText(profileSpec)) {            //profile属性可以以,分割            String[] specifiedProfiles = StringUtils.tokenizeToStringArray(                    profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);            if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {                return;            }        }    }   preProcessXml(root);   parseBeanDefinitions(root, this.delegate);   postProcessXml(root);   this.delegate = parent;}</code></pre><p>delegate的作用在于处理beans标签的嵌套，其实Spring配置文件是可以写成这样的：</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;    &lt;beans&gt;        &lt;bean class=&quot;base.SimpleBean&quot;&gt;&lt;/bean&gt;    &lt;beans&gt;        &lt;bean class=&quot;java.lang.Object&quot;&gt;&lt;/bean&gt;    &lt;/beans&gt;&lt;/beans&gt;</code></pre><p>preProcessXml方法是个空实现，供子类去覆盖，<strong>目的在于给子类一个把我们自定义的标签转为Spring标准标签的机会</strong>, 想的真周到。</p><p>DefaultBeanDefinitionDocumentReader.parseBeanDefinitions：</p><pre><code class="language-java">protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {    if (delegate.isDefaultNamespace(root)) {        NodeList nl = root.getChildNodes();        for (int i = 0; i &lt; nl.getLength(); i++) {            Node node = nl.item(i);            if (node instanceof Element) {                Element ele = (Element) node;                if (delegate.isDefaultNamespace(ele)) {                    parseDefaultElement(ele, delegate);                } else {                    delegate.parseCustomElement(ele);                }            }        }    } else {        delegate.parseCustomElement(root);    }}</code></pre><p>对于非默认命名空间的元素交由delegate处理。</p><p>默认命名空间：import、alias、bean、beans</p><pre><code class="language-java">private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {   if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {      importBeanDefinitionResource(ele);   }   else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {      processAliasRegistration(ele);   }   else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {      processBeanDefinition(ele, delegate);   }   else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {      // recurse      doRegisterBeanDefinitions(ele);   }}</code></pre><ol><li><p>import标签：</p><p>写法示例:</p><pre><code class="language-xml">&lt;import resource=&quot;CTIContext.xml&quot; /&gt;&lt;import resource=&quot;customerContext.xml&quot; /&gt;</code></pre><p>importBeanDefinitionResource(ele)：着重看下下述源码，其实核心还是只想的BeanDefinitionReader.public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException 方法：</p><pre><code class="language-java">private XmlReaderContext readerContext;//其中持有BeanDefinitionReader对象的实例protected void importBeanDefinitionResource(Element ele) {   String location = ele.getAttribute(RESOURCE_ATTRIBUTE);   int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources);}protected final XmlReaderContext getReaderContext() {return this.readerContext;}</code></pre></li><li><p>alias</p><p>加入有一个bean名为componentA-dataSource，但是另一个组件想以componentB-dataSource的名字使用，就可以这样定义:</p><pre><code class="language-xml">&lt;alias name=&quot;componentA-dataSource&quot; alias=&quot;componentB-dataSource&quot;/&gt;</code></pre><p>processAliasRegistration核心源码:</p><pre><code class="language-java">protected void processAliasRegistration(Element ele) {    String name = ele.getAttribute(NAME_ATTRIBUTE);    String alias = ele.getAttribute(ALIAS_ATTRIBUTE);    getReaderContext().getRegistry().registerAlias(name, alias);    getReaderContext().fireAliasRegistered(name, alias, extractSource(ele));}</code></pre><p>从前面的源码可以发现，registry其实就是DefaultListableBeanFactory，它实现了BeanDefinitionRegistry接口。registerAlias方法的实现在SimpleAliasRegistry:</p><pre><code class="language-java">@Overridepublic void registerAlias(String name, String alias) {    Assert.hasText(name, &quot;'name' must not be empty&quot;);    Assert.hasText(alias, &quot;'alias' must not be empty&quot;);    //名字和别名一样    if (alias.equals(name)) {        //ConcurrentHashMap        this.aliasMap.remove(alias);    } else {        String registeredName = this.aliasMap.get(alias);        if (registeredName != null) {            if (registeredName.equals(name)) {                // An existing alias - no need to re-register                return;            }            if (!allowAliasOverriding()) {                throw new IllegalStateException                    (&quot;Cannot register alias '&quot; + alias + &quot;' for name '&quot; +                    name + &quot;': It is already registered for name '&quot; + registeredName + &quot;'.&quot;);            }        }        checkForAliasCircle(name, alias);        this.aliasMap.put(alias, name);    }}</code></pre><p>所以别名关系的保存使用Map完成，key为别名，value为本来的名字。</p></li><li><p>bean</p><p>bean节点是Spring最最常见的节点了。</p><p>DefaultBeanDefinitionDocumentReader.processBeanDefinition:</p><pre><code class="language-java">protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {//生成BeanDefinition    BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);    if (bdHolder != null) {        bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);        try {            // Register the final decorated instance.            BeanDefinitionReaderUtils.registerBeanDefinition                (bdHolder, getReaderContext().getRegistry());        }        catch (BeanDefinitionStoreException ex) {            getReaderContext().error(&quot;Failed to register bean definition with name '&quot; +                    bdHolder.getBeanName() + &quot;'&quot;, ele, ex);        }        // Send registration event.        getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));    }}</code></pre><p>id &amp; name处理</p><p>delegate.parseBeanDefinitionElement(ele)：</p><p>代码比较长，大概逻辑如下：</p><ol><li><p>首先获取到id和name属性，<strong>name属性支持配置多个，以逗号分隔，如果没有指定id，那么将以第一个name属性值代替。id必须是唯一的，name属性其实是alias的角色，可以和其它的bean重复，如果name也没有配置，那么其实什么也没做</strong>。</p></li><li><p>beanName生成：如果name和id属性都没有指定，那么Spring会自己生成一个, BeanDefinitionParserDelegate.parseBeanDefinitionElement:</p><pre><code class="language-java">beanName = this.readerContext.generateBeanName(beanDefinition);String beanClassName = beanDefinition.getBeanClassName();aliases.add(beanClassName);</code></pre><p>可见，Spring同时会把类名作为其别名。最终调用的是BeanDefinitionReaderUtils.generateBeanName:</p></li><li><p>bean解析：</p><p><strong>首先获取bean的class属性和parent属性</strong>：配置了parent属性的话，当前bean会继承父parent的属性，之后根据class和parent创建BeanDefinition对象。</p><pre><code class="language-java">AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);public AbstractBeanDefinition parseBeanDefinitionElement(Element ele, String beanName, BeanDefinition containingBean) {this.parseState.push(new BeanEntry(beanName));String className = null;if (ele.hasAttribute(CLASS_ATTRIBUTE)) {className = ele.getAttribute(CLASS_ATTRIBUTE).trim();}    String parent = null;    if (ele.hasAttribute(PARENT_ATTRIBUTE)) {            parent = ele.getAttribute(PARENT_ATTRIBUTE);        }    //1.生成bean    AbstractBeanDefinition bd = createBeanDefinition(className, parent);    //2.解析bean标签的各个属性，setter存入BeanDefinition    parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);    //3.descrition属性的信息    bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));//4.meta等属性的信息    parseMetaElements(ele, bd);    parseLookupOverrideSubElements(ele, bd.getMethodOverrides());    parseReplacedMethodSubElements(ele, bd.getMethodOverrides());    //5.标签内的构造标签处理&lt;constructor-arg&gt;    parseConstructorArgElements(ele, bd);    //6.标签内的&lt;property&gt;处理        parsePropertyElements(ele, bd);        parseQualifierElements(ele, bd);        bd.setResource(this.readerContext.getResource());        bd.setSource(extractSource(ele));        return bd;}</code></pre><p>BeanDefinition的创建在BeanDefinitionReaderUtils.createBeanDefinition:</p><p>其中返回的GenericBeanDefinition主要包括如下信息：当前bean标签class熟悉的Class信息、ClassName信息、parentName信息</p><pre><code class="language-java">public static AbstractBeanDefinition createBeanDefinition(        String parentName, String className, ClassLoader classLoader) {    GenericBeanDefinition bd = new GenericBeanDefinition();    bd.setParentName(parentName);    if (className != null) {        if (classLoader != null) {            bd.setBeanClass(ClassUtils.forName(className, classLoader));        }        else {            bd.setBeanClassName(className);        }    }    return bd;}</code></pre><p><strong>具体的attribute属性值信息调用如下发方法设置</strong>：</p><p>其实就是读取其配置，调用相应的setter方法保存在BeanDefinition中:</p><pre><code class="language-java">parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);</code></pre><p><strong>之后解析bean的decription子元素</strong>:就仅仅是个描述。</p><pre><code class="language-xml">&lt;bean id=&quot;b&quot; name=&quot;one, two&quot; class=&quot;base.SimpleBean&quot;&gt;    &lt;description&gt;SimpleBean&lt;/description&gt;&lt;/bean&gt;</code></pre><p><strong>然后是meta子元素的解析</strong>：</p><p>meta元素在xml配置文件里是这样的:</p><pre><code class="language-xml">&lt;bean id=&quot;b&quot; name=&quot;one, two&quot; class=&quot;base.SimpleBean&quot;&gt;    &lt;meta key=&quot;name&quot; value=&quot;skywalker&quot;/&gt;&lt;/bean&gt;</code></pre><p>注释上说，这样可以将任意的元数据附到对应的bean definition上。解析过程源码</p><p><strong>构造参数的解析</strong>：</p><p>作用一目了然，使用示例:</p><pre><code class="language-xml">&lt;bean class=&quot;base.SimpleBean&quot;&gt;    &lt;constructor-arg&gt;        &lt;value type=&quot;java.lang.String&quot;&gt;Cat&lt;/value&gt;    &lt;/constructor-arg&gt;&lt;/bean&gt;&lt;bean class=&quot;com.lingwuee.zhang.springlearn.IoC.SimpleBean&quot;&gt;        &lt;constructor-arg index=&quot;0&quot; type=&quot;java.lang.String&quot; value=&quot;11&quot;/&gt;&lt;/bean&gt;</code></pre><p>type一般不需要指定，除了泛型集合那种。除此之外，constructor-arg还支持name, index, ref等属性，可以具体的指定参数的位置等。构造参数解析后保存在BeanDefinition内部一个ConstructorArgumentValues对象中。如果设置了index属性，那么以Map&lt;Integer, ValueHolder&gt;的形式保存，反之，以List<ValueHolder>的形式保存。</p><p><strong>property解析</strong>:</p><p>非常常用的标签，用以为bean的属性赋值，支持value和ref两种形式，示例:</p><pre><code class="language-xml">&lt;bean class=&quot;base.SimpleBean&quot;&gt;    &lt;property name=&quot;name&quot; value=&quot;skywalker&quot; /&gt;&lt;/bean&gt;</code></pre><p>value和ref属性不能同时出现，如果是ref，那么将其值保存在不可变的RuntimeBeanReference对象中，其实现了BeanReference接口，此接口只有一个getBeanName方法。如果是value，那么将其值保存在TypedStringValue对象中。最终将对象保存在BeanDefinition内部一个MutablePropertyValues对象中(内部以ArrayList实现)。</p><p>​</p></li><li><p>Bean注册：</p><p>BeanDefinitionReaderUtils.registerBeanDefinition:</p><pre><code class="language-java">public static void registerBeanDefinition(    BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) {    // Register bean definition under primary name.    String beanName = definitionHolder.getBeanName();    registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());    // Register aliases for bean name, if any.    String[] aliases = definitionHolder.getAliases();    if (aliases != null) {        for (String alias : aliases) {            registry.registerAlias(beanName, alias);        }    }}</code></pre><p>registry其实就是<strong>DefaultListableBeanFactory</strong>对象，registerBeanDefinition方法主要就干了这么两件事:</p><pre><code class="language-java">@Overridepublic void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) {//beanName如果没有设置，那么默认为类的全限定名    this.beanDefinitionMap.put(beanName, beanDefinition);    this.beanDefinitionNames.add(beanName);}</code></pre><p>一个是Map，另一个是List，一目了然。registerAlias方法的实现在其父类SimpleAliasRegistry，就是把键值对放在了一个ConcurrentHashMap里。</p></li></ol><blockquote><p>BeanDefiniton数据结构：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210825170131706.png" alt="image-20210825170131388" /></p><p>BeanDefinition在BeanFactory中的主要数据结构如下图:</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210825170242116.png" alt="image-20210825170242116" /></p></blockquote></li></ol><h3 id="334preparebeanfactorybeanfactory">3.3.4.prepareBeanFactory(beanFactory)</h3><p>该方法复制对BeanFactory进行一些特征方面的设置</p><p>源码如下：</p><pre><code class="language-java">protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {   // Tell the internal bean factory to use the context's class loader etc.   beanFactory.setBeanClassLoader(getClassLoader());   beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));   beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));   // Configure the bean factory with context callbacks.   beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));   beanFactory.ignoreDependencyInterface(EnvironmentAware.class);   beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);   beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);   beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);   beanFactory.ignoreDependencyInterface(MessageSourceAware.class);   beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);   // BeanFactory interface not registered as resolvable type in a plain factory.   // MessageSource registered (and found for autowiring) as a bean.   beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);   beanFactory.registerResolvableDependency(ResourceLoader.class, this);   beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);   beanFactory.registerResolvableDependency(ApplicationContext.class, this);   // Register early post-processor for detecting inner beans as ApplicationListeners.   beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));   // Detect a LoadTimeWeaver and prepare for weaving, if found.   if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {      beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));      // Set a temporary ClassLoader for type matching.      beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));   }   // Register default environment beans.   if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {      beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());   }   if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {      beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());   }   if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {      beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());   }}</code></pre><h3 id="335postprocessbeanfactorybeanfactory">3.3.5.postProcessBeanFactory(beanFactory)</h3><p>空实现，运行子类在所有的bean尚未初始化之前注册BeanPostProcessor，空实现且没有子类覆盖</p><h3 id="336invokebeanfactorypostprocessorsbeanfactory">3.3.6.invokeBeanFactoryPostProcessors(beanFactory)</h3><h3 id="337registerbeanpostprocessorsbeanfactory">3.3.7.registerBeanPostProcessors(beanFactory)</h3><p>本质就是从BeanDefitions中获得BeanFactoryPostProcessor，之后调用BeanFactory.addBeanPostProcessor方法保存在一个List中，注意添加时仍然有优先级的概念，优先级高的在前面。</p><h3 id="338initmessagesource">3.3.8.initMessageSource()</h3><p>该方法用于提供spring国际化，该方法就是在BeanFactory中查找MessageSource的bean并通过getBean方法实例化，并保存在ApplicaitonContext内部的messageSource成员变量中，用以处理ApplicationContext的getMessage调用，从继承体系上来看，ApplicationContext是MessageSource的子类，此处是<strong>委托模式</strong>的体现。如果没有配置此bean，那么初始化一个DelegatingMessageSource对象，此类是一个空实现，同样用以处理getMessage调用请求</p><blockquote><p>委托模式就是指A类中关联了B类，业务通过调用A类的接口实现功能，但是A类的方法实际调用的是B类，其实就是门面模式</p></blockquote><h3 id="339initapplicationeventmulticaster">3.3.9.initApplicationEventMulticaster()</h3><p>spring的驱动（监听模式）</p><p>initApplicationEventMulticaster则首先在BeanFactory中寻找ApplicationEventMulticaster的bean，如果找到，那么调用getBean方法将其初始化，如果找不到那么使用SimpleApplicationEventMulticaster。</p><p>监听者模式：</p><h3 id="3310onrefresh">3.3.10.onRefresh()</h3><p>模板方法，交由子类实现，允许子类在bead初始化之前进行一些定制操作</p><h3 id="3311registerlisteners">3.3.11.registerListeners()</h3><p>注册监听时间以及发布时间，从BeanFactory获得ApplicationListener的bean并注册</p><h3 id="3312finishbeanfactoryinitializationbeanfactory">3.3.12.finishBeanFactoryInitialization(beanFactory)</h3><p>完成scope为singletonBean并且非lazy-init的bean的初始化（通过调用getBean，单例的bean会有一个单独的ConcurrentHashMap存储），spring的Bean默认为单例</p><h3 id="3313finishrefresh">3.3.13.finishRefresh()</h3><p>完成容器的刷新，调用LifecycleProcessor的onrefresh方法，发布ContextRefreshedEvent事件</p><p>LifecycleProcessor：负责管理ApplicationContext的生命周期</p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[并发编程总结（二）：JUC下的锁]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/bing-fa-bian-cheng-zong-jie--er---j-u-c-xia-de-suo" />
                <id>tag:https://0522-isniceday.top,2021-08-30:bing-fa-bian-cheng-zong-jie--er---j-u-c-xia-de-suo</id>
                <published>2021-08-30T23:07:42+08:00</published>
                <updated>2021-08-30T23:07:42+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<ul><li><a href="#1juc的包结构">1.JUC的包结构</a></li><li><a href="#2abstractqueuesynchronizer">2.AbstractQueueSynchronizer</a><ul><li><a href="#21aqs原理">2.1.AQS原理</a></li><li><a href="#22aqs的数据结构">2.2.AQS的数据结构</a></li><li><a href="#23locksupportparkobject-blocker和locksupportunparkthread-thread">2.3.LockSupport.park(Object blocker)和LockSupport.unpark(Thread thread)</a></li><li><a href="#23aqs中的一般处理流程">2.3.AQS中的一般处理流程</a><ul><li><a href="#231public-final-void-acquireint-arg">2.3.1.public final void acquire(int arg)</a></li><li><a href="#232public-final-boolean-releaseint-arg">2.3.2.public final boolean release(int arg)</a></li></ul></li></ul></li><li><a href="#3使用aqs实现的锁">3.使用AQS实现的锁</a></li></ul><h1 id="1juc的包结构">1.JUC的包结构</h1><p>我们查看下<code>java.util.concurrent.locks</code>包下面，发现主要包含如下类：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200301111726969-a.png-itzhai" alt="image-20200301111726969" /></p><p>构建UML图如下：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200301113556313-a.png-itzhai" alt="image-20200301113556313" /></p><p>JUC包下主要有三把锁：</p><ol><li>ReentrantLock</li><li>StampedLock，其相较于ReentrantReadWriteLock，多了个乐观读，其实就是用于校验读取的数据是否被修改了</li><li>ReentrantReadWriteLock</li></ol><p>我们可以看到上述三个锁除了StampedLock，顶层都是AbstractQueueSynchronizer类，因此我们来介绍下该类</p><h1 id="2abstractqueuesynchronizer">2.AbstractQueueSynchronizer</h1><p>AbstractQueuedSynchronizer<code>，简写为</code>AQS，抽象队列同步器，许多同步器都可以通过AQS很容易并且高效的构造出来，以下都是通过ASQ构造出来的：<code>ReentrantLock</code>，<code>Semaphore</code>，<code>CountDownLatch</code>，<code>ReentrantReadWriteLock</code>，<code>SynchronousQueue</code>，<code>FutureTask</code></p><h2 id="21aqs原理">2.1.AQS原理</h2><p>AQS的作用：通过队列来辅助实现线程同步。线程并发争夺state资源，争夺失败的则进入等待队列（同步队列）并进入阻塞状态，在state资源被释放之后，从队列头唤醒被阻塞的线程节点，进行state资源的竞争</p><p>AQS的抽象：AQS将最难写的频繁出队入队操作、线程的阻塞唤醒操作已模板的方式写好了，实现类只需要将模板的相关方法进行实现，即可实现不一样的锁或者同步器</p><p>AQS使用了模板方法，把同步队列都封装起来了，同时提供了以下五个未实现的方法，用于子类的重写：</p><table><thead><tr><th>方法签名</th><th>方法描述</th></tr></thead><tbody><tr><td>boolean tryAcquire(int arg)</td><td>尝试以独占模式进行获取。 此方法应查询对象的状态是否允许以独占模式获取对象，如果允许则获取它。如果获取失败，则将当前线程加入到等待队列，直到其他线程唤醒。</td></tr><tr><td>boolean tryRelease(int arg)</td><td>尝试以独占模式释放锁。</td></tr><tr><td>int tryAcquireShared(int arg)</td><td>尝试以共享模式获取锁，此方法应查询对象的状态是否允许以共享模式获取对象，如果允许则获取它。如果获取失败，则将当期线程加入到等待队列，直到其他线程唤醒。</td></tr><tr><td>boolean tryReleaseShared(int arg)</td><td>尝试以共享模式释放锁。</td></tr><tr><td>boolean isHeldExclusively()</td><td>是否独占模式</td></tr></tbody></table><h2 id="22aqs的数据结构">2.2.AQS的数据结构</h2><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200307215156707-a.png-itzhai" alt="image-20200307215156707" /></p><p>AQS中包含的变量</p><ul><li>state：所有线程通过CAS尝试给state设值，当state&gt;0时表示线程被占用，同一个线程多次获取state，会叠加state的值，从而实现了可重入</li><li>exclusiveOnwerThread：在独占模式下这个属性会用到，当线程尝试以独占模式成功给state设值之后，该变量会存储独占的线程。</li><li>等待队列（同步队列）：等待队列中存放了所有针对state失败的线程，是一个双向链表结构。state被某一个线程占用之后，其余线程会进入等待队列等待，等待state被释放（state=0），释放state的线程会唤醒等待队列中的线程继续尝试cas设值state</li><li>head：指向等待队列的头节点，延迟初始化，除了初始化之外，只能通过setHead方法进行修改</li><li>tail：指向等待队列的队尾，延迟初始化，只能通过enq方法修改tail，该方法主要是往队列后面添加等待节点</li></ul><p><strong>AQS队列节点Node类的数据结构</strong>：</p><ul><li>pre：指向队列的上一个节点</li><li>waitStatus：节点的等待状态，初始化为0，表示正常同步等待<ul><li><code>CANCELLED</code>：1 节点因超时或者被中断而取消时设置为取消状态</li><li><code>SIGNAL</code>：-1 指示当前节点被释放后，需要调用unpark通知后面节点，如果后面节点发生竞争导致获取锁失败，也会将当前节点设置为SIGNAL</li><li><code>CONDITION</code>：-2 指示该线程正在进行条件等待，条件队列中会用到</li><li><code>PROPAGATE</code>：-3 共享模式下释放节点时设置的状态，表示无限传播下去</li></ul></li><li>thread：当前节点操作的线程</li><li>nextWaiter：该字段在Condition条件等待中会用到，指向条件队列的下一个节点。或者链接到SHARED常量，表示节点正在以共享模式等待</li><li>next：指向队列的下一个节点</li></ul><h2 id="23locksupportparkobject-blocker和locksupportunparkthread-thread">2.3.LockSupport.park(Object blocker)和LockSupport.unpark(Thread thread)</h2><p>如果想要了解AQS的实现，您需要先知道以下这些内容，因为源码中会大量使用：</p><p>AQS中线程的阻塞和唤醒基本上都是使用这两个方法实现的，其底层都是依赖的Unsafe实现的</p><p>LockSupport是用来创建锁和其他同步类的基本<code>线程阻塞</code>的原语：</p><p>此类与每一个线程都关联一个许可（permit：0 表示无许可，1 表示有许可），如果有许可，将立即返回对park（）的调用，如果没有则阻塞当前调用线程，调用unpark（线程1）可使许可有效，此时被阻塞的线程得以执行，unpark可以先于park执行也没关系，此时unpark的入参线程调用park方法时，会直接执行</p><p>该类中常见的两个方法两个方法：</p><ul><li><code>park(Object blocker)</code>：实现线程的阻塞。除非有许可，否则出于线程调度目的将阻塞线程；如果有许可，则将许可消耗，然后线程往下继续执行；</li><li><code>unpark(Thread thread)</code>：实现解除线程的阻塞。如果线程在park方法上被阻塞，则调用该方法将取消阻塞。否则，许可变为1，保证下一次调用park方法不会阻塞。</li></ul><p>这两个方法底层是调用了Unsafe中的park和unpark的native方法。</p><h2 id="23aqs中的一般处理流程">2.3.AQS中的一般处理流程</h2><p>为了弄清楚AQS中是如何进行队列同步的，我们先从一个简单的<strong>独占</strong>加锁方法说起</p><h3 id="231public-final-void-acquireint-arg">2.3.1.public final void acquire(int arg)</h3><p>以独占模式获取锁，忽略中断。 通过至少调用一次tryAcquire ，成功返回。 否则线程会排队，可能会反复阻塞和解除阻塞，调用tryAcquire直到成功。 此方法可用于实现方法Lock.lock 。</p><p>我们先看一下这个方法的入口代码：</p><pre><code class="language-java">public final void acquire(int arg) {  if (!tryAcquire(arg) &amp;&amp;  // 尝试获取锁，这里是一个在AQS中未实现的方法，具体由子类实现      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  // 获取不到锁，则 1.addWaiter添加到等待队列 2.acquireQueued不断循环等待重试    selfInterrupt();//如果没有获取到锁并且进入了等待队列，则end}</code></pre><p><strong>tryAcquire(arg)</strong>：</p><ol><li>线程1、线程2通过方法<code>tryAcquire()</code>尝试获取锁同时去获取锁</li><li>获取成功的线程会返回true，一般都是通过CAS对state进行设置值尝试获取锁</li></ol><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200311103257880-a.png-itzhai" alt="image-20200311103257880" /></p><blockquote><p>不同的锁有不同的tryAcquire的实现，所以你可以看到ReentrantLock锁里面会有非公平锁和公平锁的实现方式</p><p>ReentrantLock公平锁的实现代码是在获取锁之前通过!hasQueuedPredecessors()，判断当前线程处于等待队列的节点前面没有别的节点了，此时才会去获取锁</p></blockquote><p><strong>addWaiter(Node.EXCLUSIVE)</strong>：</p><p>获取锁失败之后，则在等待队列之后追加一个节点，通过CAS进行追加，追加失败会循环重试，如果追加的时候发现head节点还不存在，可以先初始化一个head节点，然后追加上去</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200311103646009-a.png-itzhai" alt="image-20200311103646009" /></p><p>源码如下：</p><pre><code class="language-java">/** * 为当前线程和给定模式创建和排队节点 */private Node addWaiter(Node mode) {    //初始化一个新节点    Node node = new Node(Thread.currentThread(), mode);    // Try the fast path of enq; backup to full enq on failure    //拿到尾节点    Node pred = tail;    if (pred != null) {        //将新的节点指向尾节点        node.prev = pred;        // 尝试用新节点取代原来的尾节点        if (compareAndSetTail(pred, node)) {            pred.next = node;            return node;        }    }   // 如果当前尾指针为空，则调用enq方法去初始化tail及head    enq(node);    return node;}</code></pre><p><strong>acquireQueued(final Node node, int arg)</strong>：</p><ol><li>线程节点进入等待队列后，执行该方法，不断循环判断当前节点是否在head节点的后继节点，如果是则去尝试<code>tryAcquire()</code>获取锁，如果获取成功，则将当前节点作为head节点，并将next设置为null，head节点只是起到标识作用，<strong>每次处理的都是head的下一个节点</strong></li><li>如果当前节点（线程A）竞争锁失败或不是头结点的下一个节点，则会将前面的节点状态设置为SIGNAL（代表该节点执行完成后需要调用LockSupport的unpark（线程A）方法），<strong>如果前面的节点状态&gt;0，表示这个节点被取消，移除队列</strong>，然后通过<code>parkAndCheckInterrupt()</code>调用LockSupport.park(this)挂起线程</li><li>上述步骤1、2会for循环一直执行，类似于wait的范式，当线程从park处唤醒后，会再去执行上述步骤1、2</li></ol><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200311103510923-a.png-itzhai" alt="image-20200311103510923" /></p><pre><code class="language-java">final boolean acquireQueued(final Node node, int arg) {  boolean failed = true;  try {    boolean interrupted = false;    for (;;) {      // 获取该节点的上一个节点，判断是否头节点，如果是则尝试获取锁      final Node p = node.predecessor();      if (p == head &amp;&amp; tryAcquire(arg)) {        // 获取锁成功，把当前节点变为头节点        setHead(node);        p.next = null; // help GC        failed = false;        return interrupted;      }      // 判断是否需要阻塞线程，该方法中会把取消状态的节点移除掉，并且把当前节点的前一个节点设置为SIGNAL      if (shouldParkAfterFailedAcquire(p, node) &amp;&amp;          parkAndCheckInterrupt())        interrupted = true;    }  } finally {    if (failed)      cancelAcquire(node);  }}</code></pre><blockquote><p>大家看AQS的源码的时候，可以发现这里的线程阻塞与唤醒基本上是用一个循环+LockSupport.park+LockSupport.unpark实现的</p></blockquote><h3 id="232public-final-boolean-releaseint-arg">2.3.2.public final boolean release(int arg)</h3><p>源码如下：</p><pre><code class="language-java">public final boolean release(int arg) {  if (tryRelease(arg)) { // 尝试释放锁    Node h = head;    if (h != null &amp;&amp; h.waitStatus != 0)  // 如果头节点waitStatus不为0，则唤醒后续线程节点继续处理      unparkSuccessor(h);    return true;  }  return false;}</code></pre><p>tryRelease()具体实现由其子类实现，一般是让state值减一</p><ol><li><p>释放锁成功，并且头结点waitStatus != 0那么会调用<code>unparkSuccessor()</code>通知唤醒后续的线程节点进行处理</p><p><strong>注意：在遍历队列查找唤醒下一个节点的过程中，如果发现下一个节点状态是<code>CANCELLED</code>那么就会忽略这个节点，然后从队列尾部向前遍历，找到与头结点最近的没有被取消的节点进行唤醒操作</strong></p></li><li><p>线程A被唤醒之后，其又会从<code>acquireQueued()</code>方法被阻塞出继续执行，其流程和上述<code>acquireQueued()</code>方法一致</p></li></ol><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200311103813051-a.png-itzhai" alt="image-20200311103813051" /></p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20200311104443722-a.png-itzhai" alt="image-20200311104443722" /></p><h1 id="3使用aqs实现的锁">3.使用AQS实现的锁</h1><p>例如<strong>ReentrantLock</strong></p><ul><li><code>lock()</code>: 调用该方法会使锁计数器加1，如果共享资源最初是空闲的，则将锁定并授予线程；</li><li><code>unlock()</code>: 调用该方法使锁计数器减1，当计数达到0的时候，将释放资源；</li><li><code>tryLock()</code>: 如果资源没有被任何其他线程占用，那么该方法返回true，并且锁计数器加1。如果资源不是空闲的，则该方法返回false。这个时候线程不会阻塞，而是直接退出返回结果；</li><li><code>lockInterruptible()</code>: 该方法使得资源空闲时允许该线程在获取资源时被其他线程中断。也就是说：如果当前线程正在等待锁，但其他线程请求该锁，则当前线程将被中断并立即返回，不会继续等待获取锁；</li><li><code>getHoldCount()</code>: 获取资源上持有的锁的计数器；</li><li><code>isHeldByCurrentThread</code>: 如果资源锁有当前线程持有，则此方法返回true。</li></ul><p>参考链接：</p><p><a href="https://www.itzhai.com/articles/aqs-and-lock-implementation-in-concurrent-packages.html#fn1">https://www.itzhai.com/articles/aqs-and-lock-implementation-in-concurrent-packages.html#fn1</a></p><p><a href="https://baijiahao.baidu.com/s?id=1666548481761194849&amp;wfr=spider&amp;for=pc">https://baijiahao.baidu.com/s?id=1666548481761194849&amp;wfr=spider&amp;for=pc</a></p>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[InfluxDB（七）：数据写入.md]]></title>
                <link rel="alternate" type="text/html" href="https://0522-isniceday.top/archives/i-n-f-l-u-x-d-b--qi---shu-ju-xie-ru--m-d" />
                <id>tag:https://0522-isniceday.top,2021-08-12:i-n-f-l-u-x-d-b--qi---shu-ju-xie-ru--m-d</id>
                <published>2021-08-12T21:23:40+08:00</published>
                <updated>2021-08-12T21:23:40+08:00</updated>
                <author>
                    <name>张豫湘</name>
                    <uri>https://0522-isniceday.top</uri>
                </author>
                <content type="html">
                        <![CDATA[<ul><li><a href="#1写入总体框架">1.写入总体框架</a></li><li><a href="#2批量时序数据shard路由">2.批量时序数据Shard路由</a></li><li><a href="#3倒排索引引擎构建倒排索引">3.倒排索引引擎构建倒排索引</a><ul><li><a href="#31wal追加写入">3.1.WAL追加写入</a></li><li><a href="#32cache的写入-inverted-index在内存中构建">3.2.Cache的写入（ Inverted Index在内存中构建）</a></li><li><a href="#33flushinverted-index-cache-flush流程">3.3.flush（Inverted Index Cache Flush流程）</a></li></ul></li><li><a href="#4时序数据写入流程">4.时序数据写入流程</a><ul><li><a href="#41wal追加写入">4.1.WAL追加写入</a></li><li><a href="#42写入cache时序数据写入内存结构">4.2.写入Cache（时序数据写入内存结构）</a></li><li><a href="#43flush流程data-cache-flush流程">4.3.Flush流程（Data Cache Flush流程）</a></li></ul></li><li><a href="#5influxdb数据删除dropmeasurementdroptagkey">5.InfluxDB数据删除（DropMeasurement，DropTagKey）</a><ul><li><a href="#51tsm-引擎的删除操作">5.1.TSM 引擎的删除操作</a></li><li><a href="#52倒排索引引擎的删除操作">5.2.倒排索引引擎的删除操作</a></li></ul></li></ul><h1 id="1写入总体框架">1.写入总体框架</h1><p>转载链接:http://hbasefly.com/2018/03/27/timeseries-database-6/</p><p>数据写入的方式：</p><ol><li>collector采集上传</li><li>opentsdb作为输入</li><li>http或udp协议批量写入数据</li></ol><p>批量数据进入InfluxDB经过三个步骤处理，总体架构图如下：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210531212235136.png" alt="image-20210531212235136" /></p><ol><li>分组到Shard：批量数据首先会分组到不同的Shard（先是根据RP，再根据Shard Group再根据SeriesKey哈希到Sjard）</li><li>倒排索引引擎构建倒排索引：InfluxDB中Shard由两个LSM引擎组成--倒排索引引擎和TSM引擎，时序数据会首先构建倒排索引</li><li>TSM引擎持久化数据：TMS处理数据流程基本和LSM一样，先将请求写入WAL日志文件，再写入Cache，一旦达到阈值就将Cache中的数据flush落盘形成TSM文件</li></ol><h1 id="2批量时序数据shard路由">2.批量时序数据Shard路由</h1><p>批量数据写入InfluxDB之后做的第一件事情是分组**，将时序数据点按照所属shard划分为多组（称为Shard Map）**，每组时序数据点将会发送给对应的shard引擎并发处理</p><p>入库流程还是之前的shard策略（range+Hash），即按时间分片，比如7天一个分片的话，最近7天的数据会分到一个shard，一周前到两周前的数据会被分到上一个shard，以此类推；在时间分片的基础上还可以再执行Hash Sharding，按照SeriesKey执行Hash（保证同一个SeriesKey对应的所有数据都落到同一个shard），再将数据分散到指定的多个shard中。</p><p><strong>当然，经过笔者深进一步了解，发现单机InfluxDB只有第一层sharding，即只有根据时间进行Range Sharding，并没有执行Hash Sharding。Hash Sharding只会在分布式InfluxDB中才会用到。</strong></p><h1 id="3倒排索引引擎构建倒排索引">3.倒排索引引擎构建倒排索引</h1><p>InfluxDB中倒排索引引擎使用LSM引擎构建，LSM引擎非常适合写多读少的场景，例如HBase、Kudu都是使用的LSM存储引擎，InfluxDB的戴欧索引引擎既然使用的是LSM，那么流程必然是：</p><ol><li>先写WAL以及Cache：数据写入WAL再写入Cache，就返回写入成功，WAL可以保证即使发生异常宕机也可以恢复Cache丢失的数据（MySQL也有类似的double writer机制）</li><li>Cache的flush：一旦满足特定条件系统会将Cache中的时序数据执行flush操作落盘形成文件，文件数量超过一定的阈值就合并成一个大文件</li></ol><p>具体流程如下：</p><h2 id="31wal追加写入">3.1.WAL追加写入</h2><p>Inverted Index WAL的文件格式如下，由一个个LogEntry组成，用于记录数据的每一次的写入，如下图：</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210531214808990.png" alt="image-20210531214808990" /></p><p>一个LogEntry由如下组成</p><ul><li>Flag：更新类型，写入或删除等</li><li>Measurement：表示数据表</li><li>Key/Value：表示写入的Tag Set和Checksum。Checksum用于根据WAL恢复数据时验证LogEntry的完整性</li></ul><h2 id="32cache的写入-inverted-index在内存中构建">3.2.Cache的写入（ Inverted Index在内存中构建）</h2><ol><li>拼接seriesKey：时序数据写入到系统之后先将measurement和所有的维度值拼成一个seriesKey</li><li>确认seriesKey是否已经构建过索引：在文件中确认seriesKey是否存在，这就是Series Block中Bloom Filter的核心作用，首先使用Bloom Filter进行判断，如果不存在，肯定不存在。如果存在，不一定存在，需要进一步判断。再进一步使用B+树以及HashIndex进一步查找判断</li><li>如果seriesKey在文件中不存在，则将其写入内存倒排索引。倒排索引内存结构主要包含两个Map：&lt;measurement, List<tagKey>&gt; 和 &lt;tagKey, &lt;tagValue, List<SeriesKey>&gt;&gt;。InfluxDB中SeriesKey就是一把钥匙，只有拿到这把钥匙才能找到这个SeriesKey对应的数据。而倒排索引就是根据一些线索去找这把钥匙</li></ol><h2 id="33flushinverted-index-cache-flush流程">3.3.flush（Inverted Index Cache Flush流程）</h2><p><strong>触发时机</strong>：当Inverted Index WAL日志的大小超过阈值（默认5MB），就会执行flush操作将缓存中的两个Map写成文件</p><p><strong>基本流程</strong>：</p><ol><li>缓存Map排序：&lt;measurement, List<tagKey>&gt;以及&lt;tagKey, &lt;tagValue, List<SeriesKey>&gt;都需要经过排序处理，排序的意义在于有序数据可以结合Hash Index实现范围查询，另外Series Block中B+树的构建也需要SeriesKey排序。</li><li>构建并持久化Series Block：在排序的基础上首先持久化&lt;tagKey, tagValue, List<SeriesKey>&gt;结构中所有的SeriesKey，也就是先构建Series Block。依次持久化SeriesKey到SeriesKeyChunk，当Chunk满了之后，根据Chunk中最小的SeriesKey构建B+树中的Index Entry节点。当然，Hash Index以及Bloom Filter是需要实时构建的。需要注意的是，Series Block在构建的同时需要记录下SeriesKey与该Key在文件中偏移量的对应关系，即&lt;SeriesKey, SeriesKeyOffset&gt;，这一点至关重要。</li><li>内存中将SeriesKey映射为SeriesId：将&lt;tagKey, &lt;tagValue, List<SeriesKey>&gt;结构中所有的SeriesKey由上一步中得到的&lt;SeriesKey, SeriesKeyOffset &gt;中的SeriesKeyOffset代替。形成新的结构：&lt;tagKey, &lt;tagValue, List<SeriesKeyOffset>&gt;，即&lt;tagKey, &lt;tagValue, List<SeriesKeyId>&gt;&gt;，其中SeriesKeyId就是SeriesKeyOffset。</li><li>构建并持久化Tag Block：在新结构&lt;tagKey, &lt;tagValue, List<SeriesKeyId>&gt;&gt;的基础上首先持久化tagValue，将同一个tagKey下的所有tagValue持久化在一起并生成对应Hash Index写入文件，接着持久化下一个tagKey的所有tagValue。所有tagValue都持久话完成之后再依次持久化所有的tagKey，形成Tag Block。</li><li>构建并持久化Measurement Block：最后持久化measurement形成Measurement Block。</li></ol><h1 id="4时序数据写入流程">4.时序数据写入流程</h1><p>数据写入引擎也是用的TSM类似于LSM的引擎，写入流程也大致如下：先写WAL、再写Cache、最后满足一定条件的阈值后，将Cache中的数据flush到文件</p><h2 id="41wal追加写入">4.1.WAL追加写入</h2><p>时间线数据数据会经过两重处理，首先格式化为WriteWALEntry对象，该对象字段元素如下图所示。然后经过snappy压缩后写入WAL并持久化到文件</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210531224747429.png" alt="image-20210531224747429" /></p><h2 id="42写入cache时序数据写入内存结构">4.2.写入Cache（时序数据写入内存结构）</h2><ol><li><p>时序数据点格式化：将时序数据点point按照时间线性组织成一个Map，&lt;SeriesKey+FieldKey, List<Value>&gt;，即将相同的Key(SeriesKey+FieldKey)的时序数据集中放在一个List中并写入TSM File的block中</p></li><li><p>时序数据点写入Cache：InfluxDB中Cache是一个crude hash ring（一致性hash），这个ring由256个partition组成，每个partition负责存储一部分时序数据key对应的值，就相当于数据写入Cache的时候又根据Key Hash了一次，根据Hash结果映射到不同的partition。</p><p>这样做的原因：个人认为有点像Java中ConcurrentHashMap的思路，将一个大HashMap切分成多个小HashMap，每个HashMap内部在写的时候需要加锁。这样处理可以减小锁粒度，提高写性能。</p></li></ol><h2 id="43flush流程data-cache-flush流程">4.3.Flush流程（Data Cache Flush流程）</h2><p><strong>触发时机</strong>:</p><ol><li>Cache大小超过一定阈值，可以通过参数’cache-snapshot-memory-size’配置，默认是25M大小。</li><li>超过一定时间没有数据写入WAL，默认时间间隔是10分钟，以通过参数’cache-snapshot-write-cold-duration’配置</li></ol><p><strong>基本流程</strong>：</p><ol><li><p>内存中构建Series Data Block：顺序遍历Map：&lt;SeriesKey+FieldKey, List<Value>&gt;中的时序数据，分别对时序数据的时间列和数值列进行相应的编码，按照Series Data Block的格式进行组织，当Block大小超过一定的阈值就构建成功，并记录这个Block</p></li><li><p>将构建好的Series Data Block写入文件：使用输出流将内存中的数据输出到文件，并返回Block在文件中的偏移量offset以及总大小Siez</p></li><li><p>构建文件级别B+树索引：在内存中为该Series Data Block构建一个索引节点Index Entry，使用数据Block在文件中的偏移量offset、总大小size以及MinTime，MaxTime构建一个Index Entry对象，写入到内存搞Series Index Block</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210527202648094.png" alt="image-20210527202648094" /></p></li></ol><p>这样每构建一个Series Data Block并写入文件后都会在内存中顺序构建（注意是顺序，代表这里的B+树是顺序写，这样的再平衡就会快很多，并且也可以利用到B+树查询的快速）一个Index Entry，写入内存Series Index Block对象，一旦一个Key对应的所有时序数据都持久化完成，一个Series Index Block就构建完成（因为一个Series Index Block只会存储一个SeriesKey的数据），构建完成之后填充Index Block Meta信息。接着新建一个新的Series Index Block开始构建下一个Key的对应的数据索引信息</p><h1 id="5influxdb数据删除dropmeasurementdroptagkey">5.InfluxDB数据删除（DropMeasurement，DropTagKey）</h1><p>一般LSM存储引擎采取的删除操作通常都是Tag标记的方式，即删除操作和写入流程一致，只是数据上会多一个Tag标记 – deleted，表示该值已经被deleted。但是这种方案删除的代价是变低了，但是Tag方案在读取的时候需要对标记有deleted的数值进行特殊处理，这个代价还是很大的，但是InfluxDB采取的方案是通常不会删除一条记录，而是会删除某段时间或某个维度下的数据，甚至一张表</p><p>InfluxDB一共有两个LSM引擎，一个是倒排索引引擎（存储维度列到SeriesKey的映射关系，方便多维度查找），一个是TSM 引擎（用于存储时序数据），如果只删除一条记录，倒排索引引擎不需要操作，但是如果删除Measurement的话，两个引擎就都需要操作：</p><ul><li>TSM采取的是同步删除策略</li><li>倒排索引引擎采取标记删除策略</li></ul><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210602231453261.png" alt="image-20210602231453261" /></p><h2 id="51tsm-引擎的删除操作">5.1.TSM 引擎的删除操作</h2><p>同步删除策略流程如下：</p><ol><li><p>TSM FIle Index相关处理：在<strong>内存</strong>中删除Index Entry，通常删除会带有时间筛选或key筛选，而且TSM File Index会在引擎启动之后加载到内存。因此删除操作会将满足条件的Index Entry从内存中删除</p></li><li><p>生成tombstoner文件：tombstoner文件会记录当前TSM File中所有被删除的时序数据，时序数据用[key, min, max]三个字段表示，其中Key即SeriesKey+fileKey，［min, max］表示要删除的时间段</p><p><img src="https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/image-20210602231503645.png" alt="image-20210602231503645" /></p></li><li><p>删除Cache中满足条件的Series</p></li><li><p>在WAL中生成一条删除series的记录并持久化到硬盘</p></li></ol><h2 id="52倒排索引引擎的删除操作">5.2.倒排索引引擎的删除操作</h2><p>标记Tag删除策略，标记Tag删除非常简单，和一次写入流程基本相同</p><ol><li>在WAL中生成一条flag为deleted的LogEntry并持久化到硬盘</li><li>将要删除的维度信息写入Cache，需要标记deleted（设置type=deleted）</li><li>当WAL大小超过阈值之后标记为deleted的维度信息会随Cache Flush到倒排索引文件</li><li>和HBase一样，Inverted Index Engine中索引信息真正被删除发生在<strong>compact阶段</strong></li></ol>]]>
                </content>
            </entry>
</feed>
