<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>小米信息部技术团队</title>
  
  
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://xiaomi-info.github.io/"/>
  <updated>2021-05-24T03:39:30.206Z</updated>
  <id>https://xiaomi-info.github.io/</id>
  
  <author>
    <name>小米信息部技术团队</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>统计建模初探 —— Analysis of Correlation</title>
    <link href="https://xiaomi-info.github.io/2020/05/24/analysis-of-correlation/"/>
    <id>https://xiaomi-info.github.io/2020/05/24/analysis-of-correlation/</id>
    <published>2020-05-24T09:15:00.000Z</published>
    <updated>2021-05-24T03:39:30.206Z</updated>
    
    <content type="html"><![CDATA[<h1 id="统计建模初探-——-Analysis-of-Correlation"><a href="#统计建模初探-——-Analysis-of-Correlation" class="headerlink" title="统计建模初探 —— Analysis of Correlation"></a>统计建模初探 —— Analysis of Correlation</h1><p><strong>[作者简介]</strong> 焦家耀，小米信息技术部售后组</p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>随着现在很多统计分析工具的进化，尤其是 <code>Python</code> 相关的成熟的统计分析包的诞生，让相关统计分析变得简单化。但同时也带来很多滥用的行为，比如在处理回归分析问题，就粗暴的将一个或者多个自变量，直接和因变量做回归分析。这样带来了两方面问题，一是对于结果的解释性变差，二是大多时间是在做无用功，甚至得出错误的结论，尤其是当自变量和因变量之间没有相关性或者只是弱相关性时。</p><p>本文将基于上述背景给出相关性分析的定义，相关性分析的常用方法以及部分相关证明，以及随机变量的数字特征与相关性之间的关系，最后会给出相关性的适用范围。</p><h2 id="关键字"><a href="#关键字" class="headerlink" title="关键字"></a>关键字</h2><p><strong>相关性分析；协方差；相关系数；Pearson 系数；</strong></p><h2 id="相关性分析定义"><a href="#相关性分析定义" class="headerlink" title="相关性分析定义"></a>相关性分析定义</h2><blockquote><p>相关分析是研究两个或两个以上处于同等地位的随机变量间的相关关系的统计分析方法。</p></blockquote><h3 id="两种误解"><a href="#两种误解" class="headerlink" title="两种误解"></a>两种误解</h3><blockquote><p>相关性 = 因果性 ？</p><blockquote><p>相关性是指两个或者多个随机变量之间存在某种关联，因果性是指输入变量对输出变量会造成结 果，前者是原因后者是结果。</p></blockquote></blockquote><p>举个例子：一元方程 $y_t$=a$x_{t-1}$+b，可以说明随机变量 $x$ 与 $y$ 之间存在相关，当随机变量$x$ 与 $y$是平稳时间序列，而且$x$ 与 $y$通过显著性检验，可以说存在因果，由此也可见因果性对证据等级的要求是极为苛刻的。</p><blockquote><p>相关性 = 一定存在映射关系 ？</p><blockquote><p>日常对于相关性存在一种误解，认为相关性就是随机变量间一定存在某种映射。比如计算自由落体运动高度的公式：$h = \frac{1}2gt^2$，给定每一个时间 t 都能得到对应的相对位移 h，于是认为时间 t 和相对位移 h 之间存在相关性。</p></blockquote></blockquote><p>首先可以肯定两者是具有相关性，存在映射关系的一定具有相关性，而且是强相关，下文的相关性分析方法之一的一元回归分析就是一个证明。但是生活中的实际问题并不能一概而论，比如天气温度与冰淇淋销量之间是否存在相关，经验主义告诉我们这个很大可能存在某种相关性，但是天气温度与手机销量是否存在相关性，这个时候就需要定量分析了。</p><p>在实际应用场景中很多变量之间不存在映射或者存在一些非线性的映射关系，这些凭借经验主义是很难发现的，所以要定量的分析变量之间的关系是，才能给出天气温度与手机销量是否相关，促销与销量之间是否相关等问题的科学分析。</p><h2 id="相关性分析方法"><a href="#相关性分析方法" class="headerlink" title="相关性分析方法"></a>相关性分析方法</h2><p>常见的相关性分析方法：</p><ul><li>基于图表的肉眼观测</li><li>协方差｜协方差矩阵</li><li>相关系数（Pearson 系数）</li><li>一元回归｜多元回归</li></ul><h3 id="基于图表的肉眼观测"><a href="#基于图表的肉眼观测" class="headerlink" title="基于图表的肉眼观测"></a>基于图表的肉眼观测</h3><blockquote><p>基于数据绘制相关的散点图，折线图等，凭借肉眼和经验来观测是够具有相关性。这种经验主义的做法，经常会让一些不易观察但实际具有相关的关系被忽视，优劣势十分明显。</p></blockquote><h3 id="协方差｜协方差矩阵"><a href="#协方差｜协方差矩阵" class="headerlink" title="协方差｜协方差矩阵"></a>协方差｜协方差矩阵</h3><h4 id="协方差定义"><a href="#协方差定义" class="headerlink" title="协方差定义"></a>协方差定义</h4><blockquote><p>设$X$, $Y$是两个随机变量，则有</p></blockquote><p>$$ Var(X+Y) = Var(X) + Var(Y) + 2E[(X-E(X))(Y-E(Y))]$$</p><blockquote><p>若$X$, $Y$相互独立，则 $E{[X-E(X)][Y-E(Y)]} = 0$。根据方差的性质，我们可以知道，当 $E{[X-E(X)][Y-E(Y)]} \neq 0$时，则$X$, $Y$存在一定关系。所以称$E{[X-E(X)][Y-E(Y)]}$为随机变量$X$, $Y$的 <code>协方差</code> ，记为$cov(X, Y)$。    </p></blockquote><p>$$ cov(X, Y) = E{[X-E(X)][Y-E(Y)]}$$</p><h4 id="协方差矩阵定义"><a href="#协方差矩阵定义" class="headerlink" title="协方差矩阵定义"></a>协方差矩阵定义</h4><blockquote><p>设$n$维随机变量$(X_1, X_2, …, X_n)$是二阶混合中心矩</p></blockquote><p>$$ c_ij = cov(X_i, X_j) = E{[X_i-E(X_i)][X_j-E(X_j)]} \quad i, j = 1, 2, …, n$$</p><blockquote><p>都存在，则称矩阵  </p></blockquote><p>$$C = \begin{bmatrix}c_{11} &amp; c_{12} &amp; … &amp; c_{1n} \\ c_{21} &amp; c_{22} &amp; … &amp; c_{2n} \\ \vdots &amp; \vdots &amp; \vdots &amp; \vdots \\ c_{n1} &amp; c_{n2} &amp; … &amp; c_{nn}\end{bmatrix}$$</p><blockquote><p>为$n$维随机变量$(X_1, X_2, …, X_n)$的协方差矩阵。由于$c_{ij} = c_{ji}$，因此协方差矩阵还是一个对称矩阵。</p></blockquote><h4 id="协方差｜协方差矩阵应用"><a href="#协方差｜协方差矩阵应用" class="headerlink" title="协方差｜协方差矩阵应用"></a>协方差｜协方差矩阵应用</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> math</span><br><span class="line"><span class="keyword">import</span> random</span><br><span class="line"><span class="keyword">import</span> numpy <span class="keyword">as</span> np</span><br><span class="line"><span class="comment"># 计算期望</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">mean</span><span class="params">(x)</span>:</span></span><br><span class="line">    <span class="keyword">return</span> sum(x)/len(x)</span><br><span class="line"><span class="comment"># 计算每一项数据与均值的差</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">de_mean</span><span class="params">(x)</span>:</span></span><br><span class="line">    x_bar = mean(x)</span><br><span class="line">    <span class="keyword">return</span> [x_i - x_bar <span class="keyword">for</span> x_i <span class="keyword">in</span> x]</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">dot</span><span class="params">(v, w)</span>:</span></span><br><span class="line">    <span class="keyword">return</span> sum(v_i * w_i <span class="keyword">for</span> v_i, w_i <span class="keyword">in</span> zip(v, w))</span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">sum_of_squares</span><span class="params">(v)</span>:</span></span><br><span class="line">    <span class="keyword">return</span> dot(v, v)</span><br><span class="line"><span class="comment"># 方差</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">variance</span><span class="params">(x)</span>:</span></span><br><span class="line">    n = len(x)</span><br><span class="line">    deviations = de_mean(x)</span><br><span class="line">    <span class="keyword">return</span> sum_of_squares(deviations) / (n - <span class="number">1</span>)</span><br><span class="line"><span class="comment"># 标准差</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">standard_deviation</span><span class="params">(x)</span>:</span></span><br><span class="line">    <span class="keyword">return</span> math.sqrt(variance(x))</span><br><span class="line"><span class="comment"># 协方差</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">covariance</span><span class="params">(x, y)</span>:</span></span><br><span class="line">    n = len(x)</span><br><span class="line">    <span class="keyword">return</span> dot(de_mean(x), de_mean(y)) / (n - <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># 随机生成数据</span></span><br><span class="line">a = [random.randint(<span class="number">0</span>, <span class="number">100</span>) <span class="keyword">for</span> i <span class="keyword">in</span> range(<span class="number">25</span>)]</span><br><span class="line">b = [random.randint(<span class="number">0</span>, <span class="number">100</span>) <span class="keyword">for</span> j <span class="keyword">in</span> range(<span class="number">25</span>)]</span><br><span class="line">print(a)</span><br><span class="line">print(b)</span><br><span class="line">print(covariance(a, b))</span><br><span class="line"><span class="comment"># 构造矩阵</span></span><br><span class="line">ab = np.array([a, b])</span><br><span class="line"><span class="comment"># 计算协方差矩阵</span></span><br><span class="line">print(np.cov(ab))</span><br></pre></td></tr></table></figure><img src="/2020/05/24/analysis-of-correlation/covariance.png"><h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><blockquote><p>协方差是用来衡量两个随机变量之间的总体误差。若两个变量的变化趋势一致，协方差是正值，两变量是正相关；若两个变量变化趋势相反，协方差是负值，两变量是负相关；若两个变量相互独立，那么协方差是 0，两变量之间不相关。</p></blockquote><p>换句话来说，协方差｜协方差矩阵给了我们一个定量分析随机变量之间是否存在相关性，以及是正相关还是负相关的方法，但是无法衡量随机变量之间的相关性强弱。</p><h3 id="相关系数"><a href="#相关系数" class="headerlink" title="相关系数"></a>相关系数</h3><h4 id="相关系数（Pearson-系数）定义"><a href="#相关系数（Pearson-系数）定义" class="headerlink" title="相关系数（Pearson 系数）定义"></a>相关系数（Pearson 系数）定义</h4><blockquote><p>设随机变量$X$, $Y$的期望，方差都存在，则</p></blockquote><p>$$\rho_{XY}=\frac{cov(X, Y)}{\sqrt{Var(X)Var(Y)}}$$</p><blockquote><p>为随机变量$X$, $Y$的相关系数，$p_{XY}$是一个无量纲的量。</p></blockquote><h4 id="相关系数的性质"><a href="#相关系数的性质" class="headerlink" title="相关系数的性质"></a>相关系数的性质</h4><ul><li>$|p_{XY}|\leq1$</li><li><p>$|p_{XY}|=1$的充要条件是存在常数$a$, $b$，使得$P\{Y=aX+b\} = 1$</p><blockquote><p>性质 1 可知，相关系数定量的描述了随机变量$X$, $Y$的相关程度，即$|p_{XY}|$越大，相关程度越大，$|p_{XY}|=0$相关程度最低。<br>性质 2 可知，$X$, $Y$完全相关是指在概率为 1 的情况下存在线性关系，于是$|p_{XY}|$也可以表示$X$, $Y$之间的线性关系紧密程度的量，当$|p_{XY}|$较大的时候，通常可以说$X$, $Y$之间线性相关程度较好。</p></blockquote></li></ul><h4 id="相关系数应用"><a href="#相关系数应用" class="headerlink" title="相关系数应用"></a>相关系数应用</h4><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 相关系数</span></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">correlation</span><span class="params">(x, y)</span>:</span></span><br><span class="line">    stdev_x = standard_deviation(x)</span><br><span class="line">    stdev_y = standard_deviation(y)</span><br><span class="line">    <span class="keyword">if</span> stdev_x &gt; <span class="number">0</span> <span class="keyword">and</span> stdev_y &gt; <span class="number">0</span>:</span><br><span class="line">        <span class="keyword">return</span> covariance(x, y) / stdev_x / stdev_y</span><br><span class="line">    <span class="keyword">else</span>:</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line">a = [random.randint(<span class="number">0</span>, <span class="number">100</span>) <span class="keyword">for</span> i <span class="keyword">in</span> range(<span class="number">20</span>)]</span><br><span class="line">b = [random.randint(<span class="number">0</span>, <span class="number">100</span>) <span class="keyword">for</span> j <span class="keyword">in</span> range(<span class="number">20</span>)]</span><br><span class="line">print(a)</span><br><span class="line">print(b)</span><br><span class="line">print(correlation(a, b))</span><br><span class="line"><span class="comment"># 构造矩阵</span></span><br><span class="line">ab = np.array([a, b])</span><br><span class="line"><span class="comment"># 计算相关系数</span></span><br><span class="line">print(np.corrcoef(ab))</span><br></pre></td></tr></table></figure><img src="/2020/05/24/analysis-of-correlation/correlation.png"><h4 id="总结-1"><a href="#总结-1" class="headerlink" title="总结"></a>总结</h4><blockquote><p>相关系数不仅刻画了正负相关，还用量纲刻画了随机变量之间相关性强弱和线性相关性强弱，如上图所示，相关系数矩阵是中$i=j$的时候，$|p_{XY}|=1$也从侧面说明了性质 2 的正确性。<br>Pearson 系数在使用中经常会和斯皮尔曼相关性系数（Spearman 系数），肯德尔相关性系数（Kendall 系数）一起出现，但是三者侧重点各不相同，感兴趣的同学可以自己搜索了解一下。笔者推荐一个相关资料 <a href="https://www.cnblogs.com/yjd_hycf_space/p/11537153.html" target="_blank" rel="noopener">三大系数</a></p></blockquote><h3 id="一元回归｜多元回归"><a href="#一元回归｜多元回归" class="headerlink" title="一元回归｜多元回归"></a>一元回归｜多元回归</h3><h4 id="一元回归的拟合优度"><a href="#一元回归的拟合优度" class="headerlink" title="一元回归的拟合优度"></a>一元回归的拟合优度</h4><blockquote><p>一元回归以及多元回归的相关推导，先留个坑，会在后续的专项回归分析中详细推导，本次重点关注相关性分析。</p></blockquote><blockquote><p>一元回归的拟合优度一般都是通过$R^2$来评估的，</p></blockquote><p>$$R^2=1-\frac{\sum_i{(\hat{y}_i - y_i)^2}}{\sum_i{(\overline{y}_i - y_i)^2}} = 1 - \frac{MSE}{Var}$$</p><blockquote><p>其中$R^2$越大越好，当$R^2=1$的时候，是完美拟合。</p></blockquote><h4 id="多元回归的拟合优度"><a href="#多元回归的拟合优度" class="headerlink" title="多元回归的拟合优度"></a>多元回归的拟合优度</h4><blockquote><p>多元回归的拟合优度以及相关指标的影响权重问题是基于$F$检验和$p-value$检验做，这个涉及到了多元回归的相关推导，我们先按下不表。</p></blockquote><h3 id="最终总结"><a href="#最终总结" class="headerlink" title="最终总结"></a>最终总结</h3><blockquote><p>相关性分析是验证随机变量之间是否存在某种相关性，这种相关性可以是线性也可以是非线性，相关性分析给出了定量的度量，可以有效的解决盲目将数据丢入模型中带来的不可解释性。本文前只是介绍了几种常见的相关性分析方法，像$u$检验，$t$检验等假设检验，还有$F$检验，$p-value$检验等会在后续整理完成后继续更新。</p></blockquote><h2 id="下集预告"><a href="#下集预告" class="headerlink" title="下集预告"></a>下集预告</h2><p>Regression Analysis （回归分析）</p><hr><p><strong>作者</strong></p><p>焦家耀，小米信息技术部售后组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com（武汉）</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;统计建模初探-——-Analysis-of-Correlation&quot;&gt;&lt;a href=&quot;#统计建模初探-——-Analysis-of-Correlation&quot; class=&quot;headerlink&quot; title=&quot;统计建模初探 —— Analysis of Corr
      
    
    </summary>
    
    
      <category term="相关性分析" scheme="https://xiaomi-info.github.io/tags/%E7%9B%B8%E5%85%B3%E6%80%A7%E5%88%86%E6%9E%90/"/>
    
      <category term="协方差" scheme="https://xiaomi-info.github.io/tags/%E5%8D%8F%E6%96%B9%E5%B7%AE/"/>
    
      <category term="相关系数" scheme="https://xiaomi-info.github.io/tags/%E7%9B%B8%E5%85%B3%E7%B3%BB%E6%95%B0/"/>
    
  </entry>
  
  <entry>
    <title>微前端在小米 CRM 系统的实践</title>
    <link href="https://xiaomi-info.github.io/2020/04/14/fe-microfrontends-practice/"/>
    <id>https://xiaomi-info.github.io/2020/04/14/fe-microfrontends-practice/</id>
    <published>2020-04-14T07:53:05.000Z</published>
    <updated>2021-05-24T03:39:30.228Z</updated>
    
    <content type="html"><![CDATA[<img src="/2020/04/14/fe-microfrontends-practice/2020-04-01_15-04-43.png"><p><strong>[作者简介]</strong> 李帅帅，信息技术部平台部前端组，目前主要负责中台业务前端架构及小程序开发。<br><strong>[文章原地址]</strong> <a href="https://www.lishuaishuai.com/architecture/1344.html" target="_blank" rel="noopener">https://www.lishuaishuai.com/architecture/1344.html</a></p><h2 id="一、前言"><a href="#一、前言" class="headerlink" title="一、前言"></a>一、前言</h2><p>大型组织的组织结构、软件架构在不断地发生变化。移动优先（Mobile First）、App 中台（One App）、中台战略等，各种口号在不断的提出、修改和演进。同时，业务也在不断地发展，导致应用不断膨胀，进一步映射到软件架构上。</p><p>现有 Web 应用（SPA）不能很好的拓展和部署，随着时间的推移，各个项目变得越来越臃肿，web 应用变得越来越难以维护。</p><p>微前端是一种类似于微服务的架构，它将微服务的理念应用于浏览器端，即将 Web 应用由单一的单体应用转变为<strong>多个小型前端应用聚合为一的应用</strong>。各个前端应用还可以<strong>独立运行、独立开发、独立部署</strong>。</p><blockquote><p>Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. — Micro Frontends</p></blockquote><p>关键词：解耦、聚合、技术栈无关、独立运行、独立开发、独立部署、易拓展</p><h2 id="二、实施微前端的六种方式"><a href="#二、实施微前端的六种方式" class="headerlink" title="二、实施微前端的六种方式"></a>二、实施微前端的六种方式</h2><p>《前端架构从入门到微前端》一书中，将微前端的实施分为六种：</p><h3 id="2-1-路由分发"><a href="#2-1-路由分发" class="headerlink" title="2.1 路由分发"></a>2.1 路由分发</h3><p>路由分发式微前端，即通过路由将不同的业务分发到不同的、独立前端应用上。其通常可以通过 HTTP 服务器的反向代理来实现，又或者是应用框架自带的路由来解决。如图：</p><img src="/2020/04/14/fe-microfrontends-practice/2020-03-30_19-08-06.png"><h3 id="2-2-前端微服务化"><a href="#2-2-前端微服务化" class="headerlink" title="2.2 前端微服务化"></a>2.2 前端微服务化</h3><p>前端微服务化，是微服务架构在前端的实施，每个前端应用都是完全独立（技术栈、开发、部署、构建独立）、自主运行的，最后通过模块化的方式组合出完成的应用。</p><p>采用这种方式意味着，一个页面上可以同时存在两个以上的前端应用在运行。如图：</p><img src="/2020/04/14/fe-microfrontends-practice/2020-03-30_19-31-27.png"><p>目前主流的框架有 <a href="https://single-spa.js.org/" target="_blank" rel="noopener">Single-SPA</a>、<a href="https://qiankun.umijs.org/" target="_blank" rel="noopener">qiankun</a>、<a href="https://github.com/phodal/mooa" target="_blank" rel="noopener">Mooa</a>，后两者都是基于 <code>Single-SPA</code> 的封装。</p><h3 id="2-3-微应用"><a href="#2-3-微应用" class="headerlink" title="2.3 微应用"></a>2.3 微应用</h3><p>微应用化是指在开发时应用都是以单一、微小应用的形式存在的，而在运行时，则是通过构建系统合并这些应用，并组合成一个新的应用。</p><p>微应用化大都是以软件工程的方式来完成前端应用的聚合，因此又可以称之为<strong>组合式集成</strong>。</p><p>微应用化只能使用唯一的一种前端框架。</p><p>如图：</p><img src="/2020/04/14/fe-microfrontends-practice/2020-03-30_21-01-15.png"><h3 id="2-4-微件化"><a href="#2-4-微件化" class="headerlink" title="2.4 微件化"></a>2.4 微件化</h3><p>微件化（Widget）是一段可以直接嵌入应用上运行的代码，它由开发人员预先编译好，在加载时不需要再做任何修改或编译。微前端下的微件化是指，每个业务团第编写自己的业务代码，并将编译好的代码部署到指定的服务器上，运行时只需要加载指定的代码即可。</p><p>如图：</p><img src="/2020/04/14/fe-microfrontends-practice/2020-03-30_21-56-36.png"><h3 id="2-5-iframe"><a href="#2-5-iframe" class="headerlink" title="2.5 iframe"></a>2.5 iframe</h3><p>iFrame 作为一个非常古老的，人人都觉得普通的技术，却一直很管用。</p><blockquote><p>HTML 内联框架元素 <code>&lt;iframe&gt;</code> 表示嵌套的正在浏览的上下文，能有效地将另一个 HTML 页面嵌入到当前页面中。</p></blockquote><p>iframe 可以创建一个全新的独立的宿主环境，这意味着我们的前端应用之间可以相互独立运行。采用 iframe 有几个重要的前提：</p><ul><li>网站不需要 SEO 支持</li><li>拥有相应的应用管理机制。</li></ul><p>在很多业务场景下，难免会遇到一些难以解决的问题，那么可以引入 iframe 来解决。</p><h3 id="2-6-Web-Components"><a href="#2-6-Web-Components" class="headerlink" title="2.6 Web Components"></a>2.6 Web Components</h3><p>Web Components 是一套不同的技术，允许开发者创建可重用的定制元素（它们的功能封装在代码之外）并且在您 Web 应用中使用它们。</p><p>在真正的项目上使用 Web Components 技术，离现在还有一些距离，结合 Web Components 来构建前端应用，是一种面向未来演进的架构。或者说在未来可以采用这种方式来构建应用。</p><p>如图：</p><img src="/2020/04/14/fe-microfrontends-practice/2020-03-30_22-36-43.png"><p>在真实的业务场景中，往往是上面提到六种方式中的几种的结合使用，或者是某种方式的变种。下面看我遇到的真实场景。</p><h2 id="三、真实的业务场景"><a href="#三、真实的业务场景" class="headerlink" title="三、真实的业务场景"></a>三、真实的业务场景</h2><p>现有三个内部系统，下面称之为 old-a、old-b 和 C，其中，old-a 和 old-b 是老旧的前后端未分离项目，C 为前后端分离的 SPA 应用（React + HIUI），三个系统的架构图大致如下：</p><img src="/2020/04/14/fe-microfrontends-practice/2020-03-31_09-24-28.png"><p>可以看到，old-a 运行在一台服务器 1 上，old-b 运行在服务器 2 上，C 系统的前端资源在服务器 2 上，并且 C 没有自己的域名。</p><p>三个系统均在后端同学维护和开发，他们的需求如下：</p><ul><li>统一的域名</li><li>统一的界面和交互</li><li>系统需要方便拓展</li><li>不希望开发阶段每个系统有独立的代码仓库</li><li>CI 构建</li></ul><h2 id="四、怎么改造？"><a href="#四、怎么改造？" class="headerlink" title="四、怎么改造？"></a>四、怎么改造？</h2><h3 id="4-1-关键点"><a href="#4-1-关键点" class="headerlink" title="4.1 关键点"></a>4.1 关键点</h3><p>考虑开发同学的需求和开发成本、维护成本、未来的可拓展性，系统改造关键点如下：</p><ol><li>申请统一的域名（暂且称之为 crm.mi.com）</li><li>将 old-a 和 old-b 两个老旧的系统样式调整，像系统 C 靠拢</li><li>三个系统使用统一的菜单和权限</li><li>三个系统使用统一的 SSO</li><li>C 系统和正在开发的 X 个系统使用 CI 解决打包和手动 copy 的问题</li></ol><h3 id="4-2-微前端几种方式对比"><a href="#4-2-微前端几种方式对比" class="headerlink" title="4.2 微前端几种方式对比"></a>4.2 微前端几种方式对比</h3><p>总体的改造方案使用微前端的思想进行。对上面提到的六种方式进行对比：</p><img src="/2020/04/14/fe-microfrontends-practice/2020-03-31_10-01-48.png"><h3 id="4-3-实施"><a href="#4-3-实施" class="headerlink" title="4.3 实施"></a>4.3 实施</h3><p>对于上面几种方式，在具体的实施使用了路由分发、iFrame、应用微服务化、微应用化的融合方式。或者说是某种方案的变种，因为改造之后同时具备了这几种方案的特点。</p><p>对于 C 系统和正在开发的 x 个系统使用 single-spa 做改造，对于老旧的系统 old-a 和 old-b 使用 iframe 接入。</p><p>改造后如下图：</p><img src="/2020/04/14/fe-microfrontends-practice/2020-03-31_10-24-53.png"><p>此时，两个老系统分别部署在各自的服务器，C 和未来的多个应用部署在同一台服务器。然后，在 Nginx 层 为老系统分配了两个路由（暂且称之为 old-a 和 old-b），分别将请求打到各自的服务器，根路由打到 C 和 xx 应用的服务器。</p><p>使用 React 框架的 C 和 xx 应用基于 single-spa 改造后，那么老系统 iframe 如何接入？</p><p>在配置菜单时，老系统路由会被带上标识，统一交给其中一个应用以 iframe 的方式处理。</p><p>如图：</p><img src="/2020/04/14/fe-microfrontends-practice/2020-03-31_11-01-55.png"><p>改造后微前端架构图：</p><img src="/2020/04/14/fe-microfrontends-practice/2020-03-31_14-51-36.png"><h2 id="五、一些问题"><a href="#五、一些问题" class="headerlink" title="五、一些问题"></a>五、一些问题</h2><h3 id="5-1-子应用注册方式"><a href="#5-1-子应用注册方式" class="headerlink" title="5.1 子应用注册方式"></a>5.1 子应用注册方式</h3><p>官方示例：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// single-spa-config.js</span></span><br><span class="line"><span class="keyword">import</span> &#123; registerApplication, start &#125; <span class="keyword">from</span> <span class="string">'single-spa'</span></span><br><span class="line"></span><br><span class="line">registerApplication(<span class="string">"applicationName"</span>, loadingFunction, activityFunction)</span><br><span class="line">start()</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">loadingFunction</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">import</span>(<span class="string">"src/app1/main.js"</span>)</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">activityFunction</span>(<span class="params">location</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> location.pathname.indexOf(<span class="string">"/app1/"</span>) === <span class="number">0</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>当增加一个应用的时候，就需要对 <code>single-spa-config.js</code> 文件进行修改。</p><p>通过可配置的方式实现子应用注册：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// single-spa-config.js</span></span><br><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> singleSpa <span class="keyword">from</span> <span class="string">'single-spa'</span></span><br><span class="line"><span class="keyword">import</span> config <span class="keyword">from</span> <span class="string">'./manifest.json'</span></span><br><span class="line"></span><br><span class="line">registerApp(config)</span><br><span class="line"></span><br><span class="line">singleSpa.start()</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">registerApp</span>(<span class="params">conf</span>) </span>&#123;</span><br><span class="line">  conf.forEach(<span class="function"><span class="params">application</span> =&gt;</span> &#123;</span><br><span class="line">    singleSpa.registerApplication(</span><br><span class="line">      application.name,</span><br><span class="line">      () =&gt; <span class="keyword">import</span>(<span class="string">`./<span class="subst">$&#123;application.name&#125;</span>.app/index.js`</span>),</span><br><span class="line">      pathPrefix(application.activeRule, application.strict),</span><br><span class="line">    )</span><br><span class="line">  &#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">pathPrefix</span>(<span class="params">prefix, strict</span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params">location</span>) </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (strict) &#123;</span><br><span class="line">      <span class="keyword">return</span> location.pathname === prefix</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> location.pathname.startsWith(<span class="string">`<span class="subst">$&#123;prefix&#125;</span>`</span>)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">// manifest.json</span><br><span class="line">[</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">"name"</span>: <span class="string">"layout"</span>,</span><br><span class="line">    <span class="attr">"activeRule"</span>: <span class="string">"/"</span></span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">"name"</span>: <span class="string">"welcome"</span>,</span><br><span class="line">    <span class="attr">"activeRule"</span>: <span class="string">"/"</span>,</span><br><span class="line">    <span class="attr">"strict"</span>: <span class="literal">true</span></span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">"name"</span>: <span class="string">"iframe"</span>,</span><br><span class="line">    <span class="attr">"activeRule"</span>: <span class="string">"/link"</span></span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">"name"</span>: <span class="string">"app1"</span>,</span><br><span class="line">    <span class="attr">"activeRule"</span>: <span class="string">"/app1"</span></span><br><span class="line">  &#125;,</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="attr">"name"</span>: <span class="string">"app2"</span>,</span><br><span class="line">    <span class="attr">"activeRule"</span>: <span class="string">"/app2"</span></span><br><span class="line">  &#125;</span><br><span class="line">]</span><br></pre></td></tr></table></figure><h3 id="5-2-共享-cookie"><a href="#5-2-共享-cookie" class="headerlink" title="5.2 共享 cookie"></a>5.2 共享 cookie</h3><p>将域名统一的一大好处是 iframe 域名和主应用域名同源。没有了跨域 可以在 <code>layout</code> 统一 SSO 登录，通过 cookie 共享让其他模块拿到登录信息。</p><h3 id="5-3-应用之间数据共享及通信"><a href="#5-3-应用之间数据共享及通信" class="headerlink" title="5.3 应用之间数据共享及通信"></a>5.3 应用之间数据共享及通信</h3><p>由于此次改造，应用之间不涉及数据共享，所以没有顶级 store 的概念。模块之间的简单通信 可以通过 <code>postMessage</code> 或基于浏览器原生事件做通信。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 应用 A</span></span><br><span class="line"><span class="built_in">window</span>.dispatchEvent(</span><br><span class="line">  <span class="keyword">new</span> CustomEvent(<span class="string">'iframe:change'</span>, &#123; <span class="attr">detail</span>: &#123; <span class="attr">path</span>: <span class="string">'/a/b/c'</span>&#125; &#125;)</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 应用 B</span></span><br><span class="line"><span class="built_in">window</span>.addEventListener(<span class="string">'iframe:change'</span>, (event) =&gt; &#123;</span><br><span class="line">  <span class="built_in">console</span>.log(event.detail.path)</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="5-4-css-隔离"><a href="#5-4-css-隔离" class="headerlink" title="5.4 css 隔离"></a>5.4 css 隔离</h3><p>样式的隔离有很多种处理方式，如：BEM、CSS Module、css 前缀、动态加载/卸载样式表、Web Components 自带隔离机制等。</p><p>此次采用添加 css 前缀来隔离样式，比如 postcss 插件：<code>postcss-plugin-namespace</code>。但是这个插件并不满足需求，我们的应用分布在 <code>src/</code>下，并以 <code>name.app</code> 的方式命名，需要给不同的应用添加不同的前缀。因此使用自己定制的插件：</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">postcss.plugin(<span class="string">'postcss-plugin-namespace'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">return</span> <span class="function"><span class="keyword">function</span>(<span class="params">css</span>) </span>&#123;</span><br><span class="line">    css.walkRules(<span class="function"><span class="params">rule</span> =&gt;</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> (rule.parent &amp;&amp; rule.parent.type === <span class="string">'atrule'</span> &amp;&amp; rule.parent.name !== <span class="string">'media'</span>) <span class="keyword">return</span></span><br><span class="line">      <span class="keyword">const</span> filePath = rule.source &amp;&amp; rule.source.input.file</span><br><span class="line">      <span class="keyword">const</span> appName = <span class="regexp">/src\/(\S*?)\//</span>.exec(filePath)[<span class="number">1</span>] || <span class="string">''</span></span><br><span class="line">      <span class="keyword">const</span> namespace = appName.split(<span class="string">'.'</span>)[<span class="number">0</span>] || <span class="string">''</span></span><br><span class="line"></span><br><span class="line">      rule.selectors = rule.selectors.map(<span class="function"><span class="params">s</span> =&gt;</span> <span class="string">`#<span class="subst">$&#123;namespace&#125;</span> <span class="subst">$&#123;s === <span class="string">'body'</span> ? <span class="string">''</span> : s&#125;</span>`</span>)</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;)</span><br></pre></td></tr></table></figure><h3 id="5-5-js-沙箱"><a href="#5-5-js-沙箱" class="headerlink" title="5.5 js 沙箱"></a>5.5 js 沙箱</h3><p>有一个可严重可不严重的问题，如何确保子应用之间的全局变量不会互相干扰，实现 js 的隔离。普遍的做法是给全局变量添加前缀，这种方式类似 css 的 BEM，通过约定的方式来避免冲突。这种方式简单，但不是很靠谱。</p><p>qiankun 内部的实现方式是通过 <code>Proxy</code> 来实现的沙箱模式，即在应用的 <code>bootstrap</code> 及 <code>mount</code> 两个生命周期开始之前分别给全局状态打下快照，然后当应用切出/卸载时，将状态回滚至 <code>bootstrap</code> 开始之前的阶段，确保应用对全局状态的污染全部清零。有兴趣的同学可以看源码。</p><h2 id="六、优化体验-PWA"><a href="#六、优化体验-PWA" class="headerlink" title="六、优化体验 PWA"></a>六、优化体验 PWA</h2><ol><li>创建桌面图标，快速访问</li><li>Service Worker 缓存，对大文件和不经常更改的文件缓存，优化加载。</li></ol><h2 id="七、-还可以做什么？"><a href="#七、-还可以做什么？" class="headerlink" title="七、 还可以做什么？"></a>七、 还可以做什么？</h2><p>上面的改造已经基本满足了业务需求，针对此业务还有更进一步的做法，达到更好的体验：</p><ul><li>可以使用 <a href="https://github.com/lerna/lerna" target="_blank" rel="noopener">lerna </a> 统一管理所有项目，方便维护，或者让每个应用拥有独立的仓库，做到独立开发。</li><li>可以使用 <a href="https://github.com/systemjs/systemjs" target="_blank" rel="noopener">SystemJS</a> 实现应用的动态加载、独立部署。</li></ul><h2 id="八、总结"><a href="#八、总结" class="headerlink" title="八、总结"></a>八、总结</h2><p>上面提到，此次的实践方式是微前端实现方式中几种的结合，或者是某种方式的变种。也许在理论上并不是最优的，但是在具体的问题中要是最优解。架构设计必须要与当前要解决的问题相匹配，<strong>“没有最优的架构，只有最合适的架构”</strong>。</p><p>微前端不是一个框架或者工具，而是一套架构体系。</p><p>这套体系除了<strong>微前端的基础设施</strong>外还需要具备<strong>微前端配置中心</strong>（版本管理、发布策略、动态构建、中心化管理）、<strong>微前端观察工具</strong>（应用状态可见、可控）等。</p><p>整个体系的搭建将是一个庞大的工程，目前大部分团队是在使用微前端的模式和思想来解决现有系统中的痛点。</p><h2 id="九、推荐阅读"><a href="#九、推荐阅读" class="headerlink" title="九、推荐阅读"></a>九、推荐阅读</h2><p><a href="https://alili.tech/archive/ea599f7c/" target="_blank" rel="noopener">前端微服务化解决方案</a></p><p><a href="https://zhuanlan.zhihu.com/p/78362028" target="_blank" rel="noopener">可能是你见过最完善的微前端解决方案</a></p><p><a href="https://microfrontends.cn/" target="_blank" rel="noopener">微前端的那些事儿</a></p><p><a href="https://micro-frontends.org/" target="_blank" rel="noopener">Micro Frontends</a></p><p><a href="https://single-spa.js.org/" target="_blank" rel="noopener">https://single-spa.js.org/</a></p><hr><p><strong>作者</strong></p><p>李帅帅，小米信息技术部平台部前端组</p><p><strong>招聘</strong></p><p>小米信息部武汉研发中心，信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p><strong>欢迎投递简历：jin.zhang(a)xiaomi.com</strong></p><p>更多技术文章：<a href="https://xiaomi-info.github.io/">小米信息部技术团队</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;img src=&quot;/2020/04/14/fe-microfrontends-practice/2020-04-01_15-04-43.png&quot;&gt;
&lt;p&gt;&lt;strong&gt;[作者简介]&lt;/strong&gt; 李帅帅，信息技术部平台部前端组，目前主要负责中台业务前端架构及小程序开发。&lt;
      
    
    </summary>
    
      <category term="微前端" scheme="https://xiaomi-info.github.io/categories/%E5%BE%AE%E5%89%8D%E7%AB%AF/"/>
    
    
      <category term="架构设计" scheme="https://xiaomi-info.github.io/tags/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1/"/>
    
      <category term="微前端" scheme="https://xiaomi-info.github.io/tags/%E5%BE%AE%E5%89%8D%E7%AB%AF/"/>
    
  </entry>
  
  <entry>
    <title>设计模式基础之——模板模式业务实战</title>
    <link href="https://xiaomi-info.github.io/2020/03/27/oo-use-template/"/>
    <id>https://xiaomi-info.github.io/2020/03/27/oo-use-template/</id>
    <published>2020-03-27T11:30:39.000Z</published>
    <updated>2021-05-24T03:39:30.304Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p><strong>[作者简介]</strong> 施展，小米信息技术部海外商城组</p><p>本文章主要采用如下结构：</p><ul><li>什么是「XX 设计模式」？</li><li>什么真实业务场景可以使用「XX 设计模式」？</li><li>怎么用「XX 设计模式」？</li></ul><p>本文主要介绍「模板模式」如何在真实业务场景中使用。</p><h2 id="什么是「模板模式」？"><a href="#什么是「模板模式」？" class="headerlink" title="什么是「模板模式」？"></a>什么是「模板模式」？</h2><p>抽象类里定义好<strong>算法的执行步骤</strong>和<strong>具体算法</strong>，以及可能发生变化的算法定义为<strong>抽象方法</strong>。不同的子类继承该抽象类，并实现父类的抽象方法。</p><p>模板模式的优势：</p><ul><li>不变的算法被继承复用：不变的部分高度封装、复用。</li><li>变化的算法子类继承并具体实现：变化的部分子类只需要具体实现抽象的部分即可，方便扩展，且可无限扩展。</li></ul><h2 id="什么真实业务场景可以用「模板模式」？"><a href="#什么真实业务场景可以用「模板模式」？" class="headerlink" title="什么真实业务场景可以用「模板模式」？"></a>什么真实业务场景可以用「模板模式」？</h2><p>满足如下要求的所有场景：</p><blockquote><p>算法执行的步骤是稳定<strong>不变的</strong>，但是具体的某些算法可能存在<strong>变</strong>化的场景。</p></blockquote><p>怎么理解，举个例子：<code>比如说你煮个面，必然需要先烧水，水烧开之后再放面进去</code>，以上的流程我们称之为<code>煮面过程</code>。可知：这个<code>煮面过程</code>的步骤是稳定不变的，但是在不同的环境烧水的方式可能不尽相同，也许有的人用天然气烧水、有的人用电磁炉烧水、有的人用柴火烧水，等等。我们可以得到以下结论：</p><ul><li><code>煮面过程</code>的步骤是稳定不变的</li><li><code>煮面过程</code>的烧水方式是可变的</li></ul><blockquote><p>我们有哪些真实业务场景可以用「模板模式」呢？</p></blockquote><p>比如抽奖系统的抽奖接口，为什么：</p><ul><li>抽奖的步骤是稳定不变的 -&gt; <strong>不变的</strong>算法执行步骤</li><li>不同抽奖类型活动在某些逻辑处理方式可能不同 -&gt; <strong>变的</strong>某些算法</li></ul><h2 id="怎么用「模板模式」？"><a href="#怎么用「模板模式」？" class="headerlink" title="怎么用「模板模式」？"></a>怎么用「模板模式」？</h2><p>关于怎么用，完全可以生搬硬套我总结的使用设计模式的四个步骤：</p><ul><li>业务梳理</li><li>业务流程图</li><li>代码建模</li><li>代码 demo</li></ul><h3 id="业务梳理"><a href="#业务梳理" class="headerlink" title="业务梳理"></a>业务梳理</h3><p>我通过历史上接触过的各种抽奖场景（红包雨、糖果雨、打地鼠、大转盘（九宫格）、考眼力、答题闯关、游戏闯关、支付刮刮乐、积分刮刮乐等等），按照真实业务需求梳理了以下抽奖业务抽奖接口的大致文本流程。</p><table><thead><tr><th>主步骤</th><th>主逻辑</th><th>抽奖类型</th><th>子步骤</th><th>子逻辑</th></tr></thead><tbody><tr><td>1</td><td>校验活动编号 (serial_no) 是否存在、并获取活动信息</td><td>-</td><td>-</td><td>-</td></tr><tr><td>2</td><td>校验活动、场次是否正在进行</td><td>-</td><td>-</td><td>-</td></tr><tr><td>3</td><td>其他参数校验 (<strong>不同活动类型实现不同</strong>)</td><td>-</td><td>-</td><td>-</td></tr><tr><td>4</td><td>活动抽奖次数校验（同时扣减）</td><td>-</td><td>-</td><td>-</td></tr><tr><td>5</td><td>活动是否需要消费积分</td><td>-</td><td>-</td><td>-</td></tr><tr><td>6</td><td>场次抽奖次数校验（同时扣减）</td><td>-</td><td>-</td><td>-</td></tr><tr><td>7</td><td>获取场次奖品信息</td><td>-</td><td>-</td><td>-</td></tr><tr><td>8</td><td>获取 node 奖品信息 (<strong>不同活动类型实现不同</strong>)</td><td><strong>按时间抽奖类型</strong></td><td>1</td><td>do nothing（抽取该场次的奖品即可，无需其他逻辑）</td></tr><tr><td>8</td><td></td><td><strong>按抽奖次数抽奖类型</strong></td><td>1</td><td>判断是该用户第几次抽奖</td></tr><tr><td>8</td><td></td><td></td><td>2</td><td>获取对应 node 的奖品信息</td></tr><tr><td>8</td><td></td><td></td><td>3</td><td>复写原所有奖品信息（抽取该 node 节点的奖品）</td></tr><tr><td>8</td><td></td><td><strong>按数额范围区间抽奖</strong></td><td>1</td><td>判断属于哪个数额区间</td></tr><tr><td>8</td><td></td><td></td><td>2</td><td>获取对应 node 的奖品信息</td></tr><tr><td>8</td><td></td><td></td><td>3</td><td>复写原所有奖品信息（抽取该 node 节点的奖品）</td></tr><tr><td>9</td><td>抽奖</td><td>-</td><td>-</td><td>-</td></tr><tr><td>10</td><td>奖品数量判断</td><td>-</td><td>-</td><td>-</td></tr><tr><td>11</td><td>组装奖品信息</td><td>-</td><td>-</td><td>-</td></tr></tbody></table><blockquote><p>注：流程不一定完全准确</p></blockquote><p>结论：</p><ul><li><code>主逻辑</code>是稳定不变的</li><li><code>其他参数校验</code>和<code>获取 node 奖品信息</code>的算法是可变的</li></ul><h3 id="业务流程图"><a href="#业务流程图" class="headerlink" title="业务流程图"></a>业务流程图</h3><p>我们通过梳理的文本业务流程得到了如下的业务流程图：</p><img src="/2020/03/27/oo-use-template/flow.png"><h3 id="代码建模"><a href="#代码建模" class="headerlink" title="代码建模"></a>代码建模</h3><p>通过上面的分析我们可以得到：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">一个抽象类</span><br><span class="line">- 具体共有方法`Run`，里面定义了算法的执行步骤</span><br><span class="line">- 具体私有方法，不会发生变化的具体方法</span><br><span class="line">- 抽象方法，会发生变化的方法</span><br><span class="line"></span><br><span class="line">子类一（按时间抽奖类型）</span><br><span class="line">- 继承抽象类父类</span><br><span class="line">- 实现抽象方法</span><br><span class="line"></span><br><span class="line">子类二（按抽奖次数抽奖类型）</span><br><span class="line">- 继承抽象类父类</span><br><span class="line">- 实现抽象方法</span><br><span class="line"></span><br><span class="line">子类三（按数额范围区间抽奖）</span><br><span class="line">- 继承抽象类父类</span><br><span class="line">- 实现抽象方法</span><br></pre></td></tr></table></figure><p>但是 golang 里面没有继承的概念，我们就把对抽象类里抽象方法的依赖转化成对接口<code>interface</code>里抽想方法的依赖，同时也可以利用<code>合成复用</code>的方式“继承”模板：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">抽象行为的接口`BehaviorInterface`（包含如下需要实现的方法）</span><br><span class="line">- 其他参数校验的方法`checkParams`</span><br><span class="line">- 获取 node 奖品信息的方法`getPrizesByNode`</span><br><span class="line"></span><br><span class="line">抽奖结构体类</span><br><span class="line">- 具体共有方法`Run`，里面定义了算法的执行步骤</span><br><span class="line">- 具体私有方法`checkParams` 里面的逻辑实际依赖的接口 BehaviorInterface.checkParams(ctx) 的抽象方法</span><br><span class="line">- 具体私有方法`getPrizesByNode` 里面的逻辑实际依赖的接口 BehaviorInterface.getPrizesByNode(ctx) 的抽象方法</span><br><span class="line">- 其他具体私有方法，不会发生变化的具体方法</span><br><span class="line"></span><br><span class="line">实现`BehaviorInterface`的结构体一（按时间抽奖类型）</span><br><span class="line">- 实现接口方法</span><br><span class="line"></span><br><span class="line">实现`BehaviorInterface`的结构体二（按抽奖次数抽奖类型）</span><br><span class="line">- 实现接口方法</span><br><span class="line"></span><br><span class="line">实现`BehaviorInterface`的结构体三（按数额范围区间抽奖）</span><br><span class="line">- 实现接口方法</span><br></pre></td></tr></table></figure><p>同时得到了我们的 UML 图：</p><img src="/2020/03/27/oo-use-template/uml.jpg"><h3 id="代码-demo"><a href="#代码-demo" class="headerlink" title="代码 demo"></a>代码 demo</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">"fmt"</span></span><br><span class="line">    <span class="string">"runtime"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">//------------------------------------------------------------</span></span><br><span class="line"><span class="comment">//我的代码没有`else`系列</span></span><br><span class="line"><span class="comment">//模板模式</span></span><br><span class="line"><span class="comment">//@auhtor TIGERB&lt;https://github.com/TIGERB&gt;</span></span><br><span class="line"><span class="comment">//------------------------------------------------------------</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">    <span class="comment">// ConstActTypeTime 按时间抽奖类型</span></span><br><span class="line">    ConstActTypeTime <span class="keyword">int32</span> = <span class="number">1</span></span><br><span class="line">    <span class="comment">// ConstActTypeTimes 按抽奖次数抽奖</span></span><br><span class="line">    ConstActTypeTimes <span class="keyword">int32</span> = <span class="number">2</span></span><br><span class="line">    <span class="comment">// ConstActTypeAmount 按数额范围区间抽奖</span></span><br><span class="line">    ConstActTypeAmount <span class="keyword">int32</span> = <span class="number">3</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Context 上下文</span></span><br><span class="line"><span class="keyword">type</span> Context <span class="keyword">struct</span> &#123;</span><br><span class="line">    ActInfo *ActInfo</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ActInfo 上下文</span></span><br><span class="line"><span class="keyword">type</span> ActInfo <span class="keyword">struct</span> &#123;</span><br><span class="line">    <span class="comment">// 活动抽奖类型 1: 按时间抽奖 2: 按抽奖次数抽奖 3: 按数额范围区间抽奖</span></span><br><span class="line">    ActivityType <span class="keyword">int32</span></span><br><span class="line">    <span class="comment">// 其他字段略</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// BehaviorInterface 不同抽奖类型的行为差异的抽象接口</span></span><br><span class="line"><span class="keyword">type</span> BehaviorInterface <span class="keyword">interface</span> &#123;</span><br><span class="line">    <span class="comment">// 其他参数校验（不同活动类型实现不同）</span></span><br><span class="line">    checkParams(ctx *Context) error</span><br><span class="line">    <span class="comment">// 获取 node 奖品信息（不同活动类型实现不同）</span></span><br><span class="line">    getPrizesByNode(ctx *Context) error</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// TimeDraw 具体抽奖行为</span></span><br><span class="line"><span class="comment">// 按时间抽奖类型 比如红包雨</span></span><br><span class="line"><span class="keyword">type</span> TimeDraw <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkParams 其他参数校验（不同活动类型实现不同）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(draw TimeDraw)</span> <span class="title">checkParams</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"按时间抽奖类型：特殊参数校验。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// getPrizesByNode 获取 node 奖品信息（不同活动类型实现不同）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(draw TimeDraw)</span> <span class="title">getPrizesByNode</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"do nothing（抽取该场次的奖品即可，无需其他逻辑）..."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// TimesDraw 具体抽奖行为</span></span><br><span class="line"><span class="comment">// 按抽奖次数抽奖类型 比如答题闯关</span></span><br><span class="line"><span class="keyword">type</span> TimesDraw <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkParams 其他参数校验（不同活动类型实现不同）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(draw TimesDraw)</span> <span class="title">checkParams</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"按抽奖次数抽奖类型：特殊参数校验。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// getPrizesByNode 获取 node 奖品信息（不同活动类型实现不同）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(draw TimesDraw)</span> <span class="title">getPrizesByNode</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"1. 判断是该用户第几次抽奖。.."</span>)</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"2. 获取对应 node 的奖品信息。.."</span>)</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"3. 复写原所有奖品信息（抽取该 node 节点的奖品）..."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// AmountDraw 具体抽奖行为</span></span><br><span class="line"><span class="comment">// 按数额范围区间抽奖 比如订单金额刮奖</span></span><br><span class="line"><span class="keyword">type</span> AmountDraw <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkParams 其他参数校验（不同活动类型实现不同）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(draw *AmountDraw)</span> <span class="title">checkParams</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"按数额范围区间抽奖：特殊参数校验。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// getPrizesByNode 获取 node 奖品信息（不同活动类型实现不同）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(draw *AmountDraw)</span> <span class="title">getPrizesByNode</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"1. 判断属于哪个数额区间。.."</span>)</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"2. 获取对应 node 的奖品信息。.."</span>)</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"3. 复写原所有奖品信息（抽取该 node 节点的奖品）..."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Lottery 抽奖模板</span></span><br><span class="line"><span class="keyword">type</span> Lottery <span class="keyword">struct</span> &#123;</span><br><span class="line">    <span class="comment">// 不同抽奖类型的抽象行为</span></span><br><span class="line">    concreteBehavior BehaviorInterface</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Run 抽奖算法</span></span><br><span class="line"><span class="comment">// 稳定不变的算法步骤</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">Run</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 具体方法：校验活动编号 (serial_no) 是否存在、并获取活动信息</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.checkSerialNo(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：校验活动、场次是否正在进行</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.checkStatus(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ”抽象方法“：其他参数校验</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.checkParams(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：活动抽奖次数校验（同时扣减）</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.checkTimesByAct(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：活动是否需要消费积分</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.consumePointsByAct(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：场次抽奖次数校验（同时扣减）</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.checkTimesBySession(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：获取场次奖品信息</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.getPrizesBySession(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ”抽象方法“：获取 node 奖品信息</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.getPrizesByNode(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：抽奖</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.drawPrizes(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：奖品数量判断</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.checkPrizesStock(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：组装奖品信息</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.packagePrizeInfo(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkSerialNo 校验活动编号 (serial_no) 是否存在</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">checkSerialNo</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"校验活动编号 (serial_no) 是否存在、并获取活动信息。.."</span>)</span><br><span class="line">    <span class="comment">// 获取活动信息伪代码</span></span><br><span class="line">    ctx.ActInfo = &amp;ActInfo&#123;</span><br><span class="line">    <span class="comment">// 假设当前的活动类型为按抽奖次数抽奖</span></span><br><span class="line">    ActivityType: ConstActTypeTimes,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 获取当前抽奖类型的具体行为</span></span><br><span class="line">    <span class="keyword">switch</span> ctx.ActInfo.ActivityType &#123;</span><br><span class="line">    <span class="keyword">case</span> <span class="number">1</span>:</span><br><span class="line">    <span class="comment">// 按时间抽奖</span></span><br><span class="line">    lottery.concreteBehavior = &amp;TimeDraw&#123;&#125;</span><br><span class="line">    <span class="keyword">case</span> <span class="number">2</span>:</span><br><span class="line">    <span class="comment">// 按抽奖次数抽奖</span></span><br><span class="line">    lottery.concreteBehavior = &amp;TimesDraw&#123;&#125;</span><br><span class="line">    <span class="keyword">case</span> <span class="number">3</span>:</span><br><span class="line">    <span class="comment">// 按数额范围区间抽奖</span></span><br><span class="line">    lottery.concreteBehavior = &amp;AmountDraw&#123;&#125;</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">    <span class="keyword">return</span> fmt.Errorf(<span class="string">"不存在的活动类型"</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkStatus 校验活动、场次是否正在进行</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">checkStatus</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"校验活动、场次是否正在进行。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkParams 其他参数校验（不同活动类型实现不同）</span></span><br><span class="line"><span class="comment">// 不同场景变化的算法 转化为依赖抽象</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">checkParams</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 实际依赖的接口的抽象方法</span></span><br><span class="line">    <span class="keyword">return</span> lottery.concreteBehavior.checkParams(ctx)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkTimesByAct 活动抽奖次数校验</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">checkTimesByAct</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"活动抽奖次数校验。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// consumePointsByAct 活动是否需要消费积分</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">consumePointsByAct</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"活动是否需要消费积分。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkTimesBySession 活动抽奖次数校验</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">checkTimesBySession</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"活动抽奖次数校验。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// getPrizesBySession 获取场次奖品信息</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">getPrizesBySession</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"获取场次奖品信息。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// getPrizesByNode 获取 node 奖品信息（不同活动类型实现不同）</span></span><br><span class="line"><span class="comment">// 不同场景变化的算法 转化为依赖抽象</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">getPrizesByNode</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 实际依赖的接口的抽象方法</span></span><br><span class="line">    <span class="keyword">return</span> lottery.concreteBehavior.getPrizesByNode(ctx)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// drawPrizes 抽奖</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">drawPrizes</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"抽奖。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkPrizesStock 奖品数量判断</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">checkPrizesStock</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"奖品数量判断。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// packagePrizeInfo 组装奖品信息</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">packagePrizeInfo</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"组装奖品信息。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    (&amp;Lottery&#123;&#125;).Run(&amp;Context&#123;&#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取正在运行的函数名</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">runFuncName</span><span class="params">()</span> <span class="title">string</span></span> &#123;</span><br><span class="line">    pc := <span class="built_in">make</span>([]<span class="keyword">uintptr</span>, <span class="number">1</span>)</span><br><span class="line">    runtime.Callers(<span class="number">2</span>, pc)</span><br><span class="line">    f := runtime.FuncForPC(pc[<span class="number">0</span>])</span><br><span class="line">    <span class="keyword">return</span> f.Name()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以下是代码执行结果：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">[Running] go run &quot;.../easy-tips/go/src/patterns/template/template.go&quot;</span><br><span class="line">main.(*Lottery).checkSerialNo 校验活动编号 (serial_no) 是否存在、并获取活动信息。..</span><br><span class="line">main.(*Lottery).checkStatus 校验活动、场次是否正在进行。..</span><br><span class="line">main.TimesDraw.checkParams 按抽奖次数抽奖类型：特殊参数校验。..</span><br><span class="line">main.(*Lottery).checkTimesByAct 活动抽奖次数校验。..</span><br><span class="line">main.(*Lottery).consumePointsByAct 活动是否需要消费积分。..</span><br><span class="line">main.(*Lottery).checkTimesBySession 活动抽奖次数校验。..</span><br><span class="line">main.(*Lottery).getPrizesBySession 获取场次奖品信息。..</span><br><span class="line">main.TimesDraw.getPrizesByNode 1. 判断是该用户第几次抽奖。..</span><br><span class="line">main.TimesDraw.getPrizesByNode 2. 获取对应 node 的奖品信息。..</span><br><span class="line">main.TimesDraw.getPrizesByNode 3. 复写原所有奖品信息（抽取该 node 节点的奖品）...</span><br><span class="line">main.(*Lottery).drawPrizes 抽奖。..</span><br><span class="line">main.(*Lottery).checkPrizesStock 奖品数量判断。..</span><br><span class="line">main.(*Lottery).packagePrizeInfo 组装奖品信息。..</span><br></pre></td></tr></table></figure><p>demo 代码地址：<a href="https://github.com/TIGERB/easy-tips/blob/master/go/src/patterns/template/template.go" target="_blank" rel="noopener">https://github.com/TIGERB/easy-tips/blob/master/go/src/patterns/template/template.go</a></p><h3 id="代码-demo2（利用-golang-的合成复用特性实现）"><a href="#代码-demo2（利用-golang-的合成复用特性实现）" class="headerlink" title="代码 demo2（利用 golang 的合成复用特性实现）"></a>代码 demo2（利用 golang 的<code>合成复用</code>特性实现）</h3><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br><span class="line">260</span><br><span class="line">261</span><br><span class="line">262</span><br><span class="line">263</span><br><span class="line">264</span><br><span class="line">265</span><br><span class="line">266</span><br><span class="line">267</span><br><span class="line">268</span><br><span class="line">269</span><br><span class="line">270</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">"fmt"</span></span><br><span class="line">    <span class="string">"runtime"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">//------------------------------------------------------------</span></span><br><span class="line"><span class="comment">//我的代码没有`else`系列</span></span><br><span class="line"><span class="comment">//模板模式</span></span><br><span class="line"><span class="comment">//@auhtor TIGERB&lt;https://github.com/TIGERB&gt;</span></span><br><span class="line"><span class="comment">//------------------------------------------------------------</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">    <span class="comment">// ConstActTypeTime 按时间抽奖类型</span></span><br><span class="line">    ConstActTypeTime <span class="keyword">int32</span> = <span class="number">1</span></span><br><span class="line">    <span class="comment">// ConstActTypeTimes 按抽奖次数抽奖</span></span><br><span class="line">    ConstActTypeTimes <span class="keyword">int32</span> = <span class="number">2</span></span><br><span class="line">    <span class="comment">// ConstActTypeAmount 按数额范围区间抽奖</span></span><br><span class="line">    ConstActTypeAmount <span class="keyword">int32</span> = <span class="number">3</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// Context 上下文</span></span><br><span class="line"><span class="keyword">type</span> Context <span class="keyword">struct</span> &#123;</span><br><span class="line">    ActInfo *ActInfo</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// ActInfo 上下文</span></span><br><span class="line"><span class="keyword">type</span> ActInfo <span class="keyword">struct</span> &#123;</span><br><span class="line">    <span class="comment">// 活动抽奖类型 1: 按时间抽奖 2: 按抽奖次数抽奖 3: 按数额范围区间抽奖</span></span><br><span class="line">    ActivityType <span class="keyword">int32</span></span><br><span class="line">    <span class="comment">// 其他字段略</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// BehaviorInterface 不同抽奖类型的行为差异的抽象接口</span></span><br><span class="line"><span class="keyword">type</span> BehaviorInterface <span class="keyword">interface</span> &#123;</span><br><span class="line">    <span class="comment">// 其他参数校验（不同活动类型实现不同）</span></span><br><span class="line">    checkParams(ctx *Context) error</span><br><span class="line">    <span class="comment">// 获取 node 奖品信息（不同活动类型实现不同）</span></span><br><span class="line">    getPrizesByNode(ctx *Context) error</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// TimeDraw 具体抽奖行为</span></span><br><span class="line"><span class="comment">// 按时间抽奖类型 比如红包雨</span></span><br><span class="line"><span class="keyword">type</span> TimeDraw <span class="keyword">struct</span> &#123;</span><br><span class="line">    <span class="comment">// 合成复用模板</span></span><br><span class="line">    Lottery</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkParams 其他参数校验（不同活动类型实现不同）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(draw TimeDraw)</span> <span class="title">checkParams</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"按时间抽奖类型：特殊参数校验。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// getPrizesByNode 获取 node 奖品信息（不同活动类型实现不同）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(draw TimeDraw)</span> <span class="title">getPrizesByNode</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"do nothing（抽取该场次的奖品即可，无需其他逻辑）..."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// TimesDraw 具体抽奖行为</span></span><br><span class="line"><span class="comment">// 按抽奖次数抽奖类型 比如答题闯关</span></span><br><span class="line"><span class="keyword">type</span> TimesDraw <span class="keyword">struct</span> &#123;</span><br><span class="line">    <span class="comment">// 合成复用模板</span></span><br><span class="line">    Lottery</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkParams 其他参数校验（不同活动类型实现不同）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(draw TimesDraw)</span> <span class="title">checkParams</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"按抽奖次数抽奖类型：特殊参数校验。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// getPrizesByNode 获取 node 奖品信息（不同活动类型实现不同）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(draw TimesDraw)</span> <span class="title">getPrizesByNode</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"1. 判断是该用户第几次抽奖。.."</span>)</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"2. 获取对应 node 的奖品信息。.."</span>)</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"3. 复写原所有奖品信息（抽取该 node 节点的奖品）..."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// AmountDraw 具体抽奖行为</span></span><br><span class="line"><span class="comment">// 按数额范围区间抽奖 比如订单金额刮奖</span></span><br><span class="line"><span class="keyword">type</span> AmountDraw <span class="keyword">struct</span> &#123;</span><br><span class="line">    <span class="comment">// 合成复用模板</span></span><br><span class="line">    Lottery</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkParams 其他参数校验（不同活动类型实现不同）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(draw *AmountDraw)</span> <span class="title">checkParams</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"按数额范围区间抽奖：特殊参数校验。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// getPrizesByNode 获取 node 奖品信息（不同活动类型实现不同）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(draw *AmountDraw)</span> <span class="title">getPrizesByNode</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"1. 判断属于哪个数额区间。.."</span>)</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"2. 获取对应 node 的奖品信息。.."</span>)</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"3. 复写原所有奖品信息（抽取该 node 节点的奖品）..."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Lottery 抽奖模板</span></span><br><span class="line"><span class="keyword">type</span> Lottery <span class="keyword">struct</span> &#123;</span><br><span class="line">    <span class="comment">// 不同抽奖类型的抽象行为</span></span><br><span class="line">    ConcreteBehavior BehaviorInterface</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Run 抽奖算法</span></span><br><span class="line"><span class="comment">// 稳定不变的算法步骤</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">Run</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 具体方法：校验活动编号 (serial_no) 是否存在、并获取活动信息</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.checkSerialNo(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：校验活动、场次是否正在进行</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.checkStatus(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ”抽象方法“：其他参数校验</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.checkParams(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：活动抽奖次数校验（同时扣减）</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.checkTimesByAct(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：活动是否需要消费积分</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.consumePointsByAct(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：场次抽奖次数校验（同时扣减）</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.checkTimesBySession(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：获取场次奖品信息</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.getPrizesBySession(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ”抽象方法“：获取 node 奖品信息</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.getPrizesByNode(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：抽奖</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.drawPrizes(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：奖品数量判断</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.checkPrizesStock(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 具体方法：组装奖品信息</span></span><br><span class="line">    <span class="keyword">if</span> err = lottery.packagePrizeInfo(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkSerialNo 校验活动编号 (serial_no) 是否存在</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">checkSerialNo</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"校验活动编号 (serial_no) 是否存在、并获取活动信息。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkStatus 校验活动、场次是否正在进行</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">checkStatus</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"校验活动、场次是否正在进行。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkParams 其他参数校验（不同活动类型实现不同）</span></span><br><span class="line"><span class="comment">// 不同场景变化的算法 转化为依赖抽象</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">checkParams</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 实际依赖的接口的抽象方法</span></span><br><span class="line">    <span class="keyword">return</span> lottery.ConcreteBehavior.checkParams(ctx)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkTimesByAct 活动抽奖次数校验</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">checkTimesByAct</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"活动抽奖次数校验。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// consumePointsByAct 活动是否需要消费积分</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">consumePointsByAct</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"活动是否需要消费积分。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkTimesBySession 活动抽奖次数校验</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">checkTimesBySession</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"活动抽奖次数校验。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// getPrizesBySession 获取场次奖品信息</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">getPrizesBySession</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"获取场次奖品信息。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// getPrizesByNode 获取 node 奖品信息（不同活动类型实现不同）</span></span><br><span class="line"><span class="comment">// 不同场景变化的算法 转化为依赖抽象</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">getPrizesByNode</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 实际依赖的接口的抽象方法</span></span><br><span class="line">    <span class="keyword">return</span> lottery.ConcreteBehavior.getPrizesByNode(ctx)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// drawPrizes 抽奖</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">drawPrizes</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"抽奖。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkPrizesStock 奖品数量判断</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">checkPrizesStock</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"奖品数量判断。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// packagePrizeInfo 组装奖品信息</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(lottery *Lottery)</span> <span class="title">packagePrizeInfo</span><span class="params">(ctx *Context)</span> <span class="params">(err error)</span></span> &#123;</span><br><span class="line">    fmt.Println(runFuncName(), <span class="string">"组装奖品信息。.."</span>)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ctx := &amp;Context&#123;</span><br><span class="line">    ActInfo: &amp;ActInfo&#123;</span><br><span class="line">    ActivityType: ConstActTypeAmount,</span><br><span class="line">    &#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">switch</span> ctx.ActInfo.ActivityType &#123;</span><br><span class="line">    <span class="keyword">case</span> ConstActTypeTime: <span class="comment">// 按时间抽奖类型</span></span><br><span class="line">    instance := &amp;TimeDraw&#123;&#125;</span><br><span class="line">    instance.ConcreteBehavior = instance</span><br><span class="line">    instance.Run(ctx)</span><br><span class="line">    <span class="keyword">case</span> ConstActTypeTimes: <span class="comment">// 按抽奖次数抽奖</span></span><br><span class="line">    instance := &amp;TimesDraw&#123;&#125;</span><br><span class="line">    instance.ConcreteBehavior = instance</span><br><span class="line">    instance.Run(ctx)</span><br><span class="line">    <span class="keyword">case</span> ConstActTypeAmount: <span class="comment">// 按数额范围区间抽奖</span></span><br><span class="line">    instance := &amp;AmountDraw&#123;&#125;</span><br><span class="line">    instance.ConcreteBehavior = instance</span><br><span class="line">    instance.Run(ctx)</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">    <span class="comment">// 报错</span></span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 获取正在运行的函数名</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">runFuncName</span><span class="params">()</span> <span class="title">string</span></span> &#123;</span><br><span class="line">    pc := <span class="built_in">make</span>([]<span class="keyword">uintptr</span>, <span class="number">1</span>)</span><br><span class="line">    runtime.Callers(<span class="number">2</span>, pc)</span><br><span class="line">    f := runtime.FuncForPC(pc[<span class="number">0</span>])</span><br><span class="line">    <span class="keyword">return</span> f.Name()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以下是代码执行结果：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">[Running] go run &quot;.../easy-tips/go/src/patterns/template/templateOther.go&quot;</span><br><span class="line">main.(*Lottery).checkSerialNo 校验活动编号 (serial_no) 是否存在、并获取活动信息。..</span><br><span class="line">main.(*Lottery).checkStatus 校验活动、场次是否正在进行。..</span><br><span class="line">main.(*AmountDraw).checkParams 按数额范围区间抽奖：特殊参数校验。..</span><br><span class="line">main.(*Lottery).checkTimesByAct 活动抽奖次数校验。..</span><br><span class="line">main.(*Lottery).consumePointsByAct 活动是否需要消费积分。..</span><br><span class="line">main.(*Lottery).checkTimesBySession 活动抽奖次数校验。..</span><br><span class="line">main.(*Lottery).getPrizesBySession 获取场次奖品信息。..</span><br><span class="line">main.(*AmountDraw).getPrizesByNode 1. 判断属于哪个数额区间。..</span><br><span class="line">main.(*AmountDraw).getPrizesByNode 2. 获取对应 node 的奖品信息。..</span><br><span class="line">main.(*AmountDraw).getPrizesByNode 3. 复写原所有奖品信息（抽取该 node 节点的奖品）...</span><br><span class="line">main.(*Lottery).drawPrizes 抽奖。..</span><br><span class="line">main.(*Lottery).checkPrizesStock 奖品数量判断。..</span><br><span class="line">main.(*Lottery).packagePrizeInfo 组装奖品信息。..</span><br></pre></td></tr></table></figure><p>demo2 代码地址：<a href="https://github.com/TIGERB/easy-tips/blob/master/go/src/patterns/template/templateOther.go" target="_blank" rel="noopener">https://github.com/TIGERB/easy-tips/blob/master/go/src/patterns/template/templateOther.go</a></p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>最后总结下，「模板模式」抽象过程的核心是把握<strong>不变</strong>与<strong>变</strong>：</p><ul><li>不变：<code>Run</code>方法里的抽奖步骤 -&gt; <code>被继承复用</code></li><li>变：不同场景下 -&gt; <code>被具体实现</code><ul><li><code>checkParams</code>参数校验逻辑</li><li><code>getPrizesByNode</code>获取该节点奖品的逻辑</li></ul></li></ul><hr><p><strong>作者</strong></p><p>施展，小米信息技术部海外商城组</p><p><strong>招聘</strong></p><p>小米信息部武汉研发中心，信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p><strong>欢迎投递简历：jin.zhang(a)xiaomi.com</strong></p><p>更多技术文章：<a href="https://xiaomi-info.github.io/">小米信息部技术团队</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;[作者简介]&lt;/strong&gt; 施展，小米信息技术部海外商城组&lt;/p&gt;
&lt;p&gt;本文章主要采用如下结构：&lt;/p&gt;
&lt;ul&gt;
&lt;
      
    
    </summary>
    
      <category term="设计模式" scheme="https://xiaomi-info.github.io/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
    
    
      <category term="Golang" scheme="https://xiaomi-info.github.io/tags/Golang/"/>
    
      <category term="设计模式" scheme="https://xiaomi-info.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>synchronized 实现原理</title>
    <link href="https://xiaomi-info.github.io/2020/03/24/synchronized/"/>
    <id>https://xiaomi-info.github.io/2020/03/24/synchronized/</id>
    <published>2020-03-24T02:15:00.000Z</published>
    <updated>2021-05-24T03:39:30.321Z</updated>
    
    <content type="html"><![CDATA[<h1 id="synchronized-实现原理"><a href="#synchronized-实现原理" class="headerlink" title="synchronized 实现原理"></a>synchronized 实现原理</h1><p><strong>[作者简介]</strong> 张庆波，小米信息技术部架构组</p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>众所周知 <code>synchronized</code> 锁在 <code>Java</code> 中经常使用它的源码是 <code>C++</code> 实现的，它的实现原理是怎样的呢？本文以 <code>OpenJDK 8</code> 为例探究以下内容。</p><ul><li>synchronized 是如何工作的</li><li>synchronized 锁升级过程</li><li>重量级锁的队列之间协作过程和策略</li></ul><h2 id="对象头"><a href="#对象头" class="headerlink" title="对象头"></a>对象头</h2><p>对象头的内容非常多这里我们只做简单介绍以引出后文。在 JVM 中对象布局分为三块区域：</p><ul><li>对象头</li><li>实例数据</li><li>对齐填充</li></ul><img src="/2020/03/24/synchronized/object.png"><p>当线程访问同步块时首先需要获得锁并把相关信息存储在对象头中。所以 <code>wait</code>、<code>notify</code>、<code>notifyAll</code> 这些方法为什么被设计在 <code>Object</code> 中或许你已经找到答案了。</p><p>Hotspot 有两种对象头：</p><ul><li>数组类型，使用 <code>arrayOopDesc</code> 来描述对象头</li><li>其它，使用 <code>instanceOopDesc</code> 来描述对象头</li></ul><p>对象头由两部分组成</p><ul><li>Mark Word：存储自身的运行时数据，例如 HashCode、GC 年龄、锁相关信息等内容。</li><li>Klass Pointer：类型指针指向它的类元数据的指针。</li></ul><p>64 位虚拟机 Mark Word 是 64bit 其结构如下：</p><img src="/2020/03/24/synchronized/sync_1.png"><p>在 JDK 6 中虚拟机团队对锁进行了重要改进，优化了其性能引入了 <code>偏向锁</code>、<code>轻量级锁</code>、<code>适应性自旋</code>、<code>锁消除</code>、<code>锁粗化</code>等实现，其中 <code>锁消除</code>和<code>锁粗化</code>本文不做详细讨论其余内容我们将对其进行逐一探究。</p><p>总体上来说锁状态升级流程如下：</p><img src="/2020/03/24/synchronized/lock.png"><h2 id="偏向锁"><a href="#偏向锁" class="headerlink" title="偏向锁"></a>偏向锁</h2><h3 id="流程"><a href="#流程" class="headerlink" title="流程"></a>流程</h3><p>当线程访问同步块并获取锁时处理流程如下：</p><ol><li>检查 <code>mark word</code> 的<code>线程 id</code> 。</li><li>如果为空则设置 CAS 替换当前线程 id。如果替换成功则获取锁成功，如果失败则撤销偏向锁。</li><li>如果不为空则检查 <code>线程 id</code>为是否为本线程。如果是则获取锁成功，如果失败则撤销偏向锁。</li></ol><p>持有偏向锁的线程以后每次进入这个锁相关的同步块时，只需比对一下 mark word 的线程 id 是否为本线程，如果是则获取锁成功。</p><p>如果发生线程竞争发生 2、3 步失败的情况则需要撤销偏向锁。</p><h3 id="偏向锁的撤销"><a href="#偏向锁的撤销" class="headerlink" title="偏向锁的撤销"></a>偏向锁的撤销</h3><ol><li>偏向锁的撤销动作必须等待全局安全点</li><li>暂停拥有偏向锁的线程，判断锁对象是否处于被锁定状态</li><li>撤销偏向锁恢复到无锁（标志位为 01）或轻量级锁（标志位为 00）的状态</li></ol><h3 id="优点"><a href="#优点" class="headerlink" title="优点"></a>优点</h3><p>只有一个线程执行同步块时进一步提高性能，适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。</p><h3 id="缺点"><a href="#缺点" class="headerlink" title="缺点"></a>缺点</h3><p>如果存在竞争会带来额外的锁撤销操作。</p><h2 id="轻量级锁"><a href="#轻量级锁" class="headerlink" title="轻量级锁"></a>轻量级锁</h2><h3 id="加锁"><a href="#加锁" class="headerlink" title="加锁"></a>加锁</h3><p>多个线程竞争偏向锁导致偏向锁升级为轻量级锁</p><ol><li>JVM 在当前线程的栈帧中创建 Lock Reocrd，并将对象头中的 Mark Word 复制到 Lock Reocrd 中。（Displaced Mark Word）</li><li>线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向 Lock Reocrd 的指针。如果成功则获得锁，如果失败则先检查对象的 Mark Word 是否指向当前线程的栈帧如果是则说明已经获取锁，否则说明其它线程竞争锁则膨胀为重量级锁。</li></ol><h3 id="解锁"><a href="#解锁" class="headerlink" title="解锁"></a>解锁</h3><ol><li>使用 CAS 操作将 Mark Word 还原</li><li>如果第 1 步执行成功则释放完成</li><li>如果第 1 步执行失败则膨胀为重量级锁。</li></ol><h3 id="优点-1"><a href="#优点-1" class="headerlink" title="优点"></a>优点</h3><p>其性能提升的依据是对于绝大部分的锁在整个生命周期内都是不会存在竞争。在多线程交替执行同步块的情况下，可以避免重量级锁引起的性能消耗。</p><h3 id="缺点-1"><a href="#缺点-1" class="headerlink" title="缺点"></a>缺点</h3><p>在有多线程竞争的情况下轻量级锁增加了额外开销。</p><h2 id="自旋锁"><a href="#自旋锁" class="headerlink" title="自旋锁"></a>自旋锁</h2><p>自旋是一种获取锁的机制并不是一个锁状态。在膨胀为重量级锁的过程中或重入时会多次尝试自旋获取锁以避免线程唤醒的开销，但是它会占用 CPU 的时间因此如果同步代码块执行时间很短自旋等待的效果就很好，反之则浪费了 CPU 资源。默认情况下自旋次数是 10 次用户可以使用参数 <code>-XX : PreBlockSpin</code> 来更改。那么如何优化来避免此情况发生呢？我们来看适应性自旋。</p><h3 id="适应性自旋锁"><a href="#适应性自旋锁" class="headerlink" title="适应性自旋锁"></a>适应性自旋锁</h3><p>JDK 6 引入了自适应自旋锁，意味着自旋的次数不在固定，而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果对于某个锁很少自旋成功那么以后有可能省略掉自旋过程以避免资源浪费。有了自适应自旋随着程序运行和性能监控信息的不断完善，虚拟机对程序锁的状况预测就会越来越准确，虛拟机就会变得越来越“聪明”了。</p><h3 id="优点-2"><a href="#优点-2" class="headerlink" title="优点"></a>优点</h3><p>竞争的线程不会阻塞挂起，提高了程序响应速度。避免重量级锁引起的性能消耗。</p><h3 id="缺点-2"><a href="#缺点-2" class="headerlink" title="缺点"></a>缺点</h3><p>如果线程始终无法获取锁，自旋消耗 CPU 最终会膨胀为重量级锁。</p><h2 id="重量级锁"><a href="#重量级锁" class="headerlink" title="重量级锁"></a>重量级锁</h2><p>在重量级锁中没有竞争到锁的对象会 park 被挂起，退出同步块时 unpark 唤醒后续线程。唤醒操作涉及到操作系统调度会有额外的开销。</p><p><code>ObjectMonitor</code> 中包含一个同步队列（由 <code>_cxq</code> 和 <code>_EntryList</code> 组成）一个等待队列（ <code>_WaitSet</code> ）。</p><ul><li>被<code>notify</code>或 <code>notifyAll</code> 唤醒时根据 <code>policy</code> 策略选择加入的队列（policy 默认为 0）</li><li>退出同步块时根据 <code>QMode</code> 策略来唤醒下一个线程（QMode 默认为 0）</li></ul><p>这里稍微提及一下<strong>管程</strong>这个概念。synchronized 关键字及 <code>wait</code>、<code>notify</code>、<code>notifyAll</code> 这三个方法都是管程的组成部分。可以说管程就是一把解决并发问题的万能钥匙。有两大核心问题管程都是能够解决的：</p><ul><li><strong>互斥</strong>：即同一时刻只允许一个线程访问共享资源；</li><li><strong>同步</strong>：即线程之间如何通信、协作。</li></ul><p><code>synchronized</code> 的 <code>monitor</code>锁机制和 JDK 并发包中的 <code>AQS</code> 是很相似的，只不过 <code>AQS</code> 中是一个同步队列多个等待队列。熟悉 <code>AQS</code> 的同学可以拿来做个对比。</p><h3 id="队列协作流程图"><a href="#队列协作流程图" class="headerlink" title="队列协作流程图"></a>队列协作流程图</h3><img src="/2020/03/24/synchronized/sync_2.png"><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><p>在 HotSpot 中 monitor 是由 ObjectMonitor 实现的。其源码是用 c++来实现的源文件是 ObjectMonitor.hpp 主要数据结构如下所示：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">ObjectMonitor() &#123;</span><br><span class="line">    _header       = <span class="literal">NULL</span>;</span><br><span class="line">    _count        = <span class="number">0</span>;</span><br><span class="line">    _waiters      = <span class="number">0</span>,       <span class="comment">// 等待中的线程数</span></span><br><span class="line">    _recursions   = <span class="number">0</span>;       <span class="comment">// 线程重入次数</span></span><br><span class="line">    _object       = <span class="literal">NULL</span>;    <span class="comment">// 存储该 monitor 的对象</span></span><br><span class="line">    _owner        = <span class="literal">NULL</span>;    <span class="comment">// 指向拥有该 monitor 的线程</span></span><br><span class="line">    _WaitSet      = <span class="literal">NULL</span>;    <span class="comment">// 等待线程 双向循环链表_WaitSet 指向第一个节点</span></span><br><span class="line">    _WaitSetLock  = <span class="number">0</span> ;</span><br><span class="line">    _Responsible  = <span class="literal">NULL</span> ;</span><br><span class="line">    _succ         = <span class="literal">NULL</span> ;</span><br><span class="line">    _cxq          = <span class="literal">NULL</span> ;   <span class="comment">// 多线程竞争锁时的单向链表</span></span><br><span class="line">    FreeNext      = <span class="literal">NULL</span> ;</span><br><span class="line">    _EntryList    = <span class="literal">NULL</span> ;   <span class="comment">// _owner 从该双向循环链表中唤醒线程，</span></span><br><span class="line">    _SpinFreq     = <span class="number">0</span> ;</span><br><span class="line">    _SpinClock    = <span class="number">0</span> ;</span><br><span class="line">    OwnerIsThread = <span class="number">0</span> ;</span><br><span class="line">    _previous_owner_tid = <span class="number">0</span>; <span class="comment">// 前一个拥有此监视器的线程 ID</span></span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><blockquote><ol><li>_owner：初始时为 NULL。当有线程占有该 monitor 时 owner 标记为该线程的 ID。当线程释放 monitor 时 owner 恢复为 NULL。owner 是一个临界资源 JVM 是通过 CAS 操作来保证其线程安全的。</li><li>_cxq：竞争队列所有请求锁的线程首先会被放在这个队列中（单向）。_cxq 是一个临界资源 JVM 通过 CAS 原子指令来修改_cxq 队列。<br>每当有新来的节点入队，它的 next 指针总是指向之前队列的头节点，而_cxq 指针会指向该新入队的节点，所以是后来居上。</li><li>_EntryList： _cxq 队列中有资格成为候选资源的线程会被移动到该队列中。</li><li>_WaitSet: 等待队列因为调用 wait 方法而被阻塞的线程会被放在该队列中。</li></ol></blockquote><h3 id="monitor-竞争过程"><a href="#monitor-竞争过程" class="headerlink" title="monitor 竞争过程"></a>monitor 竞争过程</h3><blockquote><ol><li>通过 CAS 尝试把 monitor 的 owner 字段设置为当前线程。</li><li>如果设置之前的 owner 指向当前线程，说明当前线程再次进入 monitor，即重入锁执行 recursions ++ , 记录重入的次数。</li><li>如果当前线程是第一次进入该 monitor, 设置 recursions 为 1,_owner 为当前线程，该线程成功获得锁并返回。</li><li>如果获取锁失败，则等待锁的释放。</li></ol></blockquote><p>执行 <code>monitorenter</code> 指令时 调用以下代码</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">IRT_ENTRY_NO_ASYNC(<span class="keyword">void</span>, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))</span><br><span class="line"><span class="meta">#<span class="meta-keyword">ifdef</span> ASSERT</span></span><br><span class="line">  thread-&gt;last_frame().interpreter_frame_verify_monitor(elem);</span><br><span class="line"><span class="meta">#<span class="meta-keyword">endif</span></span></span><br><span class="line">  <span class="keyword">if</span> (PrintBiasedLockingStatistics) &#123;</span><br><span class="line">    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());</span><br><span class="line">  &#125;</span><br><span class="line">  Handle h_obj(thread, elem-&gt;obj());</span><br><span class="line">  assert(Universe::heap()-&gt;is_in_reserved_or_null(h_obj()),<span class="string">"must be NULL or an object"</span>);</span><br><span class="line"><span class="comment">// 是否使用偏向锁  JVM 启动时设置的偏向锁-XX:-UseBiasedLocking=false/true</span></span><br><span class="line">  <span class="keyword">if</span> (UseBiasedLocking) &#123;</span><br><span class="line">    <span class="comment">// Retry fast entry if bias is revoked to avoid unnecessary inflation</span></span><br><span class="line">    ObjectSynchronizer::fast_enter(h_obj, elem-&gt;lock(), <span class="literal">true</span>, CHECK);</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="comment">// 轻量级锁</span></span><br><span class="line">    ObjectSynchronizer::slow_enter(h_obj, elem-&gt;lock(), CHECK);</span><br><span class="line">  &#125;</span><br><span class="line">  assert(Universe::heap()-&gt;is_in_reserved_or_null(elem-&gt;obj()),</span><br><span class="line">         <span class="string">"must be NULL or an object"</span>);</span><br><span class="line"><span class="meta">#<span class="meta-keyword">ifdef</span> ASSERT</span></span><br><span class="line">  thread-&gt;last_frame().interpreter_frame_verify_monitor(elem);</span><br><span class="line"><span class="meta">#<span class="meta-keyword">endif</span></span></span><br><span class="line">IRT_END</span><br></pre></td></tr></table></figure><blockquote><p><code>slow_enter</code> 方法主要是轻量级锁的一些操作，如果操作失败则会膨胀为重量级锁，过程前面已经描述比较清楚此处不在赘述。<code>enter</code> 方法则为重量级锁的入口源码如下</p></blockquote><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span> ATTR ObjectMonitor::enter(TRAPS) &#123;</span><br><span class="line">  Thread * <span class="keyword">const</span> Self = THREAD ;</span><br><span class="line">  <span class="keyword">void</span> * cur ;</span><br><span class="line">  <span class="comment">// 省略部分代码</span></span><br><span class="line">  </span><br><span class="line">  <span class="comment">// 通过 CAS 操作尝试把 monitor 的_owner 字段设置为当前线程</span></span><br><span class="line">  cur = Atomic::cmpxchg_ptr (Self, &amp;_owner, <span class="literal">NULL</span>) ;</span><br><span class="line">  <span class="keyword">if</span> (cur == <span class="literal">NULL</span>) &#123;</span><br><span class="line">     assert (_recursions == <span class="number">0</span>   , <span class="string">"invariant"</span>) ;</span><br><span class="line">     assert (_owner      == Self, <span class="string">"invariant"</span>) ;</span><br><span class="line">     <span class="keyword">return</span> ;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 线程重入，recursions++</span></span><br><span class="line">  <span class="keyword">if</span> (cur == Self) &#123;</span><br><span class="line">     _recursions ++ ;</span><br><span class="line">     <span class="keyword">return</span> ;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 如果当前线程是第一次进入该 monitor, 设置_recursions 为 1,_owner 为当前线程</span></span><br><span class="line">  <span class="keyword">if</span> (Self-&gt;is_lock_owned ((address)cur)) &#123;</span><br><span class="line">    assert (_recursions == <span class="number">0</span>, <span class="string">"internal state error"</span>);</span><br><span class="line">    _recursions = <span class="number">1</span> ;</span><br><span class="line">    _owner = Self ;</span><br><span class="line">    OwnerIsThread = <span class="number">1</span> ;</span><br><span class="line">    <span class="keyword">return</span> ;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">      jt-&gt;set_suspend_equivalent();</span><br><span class="line">        <span class="comment">// 如果获取锁失败，则等待锁的释放；</span></span><br><span class="line">      EnterI (THREAD) ;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (!ExitSuspendEquivalent(jt)) <span class="keyword">break</span> ;</span><br><span class="line">          _recursions = <span class="number">0</span> ;</span><br><span class="line">      _succ = <span class="literal">NULL</span> ;</span><br><span class="line">      <span class="built_in">exit</span> (<span class="literal">false</span>, Self) ;</span><br><span class="line"></span><br><span class="line">      jt-&gt;java_suspend_self();</span><br><span class="line">    &#125;</span><br><span class="line">    Self-&gt;set_current_pending_monitor(<span class="literal">NULL</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="monitor-等待"><a href="#monitor-等待" class="headerlink" title="monitor 等待"></a>monitor 等待</h3><blockquote><ol><li>当前线程被封装成 ObjectWaiter 对象 node，状态设置成 ObjectWaiter::TS_CXQ。</li><li>for 循环通过 CAS 把 node 节点 push 到<code>_cxq</code>列表中，同一时刻可能有多个线程把自己的 node 节点 push 到<code>_cxq</code>列表中。</li><li>node 节点 push 到_cxq 列表之后，通过自旋尝试获取锁，如果还是没有获取到锁则通过 park 将当前线程挂起等待被唤醒。</li><li>当该线程被唤醒时会从挂起的点继续执行，通过 ObjectMonitor::TryLock 尝试获取锁。</li></ol></blockquote><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 省略部分代码</span></span><br><span class="line"><span class="keyword">void</span> ATTR ObjectMonitor::EnterI (TRAPS) &#123;</span><br><span class="line">    Thread * Self = THREAD ;</span><br><span class="line">    assert (Self-&gt;is_Java_thread(), <span class="string">"invariant"</span>) ;</span><br><span class="line">    assert (((JavaThread *) Self)-&gt;thread_state() == _thread_blocked   , <span class="string">"invariant"</span>) ;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Try lock 尝试获取锁</span></span><br><span class="line">    <span class="keyword">if</span> (TryLock (Self) &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        assert (_succ != Self              , <span class="string">"invariant"</span>) ;</span><br><span class="line">        assert (_owner == Self             , <span class="string">"invariant"</span>) ;</span><br><span class="line">        assert (_Responsible != Self       , <span class="string">"invariant"</span>) ;</span><br><span class="line">        <span class="comment">// 如果获取成功则退出，避免 park unpark 系统调度的开销</span></span><br><span class="line">        <span class="keyword">return</span> ;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 自旋获取锁</span></span><br><span class="line">    <span class="keyword">if</span> (TrySpin(Self) &gt; <span class="number">0</span>) &#123;</span><br><span class="line">        assert (_owner == Self, <span class="string">"invariant"</span>);</span><br><span class="line">        assert (_succ != Self, <span class="string">"invariant"</span>);</span><br><span class="line">        assert (_Responsible != Self, <span class="string">"invariant"</span>);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 当前线程被封装成 ObjectWaiter 对象 node, 状态设置成 ObjectWaiter::TS_CXQ</span></span><br><span class="line">    <span class="function">ObjectWaiter <span class="title">node</span><span class="params">(Self)</span> </span>;</span><br><span class="line">    Self-&gt;_ParkEvent-&gt;reset() ;</span><br><span class="line">    node._prev   = (ObjectWaiter *) <span class="number">0xBAD</span> ;</span><br><span class="line">    node.TState  = ObjectWaiter::TS_CXQ ;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 通过 CAS 把 node 节点 push 到_cxq 列表中</span></span><br><span class="line">    ObjectWaiter * nxt ;</span><br><span class="line">    <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">        node._next = nxt = _cxq ;</span><br><span class="line">        <span class="keyword">if</span> (Atomic::cmpxchg_ptr (&amp;node, &amp;_cxq, nxt) == nxt) <span class="keyword">break</span> ;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 再次 tryLock</span></span><br><span class="line">        <span class="keyword">if</span> (TryLock (Self) &gt; <span class="number">0</span>) &#123;</span><br><span class="line">            assert (_succ != Self         , <span class="string">"invariant"</span>) ;</span><br><span class="line">            assert (_owner == Self        , <span class="string">"invariant"</span>) ;</span><br><span class="line">            assert (_Responsible != Self  , <span class="string">"invariant"</span>) ;</span><br><span class="line">            <span class="keyword">return</span> ;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">        <span class="comment">// 本段代码的主要思想和 AQS 中相似可以类比来看</span></span><br><span class="line">        <span class="comment">// 再次尝试</span></span><br><span class="line">        <span class="keyword">if</span> (TryLock (Self) &gt; <span class="number">0</span>) <span class="keyword">break</span> ;</span><br><span class="line">        assert (_owner != Self, <span class="string">"invariant"</span>) ;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> ((SyncFlags &amp; <span class="number">2</span>) &amp;&amp; _Responsible == <span class="literal">NULL</span>) &#123;</span><br><span class="line">           Atomic::cmpxchg_ptr (Self, &amp;_Responsible, <span class="literal">NULL</span>) ;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 满足条件则 park self</span></span><br><span class="line">        <span class="keyword">if</span> (_Responsible == Self || (SyncFlags &amp; <span class="number">1</span>)) &#123;</span><br><span class="line">            TEVENT (Inflated enter - park TIMED) ;</span><br><span class="line">            Self-&gt;_ParkEvent-&gt;park ((jlong) RecheckInterval) ;</span><br><span class="line">            <span class="comment">// Increase the RecheckInterval, but clamp the value.</span></span><br><span class="line">            RecheckInterval *= <span class="number">8</span> ;</span><br><span class="line">            <span class="keyword">if</span> (RecheckInterval &gt; <span class="number">1000</span>) RecheckInterval = <span class="number">1000</span> ;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            TEVENT (Inflated enter - park UNTIMED) ;</span><br><span class="line">            <span class="comment">// 通过 park 将当前线程挂起，等待被唤醒</span></span><br><span class="line">            Self-&gt;_ParkEvent-&gt;park() ;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (TryLock(Self) &gt; <span class="number">0</span>) <span class="keyword">break</span> ;</span><br><span class="line">        <span class="comment">// 再次尝试自旋</span></span><br><span class="line">        <span class="keyword">if</span> ((Knob_SpinAfterFutile &amp; <span class="number">1</span>) &amp;&amp; TrySpin(Self) &gt; <span class="number">0</span>) <span class="keyword">break</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> ;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="monitor-释放"><a href="#monitor-释放" class="headerlink" title="monitor 释放"></a>monitor 释放</h3><blockquote><p>当某个持有锁的线程执行完同步代码块时，会释放锁并 <code>unpark</code> 后续线程（由于篇幅只保留重要代码）。</p></blockquote><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span> ATTR ObjectMonitor::<span class="built_in">exit</span>(<span class="keyword">bool</span> not_suspended, TRAPS) &#123;</span><br><span class="line">   Thread * Self = THREAD ;</span><br><span class="line">  </span><br><span class="line">   <span class="keyword">if</span> (_recursions != <span class="number">0</span>) &#123;</span><br><span class="line">     _recursions--;        <span class="comment">// this is simple recursive enter</span></span><br><span class="line">     TEVENT (Inflated <span class="built_in">exit</span> - recursive) ;</span><br><span class="line">     <span class="keyword">return</span> ;</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line">      ObjectWaiter * w = <span class="literal">NULL</span> ;</span><br><span class="line">      <span class="keyword">int</span> QMode = Knob_QMode ;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 直接绕过 EntryList 队列，从 cxq 队列中获取线程用于竞争锁</span></span><br><span class="line">      <span class="keyword">if</span> (QMode == <span class="number">2</span> &amp;&amp; _cxq != <span class="literal">NULL</span>) &#123;</span><br><span class="line">          w = _cxq ;</span><br><span class="line">          assert (w != <span class="literal">NULL</span>, <span class="string">"invariant"</span>) ;</span><br><span class="line">          assert (w-&gt;TState == ObjectWaiter::TS_CXQ, <span class="string">"Invariant"</span>) ;</span><br><span class="line">          ExitEpilog (Self, w) ;</span><br><span class="line">          <span class="keyword">return</span> ;</span><br><span class="line">      &#125;</span><br><span class="line">    <span class="comment">// cxq 队列插入 EntryList 尾部</span></span><br><span class="line">      <span class="keyword">if</span> (QMode == <span class="number">3</span> &amp;&amp; _cxq != <span class="literal">NULL</span>) &#123;</span><br><span class="line">          w = _cxq ;</span><br><span class="line">          <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">             assert (w != <span class="literal">NULL</span>, <span class="string">"Invariant"</span>) ;</span><br><span class="line">             ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (<span class="literal">NULL</span>, &amp;_cxq, w) ;</span><br><span class="line">             <span class="keyword">if</span> (u == w) <span class="keyword">break</span> ;</span><br><span class="line">             w = u ;</span><br><span class="line">          &#125;</span><br><span class="line">          ObjectWaiter * q = <span class="literal">NULL</span> ;</span><br><span class="line">          ObjectWaiter * p ;</span><br><span class="line">          <span class="keyword">for</span> (p = w ; p != <span class="literal">NULL</span> ; p = p-&gt;_next) &#123;</span><br><span class="line">              guarantee (p-&gt;TState == ObjectWaiter::TS_CXQ, <span class="string">"Invariant"</span>) ;</span><br><span class="line">              p-&gt;TState = ObjectWaiter::TS_ENTER ;</span><br><span class="line">              p-&gt;_prev = q ;</span><br><span class="line">              q = p ;</span><br><span class="line">          &#125;</span><br><span class="line"></span><br><span class="line">          ObjectWaiter * Tail ;</span><br><span class="line">          <span class="keyword">for</span> (Tail = _EntryList ; Tail != <span class="literal">NULL</span> &amp;&amp; Tail-&gt;_next != <span class="literal">NULL</span> ; Tail = Tail-&gt;_next) ;</span><br><span class="line">          <span class="keyword">if</span> (Tail == <span class="literal">NULL</span>) &#123;</span><br><span class="line">              _EntryList = w ;</span><br><span class="line">          &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">              Tail-&gt;_next = w ;</span><br><span class="line">              w-&gt;_prev = Tail ;</span><br><span class="line">          &#125;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// cxq 队列插入到_EntryList 头部</span></span><br><span class="line">      <span class="keyword">if</span> (QMode == <span class="number">4</span> &amp;&amp; _cxq != <span class="literal">NULL</span>) &#123;</span><br><span class="line">          <span class="comment">// 把 cxq 队列放入 EntryList</span></span><br><span class="line">          <span class="comment">// 此策略确保最近运行的线程位于 EntryList 的头部</span></span><br><span class="line">          w = _cxq ;</span><br><span class="line">          <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">             assert (w != <span class="literal">NULL</span>, <span class="string">"Invariant"</span>) ;</span><br><span class="line">             ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (<span class="literal">NULL</span>, &amp;_cxq, w) ;</span><br><span class="line">             <span class="keyword">if</span> (u == w) <span class="keyword">break</span> ;</span><br><span class="line">             w = u ;</span><br><span class="line">          &#125;</span><br><span class="line">          assert (w != <span class="literal">NULL</span>              , <span class="string">"invariant"</span>) ;</span><br><span class="line"></span><br><span class="line">          ObjectWaiter * q = <span class="literal">NULL</span> ;</span><br><span class="line">          ObjectWaiter * p ;</span><br><span class="line">          <span class="keyword">for</span> (p = w ; p != <span class="literal">NULL</span> ; p = p-&gt;_next) &#123;</span><br><span class="line">              guarantee (p-&gt;TState == ObjectWaiter::TS_CXQ, <span class="string">"Invariant"</span>) ;</span><br><span class="line">              p-&gt;TState = ObjectWaiter::TS_ENTER ;</span><br><span class="line">              p-&gt;_prev = q ;</span><br><span class="line">              q = p ;</span><br><span class="line">          &#125;</span><br><span class="line"></span><br><span class="line">          <span class="keyword">if</span> (_EntryList != <span class="literal">NULL</span>) &#123;</span><br><span class="line">              q-&gt;_next = _EntryList ;</span><br><span class="line">              _EntryList-&gt;_prev = q ;</span><br><span class="line">          &#125;</span><br><span class="line">          _EntryList = w ;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      w = _EntryList  ;</span><br><span class="line">      <span class="keyword">if</span> (w != <span class="literal">NULL</span>) &#123;</span><br><span class="line">          assert (w-&gt;TState == ObjectWaiter::TS_ENTER, <span class="string">"invariant"</span>) ;</span><br><span class="line">          ExitEpilog (Self, w) ;</span><br><span class="line">          <span class="keyword">return</span> ;</span><br><span class="line">      &#125;</span><br><span class="line">      w = _cxq ;</span><br><span class="line">      <span class="keyword">if</span> (w == <span class="literal">NULL</span>) <span class="keyword">continue</span> ;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">          assert (w != <span class="literal">NULL</span>, <span class="string">"Invariant"</span>) ;</span><br><span class="line">          ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (<span class="literal">NULL</span>, &amp;_cxq, w) ;</span><br><span class="line">          <span class="keyword">if</span> (u == w) <span class="keyword">break</span> ;</span><br><span class="line">          w = u ;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (QMode == <span class="number">1</span>) &#123;</span><br><span class="line">         <span class="comment">// QMode == 1 : 把 cxq 倾倒入 EntryList 逆序</span></span><br><span class="line">         ObjectWaiter * s = <span class="literal">NULL</span> ;</span><br><span class="line">         ObjectWaiter * t = w ;</span><br><span class="line">         ObjectWaiter * u = <span class="literal">NULL</span> ;</span><br><span class="line">         <span class="keyword">while</span> (t != <span class="literal">NULL</span>) &#123;</span><br><span class="line">             guarantee (t-&gt;TState == ObjectWaiter::TS_CXQ, <span class="string">"invariant"</span>) ;</span><br><span class="line">             t-&gt;TState = ObjectWaiter::TS_ENTER ;</span><br><span class="line">             u = t-&gt;_next ;</span><br><span class="line">             t-&gt;_prev = u ;</span><br><span class="line">             t-&gt;_next = s ;</span><br><span class="line">             s = t;</span><br><span class="line">             t = u ;</span><br><span class="line">         &#125;</span><br><span class="line">         _EntryList  = s ;</span><br><span class="line">         assert (s != <span class="literal">NULL</span>, <span class="string">"invariant"</span>) ;</span><br><span class="line">      &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">         <span class="comment">// QMode == 0 or QMode == 2</span></span><br><span class="line">         _EntryList = w ;</span><br><span class="line">         ObjectWaiter * q = <span class="literal">NULL</span> ;</span><br><span class="line">         ObjectWaiter * p ;</span><br><span class="line">          <span class="comment">// 将单向链表构造成双向环形链表；</span></span><br><span class="line">         <span class="keyword">for</span> (p = w ; p != <span class="literal">NULL</span> ; p = p-&gt;_next) &#123;</span><br><span class="line">             guarantee (p-&gt;TState == ObjectWaiter::TS_CXQ, <span class="string">"Invariant"</span>) ;</span><br><span class="line">             p-&gt;TState = ObjectWaiter::TS_ENTER ;</span><br><span class="line">             p-&gt;_prev = q ;</span><br><span class="line">             q = p ;</span><br><span class="line">         &#125;</span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="keyword">if</span> (_succ != <span class="literal">NULL</span>) <span class="keyword">continue</span>;</span><br><span class="line"></span><br><span class="line">      w = _EntryList  ;</span><br><span class="line">      <span class="keyword">if</span> (w != <span class="literal">NULL</span>) &#123;</span><br><span class="line">          guarantee (w-&gt;TState == ObjectWaiter::TS_ENTER, <span class="string">"invariant"</span>) ;</span><br><span class="line">          ExitEpilog (Self, w) ;</span><br><span class="line">          <span class="keyword">return</span> ;</span><br><span class="line">      &#125;</span><br><span class="line">   &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="notify-唤醒"><a href="#notify-唤醒" class="headerlink" title="notify 唤醒"></a>notify 唤醒</h3><blockquote><p>notify 或者 notifyAll 方法可以唤醒同一个锁监视器下调用 wait 挂起的线程，具体实现如下</p></blockquote><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span> ObjectMonitor::notify(TRAPS) &#123;</span><br><span class="line">    CHECK_OWNER();</span><br><span class="line">    <span class="keyword">if</span> (_WaitSet == <span class="literal">NULL</span>) &#123;</span><br><span class="line">        TEVENT (Empty - Notify);</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    DTRACE_MONITOR_PROBE(notify, <span class="keyword">this</span>, object(), THREAD);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">int</span> Policy = Knob_MoveNotifyee;</span><br><span class="line"></span><br><span class="line">    Thread::SpinAcquire(&amp;_WaitSetLock, <span class="string">"WaitSet - notify"</span>);</span><br><span class="line">    ObjectWaiter *iterator = DequeueWaiter();</span><br><span class="line">    <span class="keyword">if</span> (iterator != <span class="literal">NULL</span>) &#123;</span><br><span class="line">        <span class="comment">// 省略一些代码</span></span><br><span class="line"></span><br><span class="line">         <span class="comment">// 头插 EntryList</span></span><br><span class="line">        <span class="keyword">if</span> (Policy == <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="keyword">if</span> (List == <span class="literal">NULL</span>) &#123;</span><br><span class="line">                iterator-&gt;_next = iterator-&gt;_prev = <span class="literal">NULL</span>;</span><br><span class="line">                _EntryList = iterator;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                List-&gt;_prev = iterator;</span><br><span class="line">                iterator-&gt;_next = List;</span><br><span class="line">                iterator-&gt;_prev = <span class="literal">NULL</span>;</span><br><span class="line">                _EntryList = iterator;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (Policy == <span class="number">1</span>) &#123;      <span class="comment">// 尾插 EntryList</span></span><br><span class="line">            <span class="keyword">if</span> (List == <span class="literal">NULL</span>) &#123;</span><br><span class="line">                iterator-&gt;_next = iterator-&gt;_prev = <span class="literal">NULL</span>;</span><br><span class="line">                _EntryList = iterator;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                ObjectWaiter *Tail;</span><br><span class="line">                <span class="keyword">for</span> (Tail = List; Tail-&gt;_next != <span class="literal">NULL</span>; Tail = Tail-&gt;_next);</span><br><span class="line">                assert (Tail != <span class="literal">NULL</span> &amp;&amp; Tail-&gt;_next == <span class="literal">NULL</span>, <span class="string">"invariant"</span>);</span><br><span class="line">                Tail-&gt;_next = iterator;</span><br><span class="line">                iterator-&gt;_prev = Tail;</span><br><span class="line">                iterator-&gt;_next = <span class="literal">NULL</span>;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (Policy == <span class="number">2</span>) &#123;      <span class="comment">// 头插 cxq</span></span><br><span class="line">            <span class="comment">// prepend to cxq</span></span><br><span class="line">            <span class="keyword">if</span> (List == <span class="literal">NULL</span>) &#123;</span><br><span class="line">                iterator-&gt;_next = iterator-&gt;_prev = <span class="literal">NULL</span>;</span><br><span class="line">                _EntryList = iterator;</span><br><span class="line">            &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                iterator-&gt;TState = ObjectWaiter::TS_CXQ;</span><br><span class="line">                <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">                    ObjectWaiter *Front = _cxq;</span><br><span class="line">                    iterator-&gt;_next = Front;</span><br><span class="line">                    <span class="keyword">if</span> (Atomic::cmpxchg_ptr(iterator, &amp;_cxq, Front) == Front) &#123;</span><br><span class="line">                        <span class="keyword">break</span>;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> <span class="keyword">if</span> (Policy == <span class="number">3</span>) &#123;      <span class="comment">// 尾插 cxq</span></span><br><span class="line">            iterator-&gt;TState = ObjectWaiter::TS_CXQ;</span><br><span class="line">            <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">                ObjectWaiter *Tail;</span><br><span class="line">                Tail = _cxq;</span><br><span class="line">                <span class="keyword">if</span> (Tail == <span class="literal">NULL</span>) &#123;</span><br><span class="line">                    iterator-&gt;_next = <span class="literal">NULL</span>;</span><br><span class="line">                    <span class="keyword">if</span> (Atomic::cmpxchg_ptr(iterator, &amp;_cxq, <span class="literal">NULL</span>) == <span class="literal">NULL</span>) &#123;</span><br><span class="line">                        <span class="keyword">break</span>;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="keyword">while</span> (Tail-&gt;_next != <span class="literal">NULL</span>) Tail = Tail-&gt;_next;</span><br><span class="line">                    Tail-&gt;_next = iterator;</span><br><span class="line">                    iterator-&gt;_prev = Tail;</span><br><span class="line">                    iterator-&gt;_next = <span class="literal">NULL</span>;</span><br><span class="line">                    <span class="keyword">break</span>;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            ParkEvent *ev = iterator-&gt;_event;</span><br><span class="line">            iterator-&gt;TState = ObjectWaiter::TS_RUN;</span><br><span class="line">            OrderAccess::fence();</span><br><span class="line">            ev-&gt;unpark();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (Policy &lt; <span class="number">4</span>) &#123;</span><br><span class="line">            iterator-&gt;wait_reenter_begin(<span class="keyword">this</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 自旋释放</span></span><br><span class="line">    Thread::SpinRelease(&amp;_WaitSetLock);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (iterator != <span class="literal">NULL</span> &amp;&amp; ObjectMonitor::_sync_Notifications != <span class="literal">NULL</span>) &#123;</span><br><span class="line">        ObjectMonitor::_sync_Notifications-&gt;inc();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文介绍了 <code>synchronized</code> 工作原理和锁升级的过程。其中锁队列的协作流程较复杂，本文配了详细的流程图可以参照。最后附上了一部分重要代码的解析，理解 <code>synchronized</code> 原理之后便于写出性能更高的代码。</p><p>简单的来说偏向锁通过对比 Mark Word thread id 解决加锁问题。而轻量级锁是通过用 CAS 操作 Mark Word 和自旋来解决加锁问题，避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。</p><h2 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h2><ul><li><a href="http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html" target="_blank" rel="noopener">HotSpot Glossary of Terms</a></li><li><a href="https://www.oracle.com/technetwork/java/6-performance-137236.html" target="_blank" rel="noopener">Java SE 6 Performance White Paper</a></li></ul><hr><p><strong>作者</strong></p><p>张庆波，小米信息技术部架构组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com（武汉）</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;synchronized-实现原理&quot;&gt;&lt;a href=&quot;#synchronized-实现原理&quot; class=&quot;headerlink&quot; title=&quot;synchronized 实现原理&quot;&gt;&lt;/a&gt;synchronized 实现原理&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;[作
      
    
    </summary>
    
    
      <category term="java" scheme="https://xiaomi-info.github.io/tags/java/"/>
    
      <category term="synchronized" scheme="https://xiaomi-info.github.io/tags/synchronized/"/>
    
      <category term="隐式锁" scheme="https://xiaomi-info.github.io/tags/%E9%9A%90%E5%BC%8F%E9%94%81/"/>
    
  </entry>
  
  <entry>
    <title>小议 Java 内省机制</title>
    <link href="https://xiaomi-info.github.io/2020/03/16/java-beans-introspection/"/>
    <id>https://xiaomi-info.github.io/2020/03/16/java-beans-introspection/</id>
    <published>2020-03-16T01:18:20.000Z</published>
    <updated>2021-05-24T03:39:30.260Z</updated>
    
    <content type="html"><![CDATA[<h1 id="小议-Java-内省机制"><a href="#小议-Java-内省机制" class="headerlink" title="小议 Java 内省机制"></a>小议 Java 内省机制</h1><p><strong>[作者简介]</strong> 魏民，信息部售后组研发工程师</p><h2 id="基本概念"><a href="#基本概念" class="headerlink" title="基本概念"></a>基本概念</h2><p>Wiki 中是这样描述内省的：</p><blockquote><p>在计算机科学中，内省是指计算机程序在运行时（Run time）检查对象（Object）类型的一种能力，通常也可以称作运行时类型检查。</p></blockquote><p>这个描述非常宽泛，但有三个关键词：</p><ul><li>运行时</li><li>对象</li><li>类型</li></ul><p>Java 官方对 Java Beans 内省的定义：</p><blockquote><p>At runtime and in the builder environment we need to be able to figure out which properties, events, and methods a Java Bean supports. We call this process introspection.</p></blockquote><p>从 Java Bean 的角度来看，这里的对象就是 Bean 对象，主要关注点是属性、方法和事件等，也就是说在运行时可以获取相应的信息进行一些处理，这就是 Java Beans 的内省机制。</p><h3 id="与反射的区别"><a href="#与反射的区别" class="headerlink" title="与反射的区别"></a>与反射的区别</h3><p>Java Beans 内省其实就是对反射的一种封装，这个从源码中或者官方文档中都能看到：</p><blockquote><p>By default we will use a low level reflection mechanism to study the methods supported by a target bean and then apply simple design patterns to deduce from those methods what properties, events, and public methods are supported.</p></blockquote><h2 id="Java-Beans-内省机制"><a href="#Java-Beans-内省机制" class="headerlink" title="Java Beans 内省机制"></a>Java Beans 内省机制</h2><h3 id="核心类库"><a href="#核心类库" class="headerlink" title="核心类库"></a>核心类库</h3><p>Java Beans 内省机制的核心类是 <code>Introspector</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">* The Introspector <span class="class"><span class="keyword">class</span> <span class="title">provides</span> <span class="title">a</span> <span class="title">standard</span> <span class="title">way</span> <span class="title">for</span> <span class="title">tools</span> <span class="title">to</span> <span class="title">learn</span> <span class="title">about</span></span></span><br><span class="line"><span class="class">* <span class="title">the</span> <span class="title">properties</span>, <span class="title">events</span>, <span class="title">and</span> <span class="title">methods</span> <span class="title">supported</span> <span class="title">by</span> <span class="title">a</span> <span class="title">target</span> <span class="title">Java</span> <span class="title">Bean</span>.</span></span><br></pre></td></tr></table></figure><p>操作范围主要包括但不局限于 Java Beans 的属性，事件和方法，具体是基于以下几个类实现：</p><ul><li><code>BeanInfo</code><ul><li>Java Bean 信息类</li></ul></li><li><code>PropertyDescriptor</code><ul><li>属性描述类</li></ul></li><li><code>MethodDescriptor</code><ul><li>方法描述类</li></ul></li><li><code>EventSetDescriptor</code><ul><li>事件描述集合</li></ul></li></ul><p>先看一个示例：</p><p>定义一个 Java Bean：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">User</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String username;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Integer age;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// getter/setter</span></span><br><span class="line">    <span class="comment">// toString</span></span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>测试代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test1</span><span class="params">()</span> <span class="keyword">throws</span> IntrospectionException </span>&#123;</span><br><span class="line">    <span class="comment">//获取 User Bean 信息</span></span><br><span class="line">    BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class);</span><br><span class="line">    <span class="comment">//属性描述</span></span><br><span class="line">    PropertyDescriptor[] propertyDescriptors = userBeanInfo.getPropertyDescriptors();</span><br><span class="line">    System.out.println(<span class="string">"属性描述："</span>);</span><br><span class="line">    Stream.of(propertyDescriptors).forEach(System.out::println);</span><br><span class="line">    <span class="comment">//方法描述</span></span><br><span class="line">    System.out.println(<span class="string">"方法描述："</span>);</span><br><span class="line">    MethodDescriptor[] methodDescriptors = userBeanInfo.getMethodDescriptors();</span><br><span class="line">    Stream.of(methodDescriptors).forEach(System.out::println);</span><br><span class="line">    <span class="comment">//事件描述</span></span><br><span class="line">    System.out.println(<span class="string">"事件描述："</span>);</span><br><span class="line">    EventSetDescriptor[] eventSetDescriptors = userBeanInfo.getEventSetDescriptors();</span><br><span class="line">    Stream.of(eventSetDescriptors).forEach(System.out::println);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出结果：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">属性描述：</span><br><span class="line">java.beans.PropertyDescriptor[name=age; propertyType=<span class="class"><span class="keyword">class</span> <span class="title">java</span>.<span class="title">lang</span>.<span class="title">Integer</span></span>; readMethod=<span class="keyword">public</span> java.lang.Integer introspector.bean.User.getAge(); writeMethod=<span class="keyword">public</span> <span class="keyword">void</span> introspector.bean.User.setAge(java.lang.Integer)]</span><br><span class="line">java.beans.PropertyDescriptor[name=<span class="class"><span class="keyword">class</span></span>; propertyType=<span class="class"><span class="keyword">class</span> <span class="title">java</span>.<span class="title">lang</span>.<span class="title">Class</span></span>; readMethod=<span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">native</span> java.lang.Class java.lang.Object.getClass()]</span><br><span class="line">java.beans.PropertyDescriptor[name=username; propertyType=<span class="class"><span class="keyword">class</span> <span class="title">java</span>.<span class="title">lang</span>.<span class="title">String</span></span>; readMethod=<span class="keyword">public</span> java.lang.String introspector.bean.User.getUsername(); writeMethod=<span class="keyword">public</span> <span class="keyword">void</span> introspector.bean.User.setUsername(java.lang.String)]</span><br><span class="line">方法描述：</span><br><span class="line">java.beans.MethodDescriptor[name=getClass; method=<span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">native</span> java.lang.Class java.lang.Object.getClass()]</span><br><span class="line">java.beans.MethodDescriptor[name=setAge; method=<span class="keyword">public</span> <span class="keyword">void</span> introspector.bean.User.setAge(java.lang.Integer)]</span><br><span class="line">java.beans.MethodDescriptor[name=getAge; method=<span class="keyword">public</span> java.lang.Integer introspector.bean.User.getAge()]</span><br><span class="line">java.beans.MethodDescriptor[name=wait; method=<span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">void</span> java.lang.Object.wait(<span class="keyword">long</span>,<span class="keyword">int</span>) <span class="keyword">throws</span> java.lang.InterruptedException]</span><br><span class="line">java.beans.MethodDescriptor[name=notifyAll; method=<span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">native</span> <span class="keyword">void</span> java.lang.Object.notifyAll()]</span><br><span class="line">java.beans.MethodDescriptor[name=notify; method=<span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">native</span> <span class="keyword">void</span> java.lang.Object.notify()]</span><br><span class="line">java.beans.MethodDescriptor[name=getUsername; method=<span class="keyword">public</span> java.lang.String introspector.bean.User.getUsername()]</span><br><span class="line">java.beans.MethodDescriptor[name=wait; method=<span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">void</span> java.lang.Object.wait() <span class="keyword">throws</span> java.lang.InterruptedException]</span><br><span class="line">java.beans.MethodDescriptor[name=hashCode; method=<span class="keyword">public</span> <span class="keyword">native</span> <span class="keyword">int</span> java.lang.Object.hashCode()]</span><br><span class="line">java.beans.MethodDescriptor[name=setUsername; method=<span class="keyword">public</span> <span class="keyword">void</span> introspector.bean.User.setUsername(java.lang.String)]</span><br><span class="line">java.beans.MethodDescriptor[name=wait; method=<span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">native</span> <span class="keyword">void</span> java.lang.Object.wait(<span class="keyword">long</span>) <span class="keyword">throws</span> java.lang.InterruptedException]</span><br><span class="line">java.beans.MethodDescriptor[name=equals; method=<span class="keyword">public</span> <span class="keyword">boolean</span> java.lang.Object.equals(java.lang.Object)]</span><br><span class="line">java.beans.MethodDescriptor[name=toString; method=<span class="keyword">public</span> java.lang.String introspector.bean.User.toString()]</span><br><span class="line">事件描述：</span><br></pre></td></tr></table></figure><p>可以看出通过内省机制可以获取 Java Bean 的属性、方法描述，这里事件描述是空的（关于事件相关会在后面介绍）。由于 Java 类都会继承 <code>Object</code> 类，可以看到这里将 <code>Object</code> 类相关的属性和方法描述也输出了，如果想将某个类的描述信息排除可以使用 <code>java.beans.Introspector#getBeanInfo(java.lang.Class&lt;?&gt;, java.lang.Class&lt;?&gt;)</code> 这个方法。</p><h3 id="属性处理"><a href="#属性处理" class="headerlink" title="属性处理"></a>属性处理</h3><h4 id="配置绑定"><a href="#配置绑定" class="headerlink" title="配置绑定"></a>配置绑定</h4><p>通过 <code>PropertyDescriptor</code> 可以基于字段名为可写属性设置值。</p><p>比如我们经常会使用这样的配置文件：</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">user:</span></span><br><span class="line"><span class="attr">  username:</span> <span class="string">zhangsan</span></span><br><span class="line"><span class="attr">  age:</span> <span class="number">1</span></span><br></pre></td></tr></table></figure><p>配置文件会与对象进行数据绑定。测试代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test2</span><span class="params">()</span> <span class="keyword">throws</span> IntrospectionException </span>&#123;</span><br><span class="line">    YamlPropertiesFactoryBean yaml = <span class="keyword">new</span> YamlPropertiesFactoryBean();</span><br><span class="line">    yaml.setResources(<span class="keyword">new</span> ClassPathResource(<span class="string">"application.yml"</span>));</span><br><span class="line">    String path = <span class="string">"user."</span>;</span><br><span class="line">    Properties properties = yaml.getObject();</span><br><span class="line">    System.out.println(properties);</span><br><span class="line">    User user = <span class="keyword">new</span> User();</span><br><span class="line">    <span class="comment">//获取 User Bean 信息，排除 Object</span></span><br><span class="line">    BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class, Object.class);</span><br><span class="line">    <span class="comment">//属性描述</span></span><br><span class="line">    PropertyDescriptor[] propertyDescriptors = userBeanInfo.getPropertyDescriptors();</span><br><span class="line">    Stream.of(propertyDescriptors).forEach(propertyDescriptor -&gt; &#123;</span><br><span class="line">        <span class="comment">//获取属性名称</span></span><br><span class="line">        String property = propertyDescriptor.getName();</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            propertyDescriptor.getWriteMethod().invoke(user,properties.get(path+property));</span><br><span class="line">        &#125; <span class="keyword">catch</span> (IllegalAccessException | InvocationTargetException ignored) &#123;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">    System.out.println(user);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出结果：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">User&#123;username=<span class="string">'zhangsan'</span>, age=<span class="number">1</span>&#125;</span><br></pre></td></tr></table></figure><h5 id="在-Spring-中的使用"><a href="#在-Spring-中的使用" class="headerlink" title="在 Spring 中的使用"></a>在 Spring 中的使用</h5><p>在传统的 Spring 开发中我们需要在 web.xml 中指定一些配置参数，比如：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">servlet</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">servlet-name</span>&gt;</span>app<span class="tag">&lt;/<span class="name">servlet-name</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">servlet-class</span>&gt;</span>org.springframework.web.servlet.DispatcherServlet<span class="tag">&lt;/<span class="name">servlet-class</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">init-param</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">param-name</span>&gt;</span>contextConfigLocation<span class="tag">&lt;/<span class="name">param-name</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">param-value</span>&gt;</span><span class="tag">&lt;/<span class="name">param-value</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">init-param</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">load-on-startup</span>&gt;</span>1<span class="tag">&lt;/<span class="name">load-on-startup</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">servlet</span>&gt;</span></span><br></pre></td></tr></table></figure><p>这里有一个 <code>contextConfigLocation</code> 参数，这个参数最终是与 <code>FrameworkServlet</code> 类中的一个属性进行绑定：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">FrameworkServlet</span> <span class="keyword">extends</span> <span class="title">HttpServletBean</span> <span class="keyword">implements</span> <span class="title">ApplicationContextAware</span> </span>&#123;</span><br><span class="line">  <span class="keyword">private</span> String contextConfigLocation;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>那么 Spring 是如何将 web.xml 中的配置项与属性进行绑定的呢，可以参数看<code>org.springframework.web.servlet.HttpServletBean#init()</code> 方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title">init</span><span class="params">()</span> <span class="keyword">throws</span> ServletException </span>&#123;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Set bean properties from init parameters.</span></span><br><span class="line">  PropertyValues pvs = <span class="keyword">new</span> ServletConfigPropertyValues(getServletConfig(), <span class="keyword">this</span>.requiredProperties);</span><br><span class="line">  <span class="keyword">if</span> (!pvs.isEmpty()) &#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">      BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(<span class="keyword">this</span>);</span><br><span class="line">      ResourceLoader resourceLoader = <span class="keyword">new</span> ServletContextResourceLoader(getServletContext());</span><br><span class="line">      bw.registerCustomEditor(Resource.class, <span class="keyword">new</span> ResourceEditor(resourceLoader, getEnvironment()));</span><br><span class="line">      initBeanWrapper(bw);</span><br><span class="line">      bw.setPropertyValues(pvs, <span class="keyword">true</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">catch</span> (BeansException ex) &#123;</span><br><span class="line">      <span class="keyword">if</span> (logger.isErrorEnabled()) &#123;</span><br><span class="line">        logger.error(<span class="string">"Failed to set bean properties on servlet '"</span> + getServletName() + <span class="string">"'"</span>, ex);</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">throw</span> ex;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// Let subclasses do whatever initialization they like.</span></span><br><span class="line">  initServletBean();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到 Spring 是通过 <code>BeanWrapper</code> 完成对属性的绑定：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">BeanWrapper</span> <span class="keyword">extends</span> <span class="title">ConfigurablePropertyAccessor</span> </span>&#123;</span><br><span class="line">    <span class="comment">// 获取属性描述器</span></span><br><span class="line">    PropertyDescriptor[] getPropertyDescriptors();</span><br><span class="line"></span><br><span class="line">    <span class="function">PropertyDescriptor <span class="title">getPropertyDescriptor</span><span class="params">(String var1)</span> <span class="keyword">throws</span> InvalidPropertyException</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而 <code>BeanWrapper</code> 又继承了 <code>PropertyAccessor</code> 接口：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">PropertyAccessor</span> </span>&#123;</span><br><span class="line">    <span class="comment">//读属性</span></span><br><span class="line">    <span class="function"><span class="keyword">boolean</span> <span class="title">isReadableProperty</span><span class="params">(String var1)</span></span>;</span><br><span class="line">    <span class="comment">//写属性</span></span><br><span class="line">    <span class="function"><span class="keyword">boolean</span> <span class="title">isWritableProperty</span><span class="params">(String var1)</span></span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Nullable</span></span><br><span class="line">    Class&lt;?&gt; getPropertyType(String var1) <span class="keyword">throws</span> BeansException;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Nullable</span></span><br><span class="line">    <span class="function">TypeDescriptor <span class="title">getPropertyTypeDescriptor</span><span class="params">(String var1)</span> <span class="keyword">throws</span> BeansException</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>也就是说 Spring 中 <code>BeanWrapper</code> 基于 Java 的内省机制实现了对属性的赋值工作，但是 Spring 并未局限于 Java 提供的 API，而是也进行了扩展和进一步的封装，如 <code>TypeDescriptor</code>。</p><p>可以参考 <code>org.springframework.web.servlet.HttpServletBean#init()</code> 中 <code>BeanWrapper</code> 的使用来实现对 <code>User</code> 对象的属性赋值：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test5</span><span class="params">()</span></span>&#123;</span><br><span class="line">    User user = <span class="keyword">new</span> User();</span><br><span class="line">    BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(user);</span><br><span class="line">    MutablePropertyValues pvs = <span class="keyword">new</span> MutablePropertyValues();</span><br><span class="line">    pvs.add(<span class="string">"username"</span>,<span class="string">"zhangsan"</span>);</span><br><span class="line">    pvs.add(<span class="string">"age"</span>,<span class="number">1</span>);</span><br><span class="line">    bw.setPropertyValues(pvs);</span><br><span class="line">    System.out.println(user);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出结果：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">User&#123;username=<span class="string">'zhangsan'</span>, age=<span class="number">1</span>&#125;</span><br></pre></td></tr></table></figure><h4 id="类型转换"><a href="#类型转换" class="headerlink" title="类型转换"></a>类型转换</h4><p>有属性赋值，必然就会有类型转换。说白了我们从配置文件读取的数据是字符串，与属性进行参数绑定的过程中势必会有类型转换，<code>java.beans</code> 中提供了相应的 API：</p><ul><li><code>PropertyEditor</code><ul><li>属性编辑器顶层接口</li></ul></li><li><code>PropertyEditorSupport</code><ul><li>属性编辑器实现类</li></ul></li><li><code>PropertyEditorManager</code><ul><li>属性编辑器管理器</li><li>在 Spring 中提供了一个 <code>PropertyEditorRegistrar</code></li></ul></li></ul><p>先看一个例子：</p><p>User 类增加 Date 属性：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">User</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String username;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Integer age;</span><br><span class="line">  </span><br><span class="line">    <span class="keyword">private</span> Date createTime;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// getter/setter</span></span><br><span class="line">    <span class="comment">// toString</span></span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>日期转换器：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 日期属性编辑器</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DatPropertyEditor</span> <span class="keyword">extends</span> <span class="title">PropertyEditorSupport</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setAsText</span><span class="params">(String text)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            setValue((text == <span class="keyword">null</span>) ? <span class="keyword">null</span> : <span class="keyword">new</span> SimpleDateFormat(<span class="string">"yyyy-MM-dd"</span>).parse(text));</span><br><span class="line">        &#125; <span class="keyword">catch</span> (ParseException e) &#123;</span><br><span class="line">            e.printStackTrace();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在之前的例子中内省设置属性值都是直接通过 <code>PropertyDescriptor</code> 获取属性的写方法通过反射去赋值，而如果需要对值进行类型转换，则需要通过 <code>PropertyEditorSupport#setAsText</code> 调用 <code>setValue</code> 方法，然后 <code>setValue</code> 方法触发属性属性修改事件：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">PropertyEditorSupport</span> <span class="keyword">implements</span> <span class="title">PropertyEditor</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setValue</span><span class="params">(Object value)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">this</span>.value = value;</span><br><span class="line">        firePropertyChange();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>要注意这里的 <code>value</code> 实际上是临时存储在 <code>PropertyEditorSupport</code> 中，<code>PropertyEditorSupport</code> 则作为事件源，从而得到类型转换后的 <code>value</code>，再通过 <code>PropertyDescriptor</code> 获取属性的写方法通过反射去赋值。</p><p>测试代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test6</span><span class="params">()</span> <span class="keyword">throws</span> IntrospectionException, FileNotFoundException </span>&#123;</span><br><span class="line">   Map&lt;String,Object&gt; properties = ImmutableMap.of(<span class="string">"age"</span>,<span class="number">1</span>,<span class="string">"username"</span>,<span class="string">"zhangsan"</span>,<span class="string">"createTime"</span>,<span class="string">"2020-01-01"</span>);</span><br><span class="line">    User user = <span class="keyword">new</span> User();</span><br><span class="line">    <span class="comment">//获取 User Bean 信息，排除 Object</span></span><br><span class="line">    BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class, Object.class);</span><br><span class="line">    <span class="comment">//属性描述</span></span><br><span class="line">    PropertyDescriptor[] propertyDescriptors = userBeanInfo.getPropertyDescriptors();</span><br><span class="line">    Stream.of(propertyDescriptors).forEach(propertyDescriptor -&gt; &#123;</span><br><span class="line">        <span class="comment">//获取属性名称</span></span><br><span class="line">        String property = propertyDescriptor.getName();</span><br><span class="line">        <span class="comment">//值</span></span><br><span class="line">        Object value = properties.get(property);</span><br><span class="line">        <span class="keyword">if</span> (Objects.equals(<span class="string">"createTime"</span>, property)) &#123;</span><br><span class="line">            <span class="comment">//设置属性编辑器</span></span><br><span class="line">            propertyDescriptor.setPropertyEditorClass(DatPropertyEditor.class);</span><br><span class="line">            <span class="comment">//创建属性编辑器</span></span><br><span class="line">            PropertyEditor propertyEditor = propertyDescriptor.createPropertyEditor(user);</span><br><span class="line">            <span class="comment">//添加监听器</span></span><br><span class="line">            propertyEditor.addPropertyChangeListener(evt -&gt; &#123;</span><br><span class="line">                <span class="comment">//获取转换后的value</span></span><br><span class="line">                Object value1 = propertyEditor.getValue();</span><br><span class="line">                setPropertyValue(user, propertyDescriptor, value1);</span><br><span class="line">            &#125;);</span><br><span class="line">            propertyEditor.setAsText(String.valueOf(value));</span><br><span class="line">            <span class="keyword">return</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        setPropertyValue(user, propertyDescriptor, value);</span><br><span class="line">    &#125;);</span><br><span class="line">    System.out.println(user);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 设置属性值</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">setPropertyValue</span><span class="params">(User user, PropertyDescriptor propertyDescriptor, Object value1)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        propertyDescriptor.getWriteMethod().invoke(user, value1);</span><br><span class="line">    &#125; <span class="keyword">catch</span> (IllegalAccessException | InvocationTargetException ignored) &#123;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出结果：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">User&#123;username=<span class="string">'zhangsan'</span>, age=<span class="number">1</span>, createTime=<span class="number">2020</span>-<span class="number">1</span>-<span class="number">1</span> <span class="number">0</span>:<span class="number">00</span>:<span class="number">00</span>&#125;</span><br></pre></td></tr></table></figure><h3 id="事件监听"><a href="#事件监听" class="headerlink" title="事件监听"></a>事件监听</h3><p>先举一个的例子，在 Mac 中设置飞书通知方式的时候，当界面右侧选择“提示”的时候，那么左侧也会相应的显示为“提示”：</p><img src="/2020/03/16/java-beans-introspection/java-beans-introspection-01.png"><p>如果将右侧看成一个 Java Bean，那么这中间势必存在一个属性变化监听。<code>java.beans</code> 包中也提供了相应实现：</p><ul><li><code>PropertyChangeEvent</code><ul><li>属性变化事件</li></ul></li><li><code>PropertyChangeListener</code><ul><li>属性（生效）变化监听器</li></ul></li><li><code>PropertyChangeSupport</code><ul><li>属性（生效）变化监听器管理器’</li></ul></li><li><code>VetoableChangeListener</code><ul><li>属性（否决）变化监听器</li></ul></li><li><code>VetoableChangeSupport</code><ul><li>属性（否决）变化监听器管理器</li></ul></li></ul><p><code>PropertyChangeEvent</code> 的构造方法：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="title">PropertyChangeEvent</span><span class="params">(Object source, String propertyName,</span></span></span><br><span class="line"><span class="function"><span class="params">    Object oldValue, Object newValue)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">super</span>(source);</span><br><span class="line">    <span class="keyword">this</span>.propertyName = propertyName;</span><br><span class="line">    <span class="keyword">this</span>.newValue = newValue;</span><br><span class="line">    <span class="keyword">this</span>.oldValue = oldValue;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过这个构造方法可以看出属性变化监听的关注点：</p><ul><li><code>source</code><ul><li>事件源</li></ul></li><li><code>propertyName</code><ul><li>发生变化的属性名称</li></ul></li><li><code>oldValue</code><ul><li>旧值</li></ul></li><li><code>newValue</code><ul><li>新值</li></ul></li></ul><p>示例代码：</p><p>在 <code>User</code> 中增加属性（生效）变化监听：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">User</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> String username;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> Integer age;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 属性（生效）变化监听器管理器</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">private</span> PropertyChangeSupport propertyChangeSupport = <span class="keyword">new</span> PropertyChangeSupport(<span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 启动属性（生效）变化</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> propertyName </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> oldValue </span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> newValue</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">firePropertyChange</span><span class="params">(String propertyName, String oldValue, String newValue)</span> </span>&#123;</span><br><span class="line">        PropertyChangeEvent event = <span class="keyword">new</span> PropertyChangeEvent(<span class="keyword">this</span>, propertyName, oldValue, newValue);</span><br><span class="line">        propertyChangeSupport.firePropertyChange(event);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 添加属性（生效）变化监听器</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addPropertyChangeListener</span><span class="params">(PropertyChangeListener listener)</span></span>&#123;</span><br><span class="line">        propertyChangeSupport.addPropertyChangeListener(listener);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 删除属性（生效）变化监听器</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">removePropertyChangeListener</span><span class="params">(PropertyChangeListener listener)</span></span>&#123;</span><br><span class="line">        propertyChangeSupport.removePropertyChangeListener(listener);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 获取属性（生效）变化监听器</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="keyword">public</span> PropertyChangeListener[] getPropertyChangeListeners() &#123;</span><br><span class="line">        <span class="keyword">return</span> propertyChangeSupport.getPropertyChangeListeners();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setUsername</span><span class="params">(String username)</span> </span>&#123;</span><br><span class="line">        String oldValue = <span class="keyword">this</span>.username;</span><br><span class="line">        <span class="keyword">this</span>.username = username;</span><br><span class="line">        firePropertyChange(<span class="string">"username"</span>, oldValue, username);</span><br><span class="line">    &#125;</span><br><span class="line">  </span><br><span class="line">   <span class="comment">// getter/setter</span></span><br><span class="line">   <span class="comment">// toString</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>测试代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test3</span><span class="params">()</span></span>&#123;</span><br><span class="line">    User user = <span class="keyword">new</span> User();</span><br><span class="line">    user.setAge(<span class="number">1</span>);</span><br><span class="line">    user.setUsername(<span class="string">"zhangsan"</span>);</span><br><span class="line">    user.addPropertyChangeListener(System.out::println);</span><br><span class="line">    user.setUsername(<span class="string">"lisi"</span>);</span><br><span class="line">    user.setUsername(<span class="string">"wangwu"</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出结果：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">java.beans.PropertyChangeEvent[propertyName=name; oldValue=zhangsan; newValue=lisi; propagationId=<span class="keyword">null</span>; source=User&#123;username=<span class="string">'lisi'</span>, age=<span class="number">1</span>&#125;]</span><br><span class="line">java.beans.PropertyChangeEvent[propertyName=name; oldValue=lisi; newValue=wangwu; propagationId=<span class="keyword">null</span>; source=User&#123;username=<span class="string">'wangwu'</span>, age=<span class="number">1</span>&#125;]</span><br></pre></td></tr></table></figure><p>可以看到在添加了监听器后，当 username 属性发生变化的时候会出发监听事件。</p><p>再看看另外一种监听器 <code>VetoableChangeListener</code>。在 <code>User</code> 中添加监听器：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 属性（否决）变化监听器</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> VetoableChangeSupport vetoableChangeSupport = <span class="keyword">new</span> VetoableChangeSupport(<span class="keyword">this</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 启动属性（否决）变化</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> propertyName</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> oldValue</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> newValue</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">fireVetoableChange</span><span class="params">(String propertyName, String oldValue, String newValue)</span> <span class="keyword">throws</span> PropertyVetoException </span>&#123;</span><br><span class="line">    PropertyChangeEvent event = <span class="keyword">new</span> PropertyChangeEvent(<span class="keyword">this</span>, propertyName, oldValue, newValue);</span><br><span class="line">    vetoableChangeSupport.fireVetoableChange(event);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 添加属性（否决）变化监听器</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addVetoableChangeListener</span><span class="params">(VetoableChangeListener listener)</span></span>&#123;</span><br><span class="line">    vetoableChangeSupport.addVetoableChangeListener(listener);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 删除属性（否决）变化监听器</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">removeVetoableChangeListener</span><span class="params">(VetoableChangeListener listener)</span></span>&#123;</span><br><span class="line">    vetoableChangeSupport.removeVetoableChangeListener(listener);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">setUsername</span><span class="params">(String username)</span> <span class="keyword">throws</span> PropertyVetoException </span>&#123;</span><br><span class="line">    String oldValue = <span class="keyword">this</span>.username;</span><br><span class="line">    fireVetoableChange(<span class="string">"username"</span>,oldValue,username);</span><br><span class="line">    <span class="keyword">this</span>.username = username;</span><br><span class="line">    firePropertyChange(<span class="string">"username"</span>, oldValue, username);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>测试代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test3</span><span class="params">()</span> <span class="keyword">throws</span> PropertyVetoException </span>&#123;</span><br><span class="line">    User user = <span class="keyword">new</span> User();</span><br><span class="line">    user.setAge(<span class="number">1</span>);</span><br><span class="line">    user.addVetoableChangeListener(evt -&gt; &#123;</span><br><span class="line">        System.out.println(evt.getNewValue()+<span class="string">",,"</span>+evt.getOldValue());</span><br><span class="line">        <span class="keyword">if</span> (Objects.equals(evt.getNewValue(), evt.getOldValue())) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> PropertyVetoException(<span class="string">"当前属性值未发生任何变化"</span>, evt);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">    user.addPropertyChangeListener(System.out::println);</span><br><span class="line">    user.setUsername(<span class="string">"lisi"</span>);</span><br><span class="line">    user.setUsername(<span class="string">"zhangsan"</span>);</span><br><span class="line">    user.setUsername(<span class="string">"zhangsan"</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行时发现一直无法抛出异常。查看源码发现 <code>PropertyChangeSupport</code> 和 <code>VetoableChangeSupport</code> 当新旧值相等时不会触发监听，于是修改测试代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test3</span><span class="params">()</span> <span class="keyword">throws</span> PropertyVetoException </span>&#123;</span><br><span class="line">    User user = <span class="keyword">new</span> User();</span><br><span class="line">    user.setAge(<span class="number">1</span>);</span><br><span class="line">    user.addVetoableChangeListener(evt -&gt; &#123;</span><br><span class="line">        System.out.println(evt.getNewValue()+<span class="string">",,"</span>+evt.getOldValue());</span><br><span class="line">        <span class="keyword">if</span> (Objects.isNull(evt.getNewValue())) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> PropertyVetoException(<span class="string">"username 不能为null"</span>, evt);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;);</span><br><span class="line">    user.addPropertyChangeListener(System.out::println);</span><br><span class="line">    user.setUsername(<span class="string">"lisi"</span>);</span><br><span class="line">    user.setUsername(<span class="keyword">null</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>运行结果：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">lisi,,<span class="keyword">null</span></span><br><span class="line">java.beans.PropertyChangeEvent[propertyName=username; oldValue=<span class="keyword">null</span>; newValue=lisi; propagationId=<span class="keyword">null</span>; source=User&#123;username=<span class="string">'lisi'</span>, age=<span class="number">1</span>&#125;]</span><br><span class="line"><span class="keyword">null</span>,,lisi</span><br><span class="line"></span><br><span class="line">java.beans.PropertyVetoException: username 不能为<span class="keyword">null</span></span><br><span class="line"></span><br><span class="line">  at introspector.test.IntrospectorTest.lambda$test3$<span class="number">1</span>(IntrospectorTest.java:<span class="number">78</span>)</span><br><span class="line">  at java.beans.VetoableChangeSupport.fireVetoableChange(VetoableChangeSupport.java:<span class="number">375</span>)</span><br></pre></td></tr></table></figure><p>可以发现当符合“否决”属性变化的条件时，会抛出 <code>PropertyVetoException</code> 异常阻断属性的变化。</p><p>在之前的示例中 <code>userBeanInfo</code> 输出的 <code>EventSetDescriptor</code> 为空，这是因为并未到 <code>User</code> 类中增加事件。现在再测试一下获取 <code>EventSetDescriptor</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">test1</span><span class="params">()</span> <span class="keyword">throws</span> IntrospectionException </span>&#123;</span><br><span class="line">    BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class, Object.class);</span><br><span class="line">    EventSetDescriptor[] eventSetDescriptors = userBeanInfo.getEventSetDescriptors();</span><br><span class="line">    Stream.of(eventSetDescriptors).forEach(System.out::println);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出结果：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">java.beans.EventSetDescriptor[name=propertyChange; inDefaultEventSet; listenerType=<span class="class"><span class="keyword">interface</span> <span class="title">java</span>.<span class="title">beans</span>.<span class="title">PropertyChangeListener</span></span>; getListenerMethod=<span class="keyword">public</span> java.beans.PropertyChangeListener[] introspector.bean.User.getPropertyChangeListeners(); addListenerMethod=<span class="keyword">public</span> <span class="keyword">void</span> introspector.bean.User.addPropertyChangeListener(java.beans.PropertyChangeListener); removeListenerMethod=<span class="keyword">public</span> <span class="keyword">void</span> introspector.bean.User.removePropertyChangeListener(java.beans.PropertyChangeListener)]</span><br><span class="line">java.beans.EventSetDescriptor[name=vetoableChange; inDefaultEventSet; listenerType=<span class="class"><span class="keyword">interface</span> <span class="title">java</span>.<span class="title">beans</span>.<span class="title">VetoableChangeListener</span></span>; addListenerMethod=<span class="keyword">public</span> <span class="keyword">void</span> introspector.bean.User.addVetoableChangeListener(java.beans.VetoableChangeListener); removeListenerMethod=<span class="keyword">public</span> <span class="keyword">void</span> introspector.bean.User.removeVetoableChangeListener(java.beans.VetoableChangeListener)]</span><br></pre></td></tr></table></figure><h2 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h2><p>在 Java 生态飞速发展的今天，很多底层技术细节都被高级框架所屏蔽，而 Java Beans 就是其中一种。也许平时根本就用不到，但是其代码设计和思想理念不应该被忽视。Dubbo 2.7 之后提出了“服务自省”的概念，其灵感就来源于 Java Beans 内省机制。</p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li><a href="https://zh.wikipedia.org/zh-hans/内省_(计算机科学" target="_blank" rel="noopener">https://zh.wikipedia.org/zh-hans/%E5%86%85%E7%9C%81_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)</a>)</li><li><a href="https://yq.aliyun.com/live/1944" target="_blank" rel="noopener">https://yq.aliyun.com/live/1944</a></li><li>《beans.101.pdf》</li></ul><hr><p><strong>作者</strong></p><p>魏民，信息部售后组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com（武汉）</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;小议-Java-内省机制&quot;&gt;&lt;a href=&quot;#小议-Java-内省机制&quot; class=&quot;headerlink&quot; title=&quot;小议 Java 内省机制&quot;&gt;&lt;/a&gt;小议 Java 内省机制&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;[作者简介]&lt;/strong&gt; 魏民，信息部
      
    
    </summary>
    
    
      <category term="java" scheme="https://xiaomi-info.github.io/tags/java/"/>
    
      <category term="introspector" scheme="https://xiaomi-info.github.io/tags/introspector/"/>
    
      <category term="beans" scheme="https://xiaomi-info.github.io/tags/beans/"/>
    
  </entry>
  
  <entry>
    <title>浅析 RPC 与基本实现</title>
    <link href="https://xiaomi-info.github.io/2020/03/02/rpc-achieve/"/>
    <id>https://xiaomi-info.github.io/2020/03/02/rpc-achieve/</id>
    <published>2020-03-02T07:18:20.000Z</published>
    <updated>2021-05-24T03:39:30.319Z</updated>
    
    <content type="html"><![CDATA[<h1 id="浅析-RPC-与基本实现"><a href="#浅析-RPC-与基本实现" class="headerlink" title="浅析 RPC 与基本实现"></a>浅析 RPC 与基本实现</h1><p><em><code>注：文中所用到的代码已上传至 github: https://github.com/fankongqiumu/storm.git</code></em></p><p><strong>[作者简介]</strong> 孙浩，信息部售后组研发工程师</p><h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>本文主要论述的是“RPC 实现原理”，那么首先明确一个问题什么是 RPC 呢？RPC 是 Remote Procedure Call 的缩写，即，远程过程调用。RPC 是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序，而开发人员无需额外地为这个交互编程。<br>值得注意是，两个或多个应用程序都分布在不同的服务器上，它们之间的调用都像是本地方法调用一样。接下来我们便来分析一下一次 RPC 调用发生了些什么？</p><h2 id="一次基本的-RPC-调用会涉及到什么？"><a href="#一次基本的-RPC-调用会涉及到什么？" class="headerlink" title="一次基本的 RPC 调用会涉及到什么？"></a>一次基本的 RPC 调用会涉及到什么？</h2><p>现在业界内比较流行的一些 RPC 框架，例如 Dubbo 提供的是<code>基于接口的远程方法调用</code>，即客户端只需要知道接口的定义即可调用远程服务。在 Java 中接口并不能直接调用实例方法，必须通过其实现类对象来完成此操作，这意味着客户端必须为这些接口生成<code>代理对象</code>，对此 Java 提供了 <code>Proxy</code>、<code>InvocationHandler</code> 生成动态代理的支持；生成了代理对象，那么每个具体的发方法是怎么调用的呢？jdk 动态代理生成的代理对象调用指定方法时实际会执行 <code>InvocationHandler</code> 中定义的 <code>#invoke</code> 方法，在该方法中完成远程方法调用并获取结果。</p><p>抛开客户端，回过头来看 RPC 是两台计算机间的调用，实质上是两台主机间的<code>网络通信</code>，涉及到网络通信又必然会有<code>序列化、反序列化</code>，<code>编解码</code>等一些必须要考虑的问题；同时实际上现在大多系统都是集群部署的，多台主机/容器对外提供相同的服务，如果集群的节点数量很大的话，那么管理服务地址也将是一件十分繁琐的事情，常见的做法是各个服务节点将自己的地址和提供的服务列表注册到一个 <code>注册中心</code>，由 <code>注册中心</code> 来统一管理服务列表；这样的做法解决了一些问题同时为客户端增加了一项新的工作——那就是<code>服务发现</code>，通俗来说就是从注册中心中找到远程方法对应的服务列表并通过某种策略从中选取一个服务地址来完成网络通信。</p><p>聊了客户端和 <code>注册中心</code>，另外一个重要的角色自然是服务端，服务端最重要的任务便是提供服务接口的真正实现并在某个端口上监听网络请求，监听到请求后从网络请求中获取到对应的参数（比如服务接口、方法、请求参数等），再根据这些参数通过<code>反射</code>的方式调用接口的真正实现获取结果并将其写入对应的响应流中。</p><p>综上所述，一次基本的 RPC 调用流程大致如下：<br><img src="/2020/03/02/rpc-achieve/timing-diagram.png"></p><h2 id="基本实现"><a href="#基本实现" class="headerlink" title="基本实现"></a>基本实现</h2><h3 id="服务端（生产者）"><a href="#服务端（生产者）" class="headerlink" title="服务端（生产者）"></a>服务端（生产者）</h3><ul><li>服务接口</li></ul><p>在 RPC 中，生产者和消费者有一个共同的服务接口 API。如下，定义一个 HelloService 接口。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> 孙浩</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Descrption</span>  服务接口</span></span><br><span class="line"><span class="comment"> ***/</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">interface</span> <span class="title">HelloService</span> </span>&#123;</span><br><span class="line">    <span class="function">String <span class="title">sayHello</span><span class="params">(String somebody)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>服务实现</li></ul><p>生产者要提供服务接口的实现，创建 HelloServiceImpl 实现类。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> 孙浩</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Descrption</span> 服务实现</span></span><br><span class="line"><span class="comment"> ***/</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">HelloServiceImpl</span> <span class="keyword">implements</span> <span class="title">HelloService</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> String <span class="title">sayHello</span><span class="params">(String somebody)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="string">"hello "</span> + somebody + <span class="string">"!"</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>服务注册</li></ul><p>本例使用 Spring 来管理 bean，采用自定义 xml 和解析器的方式来将服务实现类载入容器（当然也可以采用自定义注解的方式，此处不过多论述）并将服务接口信息注册到注册中心。<br>首先自定义<code>xsd</code>,</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">xsd:element</span> <span class="attr">name</span>=<span class="string">"service"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">xsd:complexType</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="name">xsd:complexContent</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="name">xsd:extension</span> <span class="attr">base</span>=<span class="string">"beans:identifiedType"</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">xsd:attribute</span> <span class="attr">name</span>=<span class="string">"interface"</span> <span class="attr">type</span>=<span class="string">"xsd:string"</span> <span class="attr">use</span>=<span class="string">"required"</span>/&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">xsd:attribute</span> <span class="attr">name</span>=<span class="string">"timeout"</span> <span class="attr">type</span>=<span class="string">"xsd:int"</span> <span class="attr">use</span>=<span class="string">"required"</span>/&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">xsd:attribute</span> <span class="attr">name</span>=<span class="string">"serverPort"</span> <span class="attr">type</span>=<span class="string">"xsd:int"</span> <span class="attr">use</span>=<span class="string">"required"</span>/&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">xsd:attribute</span> <span class="attr">name</span>=<span class="string">"ref"</span> <span class="attr">type</span>=<span class="string">"xsd:string"</span> <span class="attr">use</span>=<span class="string">"required"</span>/&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">xsd:attribute</span> <span class="attr">name</span>=<span class="string">"weight"</span> <span class="attr">type</span>=<span class="string">"xsd:int"</span> <span class="attr">use</span>=<span class="string">"optional"</span>/&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">xsd:attribute</span> <span class="attr">name</span>=<span class="string">"workerThreads"</span> <span class="attr">type</span>=<span class="string">"xsd:int"</span> <span class="attr">use</span>=<span class="string">"optional"</span>/&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">xsd:attribute</span> <span class="attr">name</span>=<span class="string">"appKey"</span> <span class="attr">type</span>=<span class="string">"xsd:string"</span> <span class="attr">use</span>=<span class="string">"required"</span>/&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="name">xsd:attribute</span> <span class="attr">name</span>=<span class="string">"groupName"</span> <span class="attr">type</span>=<span class="string">"xsd:string"</span> <span class="attr">use</span>=<span class="string">"optional"</span>/&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="name">xsd:extension</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="name">xsd:complexContent</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="name">xsd:complexType</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">xsd:element</span>&gt;</span></span><br></pre></td></tr></table></figure><p>分别指定 schema 和 xmd，schema 和对应 handler 的映射：</p><p><code>schema</code></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">http\://www.storm.com/schema/storm-service.xsd=META-INF/storm-service.xsd</span><br><span class="line">http\://www.storm.com/schema/storm-reference.xsd=META-INF/storm-reference.xsd</span><br></pre></td></tr></table></figure><p><code>handler</code></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">http\://www.storm.com/schema/storm-service=com.hsunfkqm.storm.framework.spring.StormServiceNamespaceHandler</span><br><span class="line">http\://www.storm.com/schema/storm-reference=com.hsunfkqm.storm.framework.spring.StormRemoteReferenceNamespaceHandler</span><br></pre></td></tr></table></figure><p>将编写好的文件放入 <code>classpath</code> 下的 <code>META-INF</code> 目录下：<br><img src="/2020/03/02/rpc-achieve/spring-cfg.png"></p><p>在 Spring 配置文件中配置服务类：</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">&lt;!-- 发布远程服务 --&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"helloService"</span> <span class="attr">class</span>=<span class="string">"com.hsunfkqm.storm.framework.test.HelloServiceImpl"</span>/&gt;</span></span><br><span class="line"> <span class="tag">&lt;<span class="name">storm:service</span> <span class="attr">id</span>=<span class="string">"helloServiceRegister"</span></span></span><br><span class="line"><span class="tag">                     <span class="attr">interface</span>=<span class="string">"com.hsunfkqm.storm.framework.test.HelloService"</span></span></span><br><span class="line"><span class="tag">                     <span class="attr">ref</span>=<span class="string">"helloService"</span></span></span><br><span class="line"><span class="tag">                     <span class="attr">groupName</span>=<span class="string">"default"</span></span></span><br><span class="line"><span class="tag">                     <span class="attr">weight</span>=<span class="string">"2"</span></span></span><br><span class="line"><span class="tag">                     <span class="attr">appKey</span>=<span class="string">"ares"</span></span></span><br><span class="line"><span class="tag">                     <span class="attr">workerThreads</span>=<span class="string">"100"</span></span></span><br><span class="line"><span class="tag">                     <span class="attr">serverPort</span>=<span class="string">"8081"</span></span></span><br><span class="line"><span class="tag">                     <span class="attr">timeout</span>=<span class="string">"600"</span>/&gt;</span></span><br></pre></td></tr></table></figure><p>编写对应的 Handler 和 Parser：<br><code>StormServiceNamespaceHandler</code></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.xml.NamespaceHandlerSupport;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> 孙浩</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Descrption</span> 服务发布自定义标签</span></span><br><span class="line"><span class="comment"> ***/</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">StormServiceNamespaceHandler</span> <span class="keyword">extends</span> <span class="title">NamespaceHandlerSupport</span> </span>&#123;</span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">init</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        registerBeanDefinitionParser(<span class="string">"service"</span>, <span class="keyword">new</span> ProviderFactoryBeanDefinitionParser());</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>ProviderFactoryBeanDefinitionParser</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">protected</span> Class <span class="title">getBeanClass</span><span class="params">(Element element)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> ProviderFactoryBean.class;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">doParse</span><span class="params">(Element element, BeanDefinitionBuilder bean)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            String serviceItf = element.getAttribute(<span class="string">"interface"</span>);</span><br><span class="line">            String serverPort = element.getAttribute(<span class="string">"serverPort"</span>);</span><br><span class="line">            String ref = element.getAttribute(<span class="string">"ref"</span>);</span><br><span class="line">            <span class="comment">// ....</span></span><br><span class="line">            bean.addPropertyValue(<span class="string">"serverPort"</span>, Integer.parseInt(serverPort));</span><br><span class="line">            bean.addPropertyValue(<span class="string">"serviceItf"</span>, Class.forName(serviceItf));</span><br><span class="line">            bean.addPropertyReference(<span class="string">"serviceObject"</span>, ref);</span><br><span class="line">            <span class="comment">//...</span></span><br><span class="line">            <span class="keyword">if</span> (NumberUtils.isNumber(weight)) &#123;</span><br><span class="line">                bean.addPropertyValue(<span class="string">"weight"</span>, Integer.parseInt(weight));</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">//...</span></span><br><span class="line">       &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="comment">// ...        </span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p><code>ProviderFactoryBean</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> 孙浩</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Descrption</span> 服务发布</span></span><br><span class="line"><span class="comment"> ***/</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ProviderFactoryBean</span> <span class="keyword">implements</span> <span class="title">FactoryBean</span>, <span class="title">InitializingBean</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//服务接口</span></span><br><span class="line">    <span class="keyword">private</span> Class&lt;?&gt; serviceItf;</span><br><span class="line">    <span class="comment">//服务实现</span></span><br><span class="line">    <span class="keyword">private</span> Object serviceObject;</span><br><span class="line">    <span class="comment">//服务端口</span></span><br><span class="line">    <span class="keyword">private</span> String serverPort;</span><br><span class="line">    <span class="comment">//服务超时时间</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">long</span> timeout;</span><br><span class="line">    <span class="comment">//服务代理对象，暂时没有用到</span></span><br><span class="line">    <span class="keyword">private</span> Object serviceProxyObject;</span><br><span class="line">    <span class="comment">//服务提供者唯一标识</span></span><br><span class="line">    <span class="keyword">private</span> String appKey;</span><br><span class="line">    <span class="comment">//服务分组组名</span></span><br><span class="line">    <span class="keyword">private</span> String groupName = <span class="string">"default"</span>;</span><br><span class="line">    <span class="comment">//服务提供者权重，默认为 1 , 范围为 [1-100]</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> weight = <span class="number">1</span>;</span><br><span class="line">    <span class="comment">//服务端线程数，默认 10 个线程</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> workerThreads = <span class="number">10</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Object <span class="title">getObject</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> serviceProxyObject;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">public</span> Class&lt;?&gt; getObjectType() &#123;</span><br><span class="line">        <span class="keyword">return</span> serviceItf;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">afterPropertiesSet</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="comment">//启动 Netty 服务端</span></span><br><span class="line">        NettyServer.singleton().start(Integer.parseInt(serverPort));</span><br><span class="line">        <span class="comment">//注册到 zk, 元数据注册中心</span></span><br><span class="line">        List&lt;ProviderService&gt; providerServiceList = buildProviderServiceInfos();</span><br><span class="line">        IRegisterCenter4Provider registerCenter4Provider = RegisterCenter.singleton();</span><br><span class="line">        registerCenter4Provider.registerProvider(providerServiceList);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//================RegisterCenter#registerProvider======================</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">registerProvider</span><span class="params">(<span class="keyword">final</span> List&lt;ProviderService&gt; serviceMetaData)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (CollectionUtils.isEmpty(serviceMetaData)) &#123;</span><br><span class="line">        <span class="keyword">return</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">//连接 zk, 注册服务</span></span><br><span class="line">    <span class="keyword">synchronized</span> (RegisterCenter.class) &#123;</span><br><span class="line">        <span class="keyword">for</span> (ProviderService provider : serviceMetaData) &#123;</span><br><span class="line">            String serviceItfKey = provider.getServiceItf().getName();</span><br><span class="line"></span><br><span class="line">            List&lt;ProviderService&gt; providers = providerServiceMap.get(serviceItfKey);</span><br><span class="line">            <span class="keyword">if</span> (providers == <span class="keyword">null</span>) &#123;</span><br><span class="line">                providers = Lists.newArrayList();</span><br><span class="line">            &#125;</span><br><span class="line">            providers.add(provider);</span><br><span class="line">            providerServiceMap.put(serviceItfKey, providers);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (zkClient == <span class="keyword">null</span>) &#123;</span><br><span class="line">            zkClient = <span class="keyword">new</span> ZkClient(ZK_SERVICE, ZK_SESSION_TIME_OUT, ZK_CONNECTION_TIME_OUT, <span class="keyword">new</span> SerializableSerializer());</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//创建 ZK 命名空间/当前部署应用 APP 命名空间/</span></span><br><span class="line">        String APP_KEY = serviceMetaData.get(<span class="number">0</span>).getAppKey();</span><br><span class="line">        String ZK_PATH = ROOT_PATH + <span class="string">"/"</span> + APP_KEY;</span><br><span class="line">        <span class="keyword">boolean</span> exist = zkClient.exists(ZK_PATH);</span><br><span class="line">        <span class="keyword">if</span> (!exist) &#123;</span><br><span class="line">            zkClient.createPersistent(ZK_PATH, <span class="keyword">true</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">for</span> (Map.Entry&lt;String, List&lt;ProviderService&gt;&gt; entry : providerServiceMap.entrySet()) &#123;</span><br><span class="line">            <span class="comment">//服务分组</span></span><br><span class="line">            String groupName = entry.getValue().get(<span class="number">0</span>).getGroupName();</span><br><span class="line">            <span class="comment">//创建服务提供者</span></span><br><span class="line">            String serviceNode = entry.getKey();</span><br><span class="line">            String servicePath = ZK_PATH + <span class="string">"/"</span> + groupName + <span class="string">"/"</span> + serviceNode + <span class="string">"/"</span> + PROVIDER_TYPE;</span><br><span class="line">            exist = zkClient.exists(servicePath);</span><br><span class="line">            <span class="keyword">if</span> (!exist) &#123;</span><br><span class="line">                zkClient.createPersistent(servicePath, <span class="keyword">true</span>);</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="comment">//创建当前服务器节点</span></span><br><span class="line">            <span class="keyword">int</span> serverPort = entry.getValue().get(<span class="number">0</span>).getServerPort();<span class="comment">//服务端口</span></span><br><span class="line">            <span class="keyword">int</span> weight = entry.getValue().get(<span class="number">0</span>).getWeight();<span class="comment">//服务权重</span></span><br><span class="line">            <span class="keyword">int</span> workerThreads = entry.getValue().get(<span class="number">0</span>).getWorkerThreads();<span class="comment">//服务工作线程</span></span><br><span class="line">            String localIp = IPHelper.localIp();</span><br><span class="line">            String currentServiceIpNode = servicePath + <span class="string">"/"</span> + localIp + <span class="string">"|"</span> + serverPort + <span class="string">"|"</span> + weight + <span class="string">"|"</span> + workerThreads + <span class="string">"|"</span> + groupName;</span><br><span class="line">            exist = zkClient.exists(currentServiceIpNode);</span><br><span class="line">            <span class="keyword">if</span> (!exist) &#123;</span><br><span class="line">                <span class="comment">//注意，这里创建的是临时节点</span></span><br><span class="line">                zkClient.createEphemeral(currentServiceIpNode);</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">//监听注册服务的变化，同时更新数据到本地缓存</span></span><br><span class="line">            zkClient.subscribeChildChanges(servicePath, <span class="keyword">new</span> IZkChildListener() &#123;</span><br><span class="line">                <span class="meta">@Override</span></span><br><span class="line">                <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handleChildChange</span><span class="params">(String parentPath, List&lt;String&gt; currentChilds)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">                    <span class="keyword">if</span> (currentChilds == <span class="keyword">null</span>) &#123;</span><br><span class="line">                        currentChilds = Lists.newArrayList();</span><br><span class="line">                    &#125;</span><br><span class="line">                    <span class="comment">//存活的服务 IP 列表</span></span><br><span class="line">                    List&lt;String&gt; activityServiceIpList = Lists.newArrayList(Lists.transform(currentChilds, <span class="keyword">new</span> Function&lt;String, String&gt;() &#123;</span><br><span class="line">                        <span class="meta">@Override</span></span><br><span class="line">                        <span class="function"><span class="keyword">public</span> String <span class="title">apply</span><span class="params">(String input)</span> </span>&#123;</span><br><span class="line">                            <span class="keyword">return</span> StringUtils.split(input, <span class="string">"|"</span>)[<span class="number">0</span>];</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;));</span><br><span class="line">                    refreshActivityService(activityServiceIpList);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;);</span><br><span class="line"></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>至此服务实现类已被载入 Spring 容器中，且服务接口信息也注册到了注册中心。</p><ul><li>网络通信</li></ul><p>作为生产者对外提供 RPC 服务，必须有一个网络程序来来监听请求和做出响应。在 Java 领域 Netty 是一款高性能的 NIO 通信框架，很多的框架的通信都是采用 Netty 来实现的，本例中也采用它当做通信服务器。</p><p>构建并启动 Netty 服务监听指定端口：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">start</span><span class="params">(<span class="keyword">final</span> <span class="keyword">int</span> port)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">synchronized</span> (NettyServer.class) &#123;</span><br><span class="line">            <span class="keyword">if</span> (bossGroup != <span class="keyword">null</span> || workerGroup != <span class="keyword">null</span>) &#123;</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            bossGroup = <span class="keyword">new</span> NioEventLoopGroup();</span><br><span class="line">            workerGroup = <span class="keyword">new</span> NioEventLoopGroup();</span><br><span class="line">            ServerBootstrap serverBootstrap = <span class="keyword">new</span> ServerBootstrap();</span><br><span class="line">            serverBootstrap</span><br><span class="line">                    .group(bossGroup, workerGroup)</span><br><span class="line">                    .channel(NioServerSocketChannel.class)</span><br><span class="line">                    .option(ChannelOption.SO_BACKLOG, <span class="number">1024</span>)</span><br><span class="line">                    .childOption(ChannelOption.SO_KEEPALIVE, <span class="keyword">true</span>)</span><br><span class="line">                    .childOption(ChannelOption.TCP_NODELAY, <span class="keyword">true</span>)</span><br><span class="line">                    .handler(<span class="keyword">new</span> LoggingHandler(LogLevel.INFO))</span><br><span class="line">                    .childHandler(<span class="keyword">new</span> ChannelInitializer&lt;SocketChannel&gt;() &#123;</span><br><span class="line">                        <span class="meta">@Override</span></span><br><span class="line">                        <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">initChannel</span><span class="params">(SocketChannel ch)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">                            <span class="comment">//注册解码器 NettyDecoderHandler</span></span><br><span class="line">                            ch.pipeline().addLast(<span class="keyword">new</span> NettyDecoderHandler(StormRequest.class, serializeType));</span><br><span class="line">                            <span class="comment">//注册编码器 NettyEncoderHandler</span></span><br><span class="line">                            ch.pipeline().addLast(<span class="keyword">new</span> NettyEncoderHandler(serializeType));</span><br><span class="line">                            <span class="comment">//注册服务端业务逻辑处理器 NettyServerInvokeHandler</span></span><br><span class="line">                            ch.pipeline().addLast(<span class="keyword">new</span> NettyServerInvokeHandler());</span><br><span class="line">                        &#125;</span><br><span class="line">                    &#125;);</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                channel = serverBootstrap.bind(port).sync().channel();</span><br><span class="line">            &#125; <span class="keyword">catch</span> (InterruptedException e) &#123;</span><br><span class="line">                <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(e);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><p>上面的代码中向 Netty 服务的 pipeline 中添加了编解码和业务处理器，当接收到请求时，经过编解码后，真正处理业务的是业务处理器，即<code>NettyServerInvokeHandler</code>, 该处理器继承自<code>SimpleChannelInboundHandler</code>, 当数据读取完成将触发一个事件，并调用<code>NettyServerInvokeHandler#channelRead0</code>方法来处理请求。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">channelRead0</span><span class="params">(ChannelHandlerContext ctx, StormRequest request)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (ctx.channel().isWritable()) &#123;</span><br><span class="line">        <span class="comment">//从服务调用对象里获取服务提供者信息</span></span><br><span class="line">        ProviderService metaDataModel = request.getProviderService();</span><br><span class="line">        <span class="keyword">long</span> consumeTimeOut = request.getInvokeTimeout();</span><br><span class="line">        <span class="keyword">final</span> String methodName = request.getInvokedMethodName();</span><br><span class="line"></span><br><span class="line">        <span class="comment">//根据方法名称定位到具体某一个服务提供者</span></span><br><span class="line">        String serviceKey = metaDataModel.getServiceItf().getName();</span><br><span class="line">        <span class="comment">//获取限流工具类</span></span><br><span class="line">        <span class="keyword">int</span> workerThread = metaDataModel.getWorkerThreads();</span><br><span class="line">        Semaphore semaphore = serviceKeySemaphoreMap.get(serviceKey);</span><br><span class="line">        <span class="keyword">if</span> (semaphore == <span class="keyword">null</span>) &#123;</span><br><span class="line">            <span class="keyword">synchronized</span> (serviceKeySemaphoreMap) &#123;</span><br><span class="line">                semaphore = serviceKeySemaphoreMap.get(serviceKey);</span><br><span class="line">                <span class="keyword">if</span> (semaphore == <span class="keyword">null</span>) &#123;</span><br><span class="line">                    semaphore = <span class="keyword">new</span> Semaphore(workerThread);</span><br><span class="line">                    serviceKeySemaphoreMap.put(serviceKey, semaphore);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">//获取注册中心服务</span></span><br><span class="line">        IRegisterCenter4Provider registerCenter4Provider = RegisterCenter.singleton();</span><br><span class="line">        List&lt;ProviderService&gt; localProviderCaches = registerCenter4Provider.getProviderServiceMap().get(serviceKey);</span><br><span class="line"></span><br><span class="line">        Object result = <span class="keyword">null</span>;</span><br><span class="line">        <span class="keyword">boolean</span> acquire = <span class="keyword">false</span>;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            ProviderService localProviderCache = Collections2.filter(localProviderCaches, <span class="keyword">new</span> Predicate&lt;ProviderService&gt;() &#123;</span><br><span class="line">                <span class="meta">@Override</span></span><br><span class="line">                <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">apply</span><span class="params">(ProviderService input)</span> </span>&#123;</span><br><span class="line">                    <span class="keyword">return</span> StringUtils.equals(input.getServiceMethod().getName(), methodName);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;).iterator().next();</span><br><span class="line">            Object serviceObject = localProviderCache.getServiceObject();</span><br><span class="line"></span><br><span class="line">            <span class="comment">//利用反射发起服务调用</span></span><br><span class="line">            Method method = localProviderCache.getServiceMethod();</span><br><span class="line">            <span class="comment">//利用 semaphore 实现限流</span></span><br><span class="line">            acquire = semaphore.tryAcquire(consumeTimeOut, TimeUnit.MILLISECONDS);</span><br><span class="line">            <span class="keyword">if</span> (acquire) &#123;</span><br><span class="line">                result = method.invoke(serviceObject, request.getArgs());</span><br><span class="line">                <span class="comment">//System.out.println("---------------"+result);</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            System.out.println(JSON.toJSONString(localProviderCaches) + <span class="string">"  "</span> + methodName+<span class="string">" "</span>+e.getMessage());</span><br><span class="line">            result = e;</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="keyword">if</span> (acquire) &#123;</span><br><span class="line">                semaphore.release();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">//根据服务调用结果组装调用返回对象</span></span><br><span class="line">        StormResponse response = <span class="keyword">new</span> StormResponse();</span><br><span class="line">        response.setInvokeTimeout(consumeTimeOut);</span><br><span class="line">        response.setUniqueKey(request.getUniqueKey());</span><br><span class="line">        response.setResult(result);</span><br><span class="line">        <span class="comment">//将服务调用返回对象回写到消费端</span></span><br><span class="line">        ctx.writeAndFlush(response);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        logger.error(<span class="string">"------------channel closed!---------------"</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此处还有部分细节如自定义的编解码器等，篇幅所限不在此详述，继承 <code>MessageToByteEncoder</code> 和 <code>ByteToMessageDecoder</code> 覆写对应的 <code>encode</code> 和 <code>decode</code> 方法即可自定义编解码器，使用到的序列化工具如 Hessian/Proto 等可参考对应的官方文档。</p><ul><li>请求和响应包装<br>为便于封装请求和响应，定义两个 bean 来表示请求和响应。</li></ul><p>请求：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> 孙浩</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Descrption</span></span></span><br><span class="line"><span class="comment"> ***/</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">StormRequest</span> <span class="keyword">implements</span> <span class="title">Serializable</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">long</span> serialVersionUID = -<span class="number">5196465012408804755L</span>;</span><br><span class="line">    <span class="comment">//UUID, 唯一标识一次返回值</span></span><br><span class="line">    <span class="keyword">private</span> String uniqueKey;</span><br><span class="line">    <span class="comment">//服务提供者信息</span></span><br><span class="line">    <span class="keyword">private</span> ProviderService providerService;</span><br><span class="line">    <span class="comment">//调用的方法名称</span></span><br><span class="line">    <span class="keyword">private</span> String invokedMethodName;</span><br><span class="line">    <span class="comment">//传递参数</span></span><br><span class="line">    <span class="keyword">private</span> Object[] args;</span><br><span class="line">    <span class="comment">//消费端应用名</span></span><br><span class="line">    <span class="keyword">private</span> String appName;</span><br><span class="line">    <span class="comment">//消费请求超时时长</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">long</span> invokeTimeout;</span><br><span class="line">    <span class="comment">// getter/setter</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>响应：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> 孙浩</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Descrption</span></span></span><br><span class="line"><span class="comment"> ***/</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">StormResponse</span> <span class="keyword">implements</span> <span class="title">Serializable</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">long</span> serialVersionUID = <span class="number">5785265307118147202L</span>;</span><br><span class="line">    <span class="comment">//UUID, 唯一标识一次返回值</span></span><br><span class="line">    <span class="keyword">private</span> String uniqueKey;</span><br><span class="line">    <span class="comment">//客户端指定的服务超时时间</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">long</span> invokeTimeout;</span><br><span class="line">    <span class="comment">//接口调用返回的结果对象</span></span><br><span class="line">    <span class="keyword">private</span> Object result;</span><br><span class="line">    <span class="comment">//getter/setter</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="客户端（消费者）"><a href="#客户端（消费者）" class="headerlink" title="客户端（消费者）"></a>客户端（消费者）</h3><p>客户端（消费者）在 RPC 调用中主要是生成服务接口的代理对象，并从注册中心获取对应的服务列表发起网络请求。<br>客户端和服务端一样采用 Spring 来管理 bean 解析 xml 配置等不再赘述，重点看下以下几点：</p><ul><li>通过 jdk 动态代理来生成引入服务接口的代理对象</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Object <span class="title">getProxy</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), <span class="keyword">new</span> Class&lt;?&gt;[]&#123;targetInterface&#125;, <span class="keyword">this</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>从注册中心获取服务列表并依据某种策略选取其中一个服务节点</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//服务接口名称</span></span><br><span class="line">String serviceKey = targetInterface.getName();</span><br><span class="line"><span class="comment">//获取某个接口的服务提供者列表</span></span><br><span class="line">IRegisterCenter4Invoker registerCenter4Consumer = RegisterCenter.singleton();</span><br><span class="line">List&lt;ProviderService&gt; providerServices = registerCenter4Consumer.getServiceMetaDataMap4Consume().get(serviceKey);</span><br><span class="line"><span class="comment">//根据软负载策略，从服务提供者列表选取本次调用的服务提供者</span></span><br><span class="line">ClusterStrategy clusterStrategyService = ClusterEngine.queryClusterStrategy(clusterStrategy);</span><br><span class="line">ProviderService providerService = clusterStrategyService.select(providerServices);</span><br></pre></td></tr></table></figure><ul><li>通过 Netty 建立连接，发起网络请求</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> 孙浩</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Descrption</span> Netty 消费端 bean 代理工厂</span></span><br><span class="line"><span class="comment"> ***/</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">RevokerProxyBeanFactory</span> <span class="keyword">implements</span> <span class="title">InvocationHandler</span> </span>&#123;</span><br><span class="line">    <span class="keyword">private</span> ExecutorService fixedThreadPool = <span class="keyword">null</span>;</span><br><span class="line">    <span class="comment">//服务接口</span></span><br><span class="line">    <span class="keyword">private</span> Class&lt;?&gt; targetInterface;</span><br><span class="line">    <span class="comment">//超时时间</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">int</span> consumeTimeout;</span><br><span class="line">    <span class="comment">//调用者线程数</span></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">int</span> threadWorkerNumber = <span class="number">10</span>;</span><br><span class="line">    <span class="comment">//负载均衡策略</span></span><br><span class="line">    <span class="keyword">private</span> String clusterStrategy;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> Object <span class="title">invoke</span><span class="params">(Object proxy, Method method, Object[] args)</span> <span class="keyword">throws</span> Throwable </span>&#123;</span><br><span class="line"></span><br><span class="line">        ...</span><br><span class="line"></span><br><span class="line">        <span class="comment">//复制一份服务提供者信息</span></span><br><span class="line">        ProviderService newProvider = providerService.copy();</span><br><span class="line">        <span class="comment">//设置本次调用服务的方法以及接口</span></span><br><span class="line">        newProvider.setServiceMethod(method);</span><br><span class="line">        newProvider.setServiceItf(targetInterface);</span><br><span class="line"></span><br><span class="line">        <span class="comment">//声明调用 AresRequest 对象，AresRequest 表示发起一次调用所包含的信息</span></span><br><span class="line">        <span class="keyword">final</span> StormRequest request = <span class="keyword">new</span> StormRequest();</span><br><span class="line">        <span class="comment">//设置本次调用的唯一标识</span></span><br><span class="line">        request.setUniqueKey(UUID.randomUUID().toString() + <span class="string">"-"</span> + Thread.currentThread().getId());</span><br><span class="line">        <span class="comment">//设置本次调用的服务提供者信息</span></span><br><span class="line">        request.setProviderService(newProvider);</span><br><span class="line">        <span class="comment">//设置本次调用的方法名称</span></span><br><span class="line">        request.setInvokedMethodName(method.getName());</span><br><span class="line">        <span class="comment">//设置本次调用的方法参数信息</span></span><br><span class="line">        request.setArgs(args);</span><br><span class="line"></span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">//构建用来发起调用的线程池</span></span><br><span class="line">            <span class="keyword">if</span> (fixedThreadPool == <span class="keyword">null</span>) &#123;</span><br><span class="line">                <span class="keyword">synchronized</span> (RevokerProxyBeanFactory.class) &#123;</span><br><span class="line">                    <span class="keyword">if</span> (<span class="keyword">null</span> == fixedThreadPool) &#123;</span><br><span class="line">                        fixedThreadPool = Executors.newFixedThreadPool(threadWorkerNumber);</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="comment">//根据服务提供者的 ip,port, 构建 InetSocketAddress 对象，标识服务提供者地址</span></span><br><span class="line">            String serverIp = request.getProviderService().getServerIp();</span><br><span class="line">            <span class="keyword">int</span> serverPort = request.getProviderService().getServerPort();</span><br><span class="line">            InetSocketAddress inetSocketAddress = <span class="keyword">new</span> InetSocketAddress(serverIp, serverPort);</span><br><span class="line">            <span class="comment">//提交本次调用信息到线程池 fixedThreadPool, 发起调用</span></span><br><span class="line">            Future&lt;StormResponse&gt; responseFuture = fixedThreadPool.submit(RevokerServiceCallable.of(inetSocketAddress, request));</span><br><span class="line">            <span class="comment">//获取调用的返回结果</span></span><br><span class="line">            StormResponse response = responseFuture.get(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);</span><br><span class="line">            <span class="keyword">if</span> (response != <span class="keyword">null</span>) &#123;</span><br><span class="line">                <span class="keyword">return</span> response.getResult();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(e);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//  ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Netty 的响应是异步的，为了在方法调用返回前获取到响应结果，需要将异步的结果同步化。</p><ul><li>Netty 异步返回的结果存入阻塞队列</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">channelRead0</span><span class="params">(ChannelHandlerContext channelHandlerContext, StormResponse response)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    <span class="comment">//将 Netty 异步返回的结果存入阻塞队列，以便调用端同步获取</span></span><br><span class="line">    RevokerResponseHolder.putResultValue(response);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>请求发出后同步获取结果</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//提交本次调用信息到线程池 fixedThreadPool, 发起调用</span></span><br><span class="line">Future&lt;StormResponse&gt; responseFuture = fixedThreadPool.submit(RevokerServiceCallable.of(inetSocketAddress, request));</span><br><span class="line"><span class="comment">//获取调用的返回结果</span></span><br><span class="line">StormResponse response = responseFuture.get(request.getInvokeTimeout(), TimeUnit.MILLISECONDS);</span><br><span class="line"><span class="keyword">if</span> (response != <span class="keyword">null</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> response.getResult();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">//===================================================</span></span><br><span class="line"><span class="comment">//从返回结果容器中获取返回结果，同时设置等待超时时间为 invokeTimeout</span></span><br><span class="line"><span class="keyword">long</span> invokeTimeout = request.getInvokeTimeout();</span><br><span class="line">StormResponse response = RevokerResponseHolder.getValue(request.getUniqueKey(), invokeTimeout);</span><br></pre></td></tr></table></figure><h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2><p><code>Server</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> 孙浩</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@Descrption</span></span></span><br><span class="line"><span class="comment"> ***/</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">MainServer</span> </span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="comment">//发布服务</span></span><br><span class="line">        <span class="keyword">final</span> ClassPathXmlApplicationContext context = <span class="keyword">new</span> ClassPathXmlApplicationContext(<span class="string">"storm-server.xml"</span>);</span><br><span class="line">        System.out.println(<span class="string">" 服务发布完成"</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>Client</code>：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Client</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> Logger logger = LoggerFactory.getLogger(Client.class);</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">final</span> ClassPathXmlApplicationContext context = <span class="keyword">new</span> ClassPathXmlApplicationContext(<span class="string">"storm-client.xml"</span>);</span><br><span class="line">        <span class="keyword">final</span> HelloService helloService = (HelloService) context.getBean(<span class="string">"helloService"</span>);</span><br><span class="line">        String result = helloService.sayHello(<span class="string">"World"</span>);</span><br><span class="line">        System.out.println(result);</span><br><span class="line">        <span class="keyword">for</span> (;;) &#123;</span><br><span class="line"></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="结果"><a href="#结果" class="headerlink" title="结果"></a>结果</h3><p>生产者：<br><img src="/2020/03/02/rpc-achieve/provider.png"></p><p>消费者：<br><img src="/2020/03/02/rpc-achieve/consumer.png"></p><p>注册中心<br><img src="/2020/03/02/rpc-achieve/register-center.png"></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文简单介绍了 RPC 的整个流程，并实现了一个简单的 RPC 调用。希望阅读完本文之后，能加深你对 RPC 的一些认识。</p><ul><li><p>生产者端流程：</p><ul><li>加载服务接口，并缓存</li><li>服务注册，将服务接口以及服务主机信息写入注册中心（本例使用的是 zookeeper)</li><li>启动网络服务器并监听</li><li>反射，本地调用</li></ul></li><li><p>消费者端流程：</p><ul><li>代理服务接口生成代理对象</li><li>服务发现（连接 zookeeper，拿到服务地址列表，通过客户端负载策略获取合适的服务地址）</li><li>远程方法调用（本例通过 Netty，发送消息，并获取响应结果）</li></ul></li></ul><p>限于篇幅，本文代码并不完整，如有需要，访问：<a href="https://github.com/fankongqiumu/storm.git" target="_blank" rel="noopener">https://github.com/fankongqiumu/storm.git</a>，获取完整代码。</p><p>如有错误之处，还望大家指正。</p><hr><p><strong>作者</strong></p><p>孙浩，小米信息技术部售后组</p><p><strong>关于 storm</strong></p><p>此<code>storm</code>非彼<code>storm</code>, 文中所示代码是一个基础的 RPC 框架的 demo，还在完善中，欢迎有兴趣的童鞋参与进来。</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com（武汉）</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;浅析-RPC-与基本实现&quot;&gt;&lt;a href=&quot;#浅析-RPC-与基本实现&quot; class=&quot;headerlink&quot; title=&quot;浅析 RPC 与基本实现&quot;&gt;&lt;/a&gt;浅析 RPC 与基本实现&lt;/h1&gt;&lt;p&gt;&lt;em&gt;&lt;code&gt;注：文中所用到的代码已上传至 githu
      
    
    </summary>
    
    
      <category term="Java" scheme="https://xiaomi-info.github.io/tags/Java/"/>
    
      <category term="RPC" scheme="https://xiaomi-info.github.io/tags/RPC/"/>
    
      <category term="Spring" scheme="https://xiaomi-info.github.io/tags/Spring/"/>
    
  </entry>
  
  <entry>
    <title>Go 语言踩坑记——panic 与 recover</title>
    <link href="https://xiaomi-info.github.io/2020/01/20/go-trample-panic-recover/"/>
    <id>https://xiaomi-info.github.io/2020/01/20/go-trample-panic-recover/</id>
    <published>2020-01-20T06:35:58.000Z</published>
    <updated>2021-05-24T03:39:30.234Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Go-语言踩坑记——panic-与-recover"><a href="#Go-语言踩坑记——panic-与-recover" class="headerlink" title="Go 语言踩坑记——panic 与 recover"></a>Go 语言踩坑记——panic 与 recover</h1><p><strong>[作者简介]</strong> 易乐天，小米信息技术部海外商城组</p><h2 id="题记"><a href="#题记" class="headerlink" title="题记"></a>题记</h2><p>Go 语言自发布以来，一直以高性能、高并发著称。因为标准库提供了 http 包，即使刚学不久的程序员，也能轻松写出 http 服务程序。</p><p>不过，任何事情都有两面性。一门语言，有它值得骄傲的有点，也必定隐藏了不少坑。新手若不知道这些坑，很容易就会掉进坑里。《 Go 语言踩坑记》系列博文将以 Go 语言中的 <code>panic</code> 与 <code>recover</code> 开头，给大家介绍笔者踩过的各种坑，以及填坑方法。</p><h2 id="初识-panic-和-recover"><a href="#初识-panic-和-recover" class="headerlink" title="初识 panic 和 recover"></a>初识 panic 和 recover</h2><ul><li><code>panic</code><br><code>panic</code> 这个词，在英语中具有<code>恐慌、恐慌的</code>等意思。从字面意思理解的话，在 Go 语言中，代表极其严重的问题，程序员最害怕出现的问题。一旦出现，就意味着程序的结束并退出。Go 语言中 <code>panic</code> 关键字主要用于主动抛出异常，类似 <code>java</code> 等语言中的 <code>throw</code> 关键字。</li><li><code>recover</code><br><code>recover</code> 这个词，在英语中具有<code>恢复、复原</code>等意思。从字面意思理解的话，在 Go 语言中，代表将程序状态从严重的错误中恢复到正常状态。Go 语言中 <code>recover</code> 关键字主要用于捕获异常，让程序回到正常状态，类似 <code>java</code> 等语言中的 <code>try ... catch</code> 。</li></ul><p>笔者有过 6 年 linux 系统 C 语言开发经历。C 语言中没有异常捕获的概念，没有 <code>try ... catch</code> ，也没有 <code>panic</code> 和 <code>recover</code> 。不过，万变不离其宗，异常与 <code>if error then return</code> 方式的差别，主要体现在函数调用栈的深度上。如下图：</p><img src="/2020/01/20/go-trample-panic-recover/longjump.png"><p>正常逻辑下的函数调用栈，是逐个回溯的，而异常捕获可以理解为：程序调用栈的长距离跳转。这点在 C 语言里，是通过 <code>setjump</code> 和 <code>longjump</code> 这两个函数来实现的。</p><p><code>try catch</code> 、 <code>recover</code> 、<code>setjump</code> 等机制会将程序当前状态（主要是 cpu 的栈指针寄存器 sp 和程序计数器 pc ， Go 的 <code>recover</code> 是依赖 <code>defer</code> 来维护 sp 和 pc ）保存到一个与 <code>throw</code>、<code>panic</code>、<code>longjump</code>共享的内存里。当有异常的时候，从该内存中提取之前保存的 sp 和 pc 寄存器值，直接将函数栈调回到 sp 指向的位置，并执行 ip 寄存器指向的下一条指令，将程序从异常状态中恢复到正常状态。</p><h2 id="深入-panic-和-recover"><a href="#深入-panic-和-recover" class="headerlink" title="深入 panic 和 recover"></a>深入 panic 和 recover</h2><h3 id="源码"><a href="#源码" class="headerlink" title="源码"></a>源码</h3><p><code>panic</code> 和 <code>recover</code> 的源码在 Go 源码的 <code>src/runtime/panic.go</code> 里，名为 <code>gopanic</code> 和 <code>gorecover</code> 的函数。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// gopanic 的代码，在 src/runtime/panic.go 第 454 行</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// The implementation of the predeclared function panic.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">gopanic</span><span class="params">(e <span class="keyword">interface</span>&#123;&#125;)</span></span> &#123;</span><br><span class="line">  gp := getg()</span><br><span class="line">  <span class="keyword">if</span> gp.m.curg != gp &#123;</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">"panic: "</span>)</span><br><span class="line">    printany(e)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">"\n"</span>)</span><br><span class="line">    throw(<span class="string">"panic on system stack"</span>)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">if</span> gp.m.mallocing != <span class="number">0</span> &#123;</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">"panic: "</span>)</span><br><span class="line">    printany(e)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">"\n"</span>)</span><br><span class="line">    throw(<span class="string">"panic during malloc"</span>)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> gp.m.preemptoff != <span class="string">""</span> &#123;</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">"panic: "</span>)</span><br><span class="line">    printany(e)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">"\n"</span>)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">"preempt off reason: "</span>)</span><br><span class="line">    <span class="built_in">print</span>(gp.m.preemptoff)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">"\n"</span>)</span><br><span class="line">    throw(<span class="string">"panic during preemptoff"</span>)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">if</span> gp.m.locks != <span class="number">0</span> &#123;</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">"panic: "</span>)</span><br><span class="line">    printany(e)</span><br><span class="line">    <span class="built_in">print</span>(<span class="string">"\n"</span>)</span><br><span class="line">    throw(<span class="string">"panic holding locks"</span>)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">var</span> p _panic</span><br><span class="line">  p.arg = e</span><br><span class="line">  p.link = gp._panic</span><br><span class="line">  gp._panic = (*_panic)(noescape(unsafe.Pointer(&amp;p)))</span><br><span class="line"></span><br><span class="line">  atomic.Xadd(&amp;runningPanicDefers, <span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">  <span class="keyword">for</span> &#123;</span><br><span class="line">    d := gp._defer</span><br><span class="line">    <span class="keyword">if</span> d == <span class="literal">nil</span> &#123;</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),</span></span><br><span class="line">    <span class="comment">// take defer off list. The earlier panic or Goexit will not continue running.</span></span><br><span class="line">    <span class="keyword">if</span> d.started &#123;</span><br><span class="line">      <span class="keyword">if</span> d._panic != <span class="literal">nil</span> &#123;</span><br><span class="line">        d._panic.aborted = <span class="literal">true</span></span><br><span class="line">      &#125;</span><br><span class="line">      d._panic = <span class="literal">nil</span></span><br><span class="line">      d.fn = <span class="literal">nil</span></span><br><span class="line">      gp._defer = d.link</span><br><span class="line">      freedefer(d)</span><br><span class="line">      <span class="keyword">continue</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Mark defer as started, but keep on list, so that traceback</span></span><br><span class="line">    <span class="comment">// can find and update the defer's argument frame if stack growth</span></span><br><span class="line">    <span class="comment">// or a garbage collection happens before reflectcall starts executing d.fn.</span></span><br><span class="line">    d.started = <span class="literal">true</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// Record the panic that is running the defer.</span></span><br><span class="line">    <span class="comment">// If there is a new panic during the deferred call, that panic</span></span><br><span class="line">    <span class="comment">// will find d in the list and will mark d._panic (this panic) aborted.</span></span><br><span class="line">    d._panic = (*_panic)(noescape(unsafe.Pointer(&amp;p)))</span><br><span class="line"></span><br><span class="line">    p.argp = unsafe.Pointer(getargp(<span class="number">0</span>))</span><br><span class="line">    reflectcall(<span class="literal">nil</span>, unsafe.Pointer(d.fn), deferArgs(d), <span class="keyword">uint32</span>(d.siz), <span class="keyword">uint32</span>(d.siz))</span><br><span class="line">    p.argp = <span class="literal">nil</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// reflectcall did not panic. Remove d.</span></span><br><span class="line">    <span class="keyword">if</span> gp._defer != d &#123;</span><br><span class="line">      throw(<span class="string">"bad defer entry in panic"</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    d._panic = <span class="literal">nil</span></span><br><span class="line">    d.fn = <span class="literal">nil</span></span><br><span class="line">    gp._defer = d.link</span><br><span class="line"></span><br><span class="line">    <span class="comment">// trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic</span></span><br><span class="line">    <span class="comment">//GC()</span></span><br><span class="line"></span><br><span class="line">    pc := d.pc</span><br><span class="line">    sp := unsafe.Pointer(d.sp) <span class="comment">// must be pointer so it gets adjusted during stack copy</span></span><br><span class="line">    freedefer(d)</span><br><span class="line">    <span class="keyword">if</span> p.recovered &#123;</span><br><span class="line">      atomic.Xadd(&amp;runningPanicDefers, <span class="number">-1</span>)</span><br><span class="line"></span><br><span class="line">      gp._panic = p.link</span><br><span class="line">      <span class="comment">// Aborted panics are marked but remain on the g.panic list.</span></span><br><span class="line">      <span class="comment">// Remove them from the list.</span></span><br><span class="line">      <span class="keyword">for</span> gp._panic != <span class="literal">nil</span> &amp;&amp; gp._panic.aborted &#123;</span><br><span class="line">        gp._panic = gp._panic.link</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> gp._panic == <span class="literal">nil</span> &#123; <span class="comment">// must be done with signal</span></span><br><span class="line">        gp.sig = <span class="number">0</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="comment">// Pass information about recovering frame to recovery.</span></span><br><span class="line">      gp.sigcode0 = <span class="keyword">uintptr</span>(sp)</span><br><span class="line">      gp.sigcode1 = pc</span><br><span class="line">      mcall(recovery)</span><br><span class="line">      throw(<span class="string">"recovery failed"</span>) <span class="comment">// mcall should not return</span></span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// ran out of deferred calls - old-school panic now</span></span><br><span class="line">  <span class="comment">// Because it is unsafe to call arbitrary user code after freezing</span></span><br><span class="line">  <span class="comment">// the world, we call preprintpanics to invoke all necessary Error</span></span><br><span class="line">  <span class="comment">// and String methods to prepare the panic strings before startpanic.</span></span><br><span class="line">  preprintpanics(gp._panic)</span><br><span class="line"></span><br><span class="line">  fatalpanic(gp._panic) <span class="comment">// should not return</span></span><br><span class="line">  *(*<span class="keyword">int</span>)(<span class="literal">nil</span>) = <span class="number">0</span>      <span class="comment">// not reached</span></span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure>  <figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// gorecover 的代码，在 src/runtime/panic.go 第 585 行</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// The implementation of the predeclared function recover.</span></span><br><span class="line"><span class="comment">// Cannot split the stack because it needs to reliably</span></span><br><span class="line"><span class="comment">// find the stack segment of its caller.</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// TODO(rsc): Once we commit to CopyStackAlways,</span></span><br><span class="line"><span class="comment">// this doesn't need to be nosplit.</span></span><br><span class="line"><span class="comment">//go:nosplit</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">gorecover</span><span class="params">(argp <span class="keyword">uintptr</span>)</span> <span class="title">interface</span></span>&#123;&#125; &#123;</span><br><span class="line"><span class="comment">// Must be in a function running as part of a deferred call during the panic.</span></span><br><span class="line"><span class="comment">// Must be called from the topmost function of the call</span></span><br><span class="line"><span class="comment">// (the function used in the defer statement).</span></span><br><span class="line"><span class="comment">// p.argp is the argument pointer of that topmost deferred function call.</span></span><br><span class="line"><span class="comment">// Compare against argp reported by caller.</span></span><br><span class="line"><span class="comment">// If they match, the caller is the one who can recover.</span></span><br><span class="line">gp := getg()</span><br><span class="line">p := gp._panic</span><br><span class="line"><span class="keyword">if</span> p != <span class="literal">nil</span> &amp;&amp; !p.recovered &amp;&amp; argp == <span class="keyword">uintptr</span>(p.argp) &#123;</span><br><span class="line">  p.recovered = <span class="literal">true</span></span><br><span class="line">  <span class="keyword">return</span> p.arg</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从函数代码中我们可以看到 <code>panic</code> 内部主要流程是这样：</p><ul><li>获取当前调用者所在的 <code>g</code> ，也就是 <code>goroutine</code></li><li>遍历并执行 <code>g</code> 中的 <code>defer</code> 函数</li><li>如果 <code>defer</code> 函数中有调用 <code>recover</code> ，并发现已经发生了 <code>panic</code> ，则将 <code>panic</code> 标记为 <code>recovered</code></li><li>在遍历 <code>defer</code> 的过程中，如果发现已经被标记为 <code>recovered</code> ，则提取出该 <code>defer</code> 的 sp 与 pc，保存在 <code>g</code> 的两个状态码字段中。</li><li><p>调用 <code>runtime.mcall</code> 切到 <code>m-&gt;g0</code> 并跳转到 <code>recovery</code> 函数，将前面获取的 <code>g</code> 作为参数传给 <code>recovery</code> 函数。<br><code>runtime.mcall</code> 的代码在 go 源码的 <code>src/runtime/asm_xxx.s</code> 中，<code>xxx</code> 是平台类型，如 <code>amd64</code> 。代码如下：</p>  <figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line">// src/runtime/asm_amd64.s 第 274 行</span><br><span class="line"></span><br><span class="line">// func mcall(fn func(*g))</span><br><span class="line">// Switch to m-&gt;g0&apos;s stack, call fn(g).</span><br><span class="line">// Fn must never return. It should gogo(&amp;g-&gt;sched)</span><br><span class="line">// to keep running g.</span><br><span class="line">TEXT runtime·mcall(SB), NOSPLIT, $0-8</span><br><span class="line">    MOVQfn+0(FP), DI</span><br><span class="line"></span><br><span class="line">    get_tls(CX)</span><br><span class="line">    MOVQg(CX), AX// save state in g-&gt;sched</span><br><span class="line">    MOVQ0(SP), BX// caller&apos;s PC</span><br><span class="line">    MOVQBX, (g_sched+gobuf_pc)(AX)</span><br><span class="line">    LEAQfn+0(FP), BX// caller&apos;s SP</span><br><span class="line">    MOVQBX, (g_sched+gobuf_sp)(AX)</span><br><span class="line">    MOVQAX, (g_sched+gobuf_g)(AX)</span><br><span class="line">    MOVQBP, (g_sched+gobuf_bp)(AX)</span><br><span class="line"></span><br><span class="line">    // switch to m-&gt;g0 &amp; its stack, call fn</span><br><span class="line">    MOVQg(CX), BX</span><br><span class="line">    MOVQg_m(BX), BX</span><br><span class="line">    MOVQm_g0(BX), SI</span><br><span class="line">    CMPQSI, AX// if g == m-&gt;g0 call badmcall</span><br><span class="line">    JNE3(PC)</span><br><span class="line">    MOVQ$runtime·badmcall(SB), AX</span><br><span class="line">    JMPAX</span><br><span class="line">    MOVQSI, g(CX)// g = m-&gt;g0</span><br><span class="line">    MOVQ(g_sched+gobuf_sp)(SI), SP// sp = m-&gt;g0-&gt;sched.sp</span><br><span class="line">    PUSHQAX</span><br><span class="line">    MOVQDI, DX</span><br><span class="line">    MOVQ0(DI), DI</span><br><span class="line">    CALLDI</span><br><span class="line">    POPQAX</span><br><span class="line">    MOVQ$runtime·badmcall2(SB), AX</span><br><span class="line">    JMPAX</span><br><span class="line">    RET</span><br></pre></td></tr></table></figure><p>　　这里之所以要切到 <code>m-&gt;g0</code> ，主要是因为 Go 的 <code>runtime</code> 环境是有自己的堆栈和 <code>goroutine</code>，而 <code>recovery</code> 是在 <code>runtime</code> 环境下执行的，所以要先调度到 <code>m-&gt;g0</code> 来执行 <code>recovery</code> 函数。</p></li><li><p><code>recovery</code> 函数中，利用 <code>g</code> 中的两个状态码回溯栈指针 sp 并恢复程序计数器 pc 到调度器中，并调用 <code>gogo</code> 重新调度 <code>g</code> ，将 <code>g</code> 恢复到调用 <code>recover</code> 函数的位置， goroutine 继续执行。<br>代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// gorecover 的代码，在 src/runtime/panic.go 第 637 行</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Unwind the stack after a deferred function calls recover</span></span><br><span class="line"><span class="comment">// after a panic. Then arrange to continue running as though</span></span><br><span class="line"><span class="comment">// the caller of the deferred function returned normally.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">recovery</span><span class="params">(gp *g)</span></span> &#123;</span><br><span class="line">    <span class="comment">// Info about defer passed in G struct.</span></span><br><span class="line">    sp := gp.sigcode0</span><br><span class="line">    pc := gp.sigcode1</span><br><span class="line"></span><br><span class="line">    <span class="comment">// d's arguments need to be in the stack.</span></span><br><span class="line">    <span class="keyword">if</span> sp != <span class="number">0</span> &amp;&amp; (sp &lt; gp.stack.lo || gp.stack.hi &lt; sp) &#123;</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">"recover: "</span>, hex(sp), <span class="string">" not in ["</span>, hex(gp.stack.lo), <span class="string">", "</span>, hex(gp.stack.hi), <span class="string">"]\n"</span>)</span><br><span class="line">        throw(<span class="string">"bad recovery"</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Make the deferproc for this d return again,</span></span><br><span class="line">    <span class="comment">// this time returning 1.  The calling function will</span></span><br><span class="line">    <span class="comment">// jump to the standard return epilogue.</span></span><br><span class="line">    gp.sched.sp = sp</span><br><span class="line">    gp.sched.pc = pc</span><br><span class="line">    gp.sched.lr = <span class="number">0</span></span><br><span class="line">    gp.sched.ret = <span class="number">1</span></span><br><span class="line">    gogo(&amp;gp.sched)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight x86asm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">// src/runtime/asm_amd64.s 第 <span class="number">274</span> 行</span><br><span class="line"></span><br><span class="line">// func gogo(buf *gobuf)</span><br><span class="line">// restore state from Gobuf<span class="comment">; longjmp</span></span><br><span class="line">TEXT runtime·gogo(SB), <span class="built_in">NOSPLIT</span>, <span class="number">$16</span>-<span class="number">8</span></span><br><span class="line">    <span class="keyword">MOVQ</span>buf+<span class="number">0</span>(FP), <span class="built_in">BX</span>// gobuf</span><br><span class="line">    <span class="keyword">MOVQ</span>gobuf_g(<span class="built_in">BX</span>), <span class="built_in">DX</span></span><br><span class="line">    <span class="keyword">MOVQ</span><span class="number">0</span>(<span class="built_in">DX</span>), <span class="built_in">CX</span>// make sure g != nil</span><br><span class="line">    get_tls(<span class="built_in">CX</span>)</span><br><span class="line">    <span class="keyword">MOVQ</span><span class="built_in">DX</span>, g(<span class="built_in">CX</span>)</span><br><span class="line">    <span class="keyword">MOVQ</span>gobuf_sp(<span class="built_in">BX</span>), <span class="built_in">SP</span>// restore <span class="built_in">SP</span></span><br><span class="line">    <span class="keyword">MOVQ</span>gobuf_ret(<span class="built_in">BX</span>), <span class="built_in">AX</span></span><br><span class="line">    <span class="keyword">MOVQ</span>gobuf_ctxt(<span class="built_in">BX</span>), <span class="built_in">DX</span></span><br><span class="line">    <span class="keyword">MOVQ</span>gobuf_bp(<span class="built_in">BX</span>), <span class="built_in">BP</span></span><br><span class="line">    <span class="keyword">MOVQ</span><span class="number">$0</span>, gobuf_sp(<span class="built_in">BX</span>)// clear to help garbage collector</span><br><span class="line">    <span class="keyword">MOVQ</span><span class="number">$0</span>, gobuf_ret(<span class="built_in">BX</span>)</span><br><span class="line">    <span class="keyword">MOVQ</span><span class="number">$0</span>, gobuf_ctxt(<span class="built_in">BX</span>)</span><br><span class="line">    <span class="keyword">MOVQ</span><span class="number">$0</span>, gobuf_bp(<span class="built_in">BX</span>)</span><br><span class="line">    <span class="keyword">MOVQ</span>gobuf_pc(<span class="built_in">BX</span>), <span class="built_in">BX</span></span><br><span class="line">    <span class="keyword">JMP</span><span class="built_in">BX</span></span><br></pre></td></tr></table></figure></li></ul><p>以上便是 Go 底层处理异常的流程，精简为三步便是：</p><ul><li><code>defer</code> 函数中调用 <code>recover</code></li><li>触发 <code>panic</code> 并切到 <code>runtime</code> 环境获取在 <code>defer</code> 中调用了 <code>recover</code> 的 <code>g</code> 的 sp 和 pc</li><li>恢复到 <code>defer</code> 中 <code>recover</code> 后面的处理逻辑</li></ul><h3 id="都有哪些坑"><a href="#都有哪些坑" class="headerlink" title="都有哪些坑"></a>都有哪些坑</h3><p>前面提到，<code>panic</code> 函数主要用于主动触发异常。我们在实现业务代码的时候，在程序启动阶段，如果资源初始化出错，可以主动调用 <code>panic</code> 立即结束程序。对于新手来说，这没什么问题，很容易做到。</p><p>但是，现实往往是残酷的—— Go 的 <code>runtime</code> 代码中很多地方都调用了 <code>panic</code> 函数，对于不了解 Go 底层实现的新人来说，这无疑是挖了一堆深坑。如果不熟悉这些坑，是不可能写出健壮的 Go 代码。</p><p>接下来，笔者给大家细数下都有哪些坑。</p><ul><li><h4 id="数组-slice-下标越界"><a href="#数组-slice-下标越界" class="headerlink" title="数组 ( slice ) 下标越界"></a>数组 ( slice ) 下标越界</h4></li></ul><p>这个比较好理解，对于静态类型语言，数组下标越界是致命错误。如下代码可以验证：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">"fmt"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">foo</span><span class="params">()</span></span>&#123;</span><br><span class="line">    <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span>&#123;</span><br><span class="line">        <span class="keyword">if</span> err := <span class="built_in">recover</span>(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">            fmt.Println(err)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">var</span> bar = []<span class="keyword">int</span>&#123;<span class="number">1</span>&#125;</span><br><span class="line">    fmt.Println(bar[<span class="number">1</span>])</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>&#123;</span><br><span class="line">    foo()</span><br><span class="line">    fmt.Println(<span class="string">"exit"</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">runtime error: index out of range</span><br><span class="line"><span class="built_in">exit</span></span><br></pre></td></tr></table></figure><p>因为代码中用了 <code>recover</code> ，程序得以恢复，输出 <code>exit</code>。</p><p>如果将 <code>recover</code> 那几行注释掉，将会输出如下日志：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">panic: runtime error: index out of range</span><br><span class="line"></span><br><span class="line">goroutine 1 [running]:</span><br><span class="line">main.foo()</span><br><span class="line">  /home/letian/work/go/src/<span class="built_in">test</span>/test.go:14 +0x3e</span><br><span class="line">main.main()</span><br><span class="line">  /home/letian/work/go/src/<span class="built_in">test</span>/test.go:18 +0x22</span><br><span class="line"><span class="built_in">exit</span> status 2</span><br></pre></td></tr></table></figure><ul><li><h4 id="访问未初始化的指针或-nil-指针"><a href="#访问未初始化的指针或-nil-指针" class="headerlink" title="访问未初始化的指针或 nil 指针"></a>访问未初始化的指针或 nil 指针</h4></li></ul><p>对于有 c/c++ 开发经验的人来说，这个很好理解。但对于没用过指针的新手来说，这是最常见的一类错误。<br>如下代码可以验证：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">"fmt"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">foo</span><span class="params">()</span></span>&#123;</span><br><span class="line">  <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span>&#123;</span><br><span class="line">      <span class="keyword">if</span> err := <span class="built_in">recover</span>(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">          fmt.Println(err)</span><br><span class="line">      &#125;</span><br><span class="line">  &#125;()</span><br><span class="line">  <span class="keyword">var</span> bar *<span class="keyword">int</span></span><br><span class="line">  fmt.Println(*bar)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>&#123;</span><br><span class="line">  foo()</span><br><span class="line">  fmt.Println(<span class="string">"exit"</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">runtime error: invalid memory address or nil pointer dereference</span><br><span class="line"><span class="built_in">exit</span></span><br></pre></td></tr></table></figure><p>如果将 <code>recover</code> 那几行代码注释掉，则会输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">panic: runtime error: invalid memory address or nil pointer dereference</span><br><span class="line">[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x4869ff]</span><br><span class="line"></span><br><span class="line">goroutine 1 [running]:</span><br><span class="line">main.foo()</span><br><span class="line">  /home/letian/work/go/src/<span class="built_in">test</span>/test.go:14 +0x3f</span><br><span class="line">main.main()</span><br><span class="line">  /home/letian/work/go/src/<span class="built_in">test</span>/test.go:18 +0x22</span><br><span class="line"><span class="built_in">exit</span> status 2</span><br></pre></td></tr></table></figure><ul><li><h4 id="试图往已经-close-的-chan-里发送数据"><a href="#试图往已经-close-的-chan-里发送数据" class="headerlink" title="试图往已经 close 的 chan 里发送数据"></a>试图往已经 close 的 <code>chan</code> 里发送数据</h4></li></ul><p>这也是刚学用 <code>chan</code> 的新手容易犯的错误。如下代码可以验证：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">"fmt"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">foo</span><span class="params">()</span></span>&#123;</span><br><span class="line">  <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span>&#123;</span><br><span class="line">      <span class="keyword">if</span> err := <span class="built_in">recover</span>(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">          fmt.Println(err)</span><br><span class="line">      &#125;</span><br><span class="line">  &#125;()</span><br><span class="line">  <span class="keyword">var</span> bar = <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">int</span>, <span class="number">1</span>)</span><br><span class="line">  <span class="built_in">close</span>(bar)</span><br><span class="line">  bar&lt;<span class="number">-1</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>&#123;</span><br><span class="line">  foo()</span><br><span class="line">  fmt.Println(<span class="string">"exit"</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">send on closed channel</span><br><span class="line"><span class="built_in">exit</span></span><br></pre></td></tr></table></figure><p>如果注释掉 recover ，将输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">panic: send on closed channel</span><br><span class="line"></span><br><span class="line">goroutine 1 [running]:</span><br><span class="line">main.foo()</span><br><span class="line">  /home/letian/work/go/src/<span class="built_in">test</span>/test.go:15 +0x83</span><br><span class="line">main.main()</span><br><span class="line">  /home/letian/work/go/src/<span class="built_in">test</span>/test.go:19 +0x22</span><br><span class="line"><span class="built_in">exit</span> status 2</span><br></pre></td></tr></table></figure><p>源码处理逻辑在 <code>src/runtime/chan.go</code> 的 <code>chansend</code> 函数中，如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// src/runtime/chan.go 第 269 行</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment">  * generic single channel send/recv</span></span><br><span class="line"><span class="comment">  * If block is not nil,</span></span><br><span class="line"><span class="comment">  * then the protocol will not</span></span><br><span class="line"><span class="comment">  * sleep but return if it could</span></span><br><span class="line"><span class="comment">  * not complete.</span></span><br><span class="line"><span class="comment">  *</span></span><br><span class="line"><span class="comment">  * sleep can wake up with g.param == nil</span></span><br><span class="line"><span class="comment">  * when a channel involved in the sleep has</span></span><br><span class="line"><span class="comment">  * been closed.  it is easiest to loop and re-run</span></span><br><span class="line"><span class="comment">  * the operation; we'll see that it's now closed.</span></span><br><span class="line"><span class="comment">  */</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">chansend</span><span class="params">(c *hchan, ep unsafe.Pointer, block <span class="keyword">bool</span>, callerpc <span class="keyword">uintptr</span>)</span> <span class="title">bool</span></span> &#123;</span><br><span class="line">    <span class="keyword">if</span> c == <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> !block &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">        &#125;</span><br><span class="line">        gopark(<span class="literal">nil</span>, <span class="literal">nil</span>, waitReasonChanSendNilChan, traceEvGoStop, <span class="number">2</span>)</span><br><span class="line">        throw(<span class="string">"unreachable"</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> debugChan &#123;</span><br><span class="line">        <span class="built_in">print</span>(<span class="string">"chansend: chan="</span>, c, <span class="string">"\n"</span>)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> raceenabled &#123;</span><br><span class="line">        racereadpc(c.raceaddr(), callerpc, funcPC(chansend))</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Fast path: check for failed non-blocking operation without acquiring the lock.</span></span><br><span class="line">    <span class="comment">//</span></span><br><span class="line">    <span class="comment">// After observing that the channel is not closed, we observe that the channel is</span></span><br><span class="line">    <span class="comment">// not ready for sending. Each of these observations is a single word-sized read</span></span><br><span class="line">    <span class="comment">// (first c.closed and second c.recvq.first or c.qcount depending on kind of channel).</span></span><br><span class="line">    <span class="comment">// Because a closed channel cannot transition from 'ready for sending' to</span></span><br><span class="line">    <span class="comment">// 'not ready for sending', even if the channel is closed between the two observations,</span></span><br><span class="line">    <span class="comment">// they imply a moment between the two when the channel was both not yet closed</span></span><br><span class="line">    <span class="comment">// and not ready for sending. We behave as if we observed the channel at that moment,</span></span><br><span class="line">    <span class="comment">// and report that the send cannot proceed.</span></span><br><span class="line">    <span class="comment">//</span></span><br><span class="line">    <span class="comment">// It is okay if the reads are reordered here: if we observe that the channel is not</span></span><br><span class="line">    <span class="comment">// ready for sending and then observe that it is not closed, that implies that the</span></span><br><span class="line">    <span class="comment">// channel wasn't closed during the first observation.</span></span><br><span class="line">    <span class="keyword">if</span> !block &amp;&amp; c.closed == <span class="number">0</span> &amp;&amp; ((c.dataqsiz == <span class="number">0</span> &amp;&amp; c.recvq.first == <span class="literal">nil</span>) ||</span><br><span class="line">        (c.dataqsiz &gt; <span class="number">0</span> &amp;&amp; c.qcount == c.dataqsiz)) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> t0 <span class="keyword">int64</span></span><br><span class="line">    <span class="keyword">if</span> blockprofilerate &gt; <span class="number">0</span> &#123;</span><br><span class="line">        t0 = cputicks()</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    lock(&amp;c.lock)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> c.closed != <span class="number">0</span> &#123;</span><br><span class="line">        unlock(&amp;c.lock)</span><br><span class="line">        <span class="built_in">panic</span>(plainError(<span class="string">"send on closed channel"</span>))</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> sg := c.recvq.dequeue(); sg != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="comment">// Found a waiting receiver. We pass the value we want to send</span></span><br><span class="line">        <span class="comment">// directly to the receiver, bypassing the channel buffer (if any).</span></span><br><span class="line">        send(c, sg, ep, <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123; unlock(&amp;c.lock) &#125;, <span class="number">3</span>)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> c.qcount &lt; c.dataqsiz &#123;</span><br><span class="line">        <span class="comment">// Space is available in the channel buffer. Enqueue the element to send.</span></span><br><span class="line">        qp := chanbuf(c, c.sendx)</span><br><span class="line">        <span class="keyword">if</span> raceenabled &#123;</span><br><span class="line">            raceacquire(qp)</span><br><span class="line">            racerelease(qp)</span><br><span class="line">        &#125;</span><br><span class="line">        typedmemmove(c.elemtype, qp, ep)</span><br><span class="line">        c.sendx++</span><br><span class="line">        <span class="keyword">if</span> c.sendx == c.dataqsiz &#123;</span><br><span class="line">            c.sendx = <span class="number">0</span></span><br><span class="line">        &#125;</span><br><span class="line">        c.qcount++</span><br><span class="line">        unlock(&amp;c.lock)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> !block &#123;</span><br><span class="line">        unlock(&amp;c.lock)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Block on the channel. Some receiver will complete our operation for us.</span></span><br><span class="line">    gp := getg()</span><br><span class="line">    mysg := acquireSudog()</span><br><span class="line">    mysg.releasetime = <span class="number">0</span></span><br><span class="line">    <span class="keyword">if</span> t0 != <span class="number">0</span> &#123;</span><br><span class="line">        mysg.releasetime = <span class="number">-1</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// No stack splits between assigning elem and enqueuing mysg</span></span><br><span class="line">    <span class="comment">// on gp.waiting where copystack can find it.</span></span><br><span class="line">    mysg.elem = ep</span><br><span class="line">    mysg.waitlink = <span class="literal">nil</span></span><br><span class="line">    mysg.g = gp</span><br><span class="line">    mysg.isSelect = <span class="literal">false</span></span><br><span class="line">    mysg.c = c</span><br><span class="line">    gp.waiting = mysg</span><br><span class="line">    gp.param = <span class="literal">nil</span></span><br><span class="line">    c.sendq.enqueue(mysg)</span><br><span class="line">    goparkunlock(&amp;c.lock, waitReasonChanSend, traceEvGoBlockSend, <span class="number">3</span>)</span><br><span class="line">    <span class="comment">// Ensure the value being sent is kept alive until the</span></span><br><span class="line">    <span class="comment">// receiver copies it out. The sudog has a pointer to the</span></span><br><span class="line">    <span class="comment">// stack object, but sudogs aren't considered as roots of the</span></span><br><span class="line">    <span class="comment">// stack tracer.</span></span><br><span class="line">    KeepAlive(ep)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// someone woke us up.</span></span><br><span class="line">    <span class="keyword">if</span> mysg != gp.waiting &#123;</span><br><span class="line">        throw(<span class="string">"G waiting list is corrupted"</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    gp.waiting = <span class="literal">nil</span></span><br><span class="line">    <span class="keyword">if</span> gp.param == <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">if</span> c.closed == <span class="number">0</span> &#123;</span><br><span class="line">            throw(<span class="string">"chansend: spurious wakeup"</span>)</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="built_in">panic</span>(plainError(<span class="string">"send on closed channel"</span>))</span><br><span class="line">    &#125;</span><br><span class="line">    gp.param = <span class="literal">nil</span></span><br><span class="line">    <span class="keyword">if</span> mysg.releasetime &gt; <span class="number">0</span> &#123;</span><br><span class="line">        blockevent(mysg.releasetime-t0, <span class="number">2</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    mysg.c = <span class="literal">nil</span></span><br><span class="line">    releaseSudog(mysg)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li><h4 id="并发读写相同-map"><a href="#并发读写相同-map" class="headerlink" title="并发读写相同 map"></a>并发读写相同 map</h4></li></ul><p>对于刚学并发编程的同学来说，并发读写 map 也是很容易遇到的问题。如下代码可以验证：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">"fmt"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">foo</span><span class="params">()</span></span>&#123;</span><br><span class="line">  <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span>&#123;</span><br><span class="line">      <span class="keyword">if</span> err := <span class="built_in">recover</span>(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">          fmt.Println(err)</span><br><span class="line">      &#125;</span><br><span class="line">  &#125;()</span><br><span class="line">  <span class="keyword">var</span> bar = <span class="built_in">make</span>(<span class="keyword">map</span>[<span class="keyword">int</span>]<span class="keyword">int</span>)</span><br><span class="line">  <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span>&#123;</span><br><span class="line">      <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span>&#123;</span><br><span class="line">          <span class="keyword">if</span> err := <span class="built_in">recover</span>(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">              fmt.Println(err)</span><br><span class="line">          &#125;</span><br><span class="line">      &#125;()</span><br><span class="line">      <span class="keyword">for</span>&#123;</span><br><span class="line">          _ = bar[<span class="number">1</span>]</span><br><span class="line">      &#125;</span><br><span class="line">  &#125;()</span><br><span class="line">  <span class="keyword">for</span>&#123;</span><br><span class="line">      bar[<span class="number">1</span>]=<span class="number">1</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>&#123;</span><br><span class="line">  foo()</span><br><span class="line">  fmt.Println(<span class="string">"exit"</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">fatal error: concurrent map <span class="built_in">read</span> and map write</span><br><span class="line"></span><br><span class="line">goroutine 5 [running]:</span><br><span class="line">runtime.throw(0x4bd8b0, 0x21)</span><br><span class="line">  /home/letian/.gvm/gos/go1.12/src/runtime/panic.go:617 +0x72 fp=0xc00004c780 sp=0xc00004c750 pc=0x427f22</span><br><span class="line">runtime.mapaccess1_fast64(0x49eaa0, 0xc000088180, 0x1, 0xc0000260d8)</span><br><span class="line">  /home/letian/.gvm/gos/go1.12/src/runtime/map_fast64.go:21 +0x1a8 fp=0xc00004c7a8 sp=0xc00004c780 pc=0x40eb58</span><br><span class="line">main.foo.func2(0xc000088180)</span><br><span class="line">  /home/letian/work/go/src/<span class="built_in">test</span>/test.go:21 +0x5c fp=0xc00004c7d8 sp=0xc00004c7a8 pc=0x48708c</span><br><span class="line">runtime.goexit()</span><br><span class="line">  /home/letian/.gvm/gos/go1.12/src/runtime/asm_amd64.s:1337 +0x1 fp=0xc00004c7e0 sp=0xc00004c7d8 pc=0x450e51</span><br><span class="line">created by main.foo</span><br><span class="line">  /home/letian/work/go/src/<span class="built_in">test</span>/test.go:14 +0x68</span><br><span class="line"></span><br><span class="line">goroutine 1 [runnable]:</span><br><span class="line">main.foo()</span><br><span class="line">  /home/letian/work/go/src/<span class="built_in">test</span>/test.go:25 +0x8b</span><br><span class="line">main.main()</span><br><span class="line">  /home/letian/work/go/src/<span class="built_in">test</span>/test.go:30 +0x22</span><br><span class="line"><span class="built_in">exit</span> status 2</span><br></pre></td></tr></table></figure><p>细心的朋友不难发现，输出日志里没有出现我们在程序末尾打印的 <code>exit</code>，而是直接将调用栈打印出来了。查看 <code>src/runtime/map.go</code> 中的代码不难发现这几行：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> h.flags&amp;hashWriting != <span class="number">0</span> &#123;</span><br><span class="line">  throw(<span class="string">"concurrent map read and map write"</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>与前面提到的几种情况不同，<code>runtime</code> 中调用 <code>throw</code> 函数抛出的异常是无法在业务代码中通过 <code>recover</code> 捕获的，这点最为致命。所以，对于并发读写 map 的地方，应该对 map 加锁。</p><ul><li><h4 id="类型断言"><a href="#类型断言" class="headerlink" title="类型断言"></a>类型断言</h4></li></ul><p>在使用类型断言对 <code>interface</code> 进行类型转换的时候也容易一不小心踩坑，而且这个坑是即使用 <code>interface</code> 有一段时间的人也容易忽略的问题。如下代码可以验证：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">"fmt"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">foo</span><span class="params">()</span></span>&#123;</span><br><span class="line">    <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span>&#123;</span><br><span class="line">        <span class="keyword">if</span> err := <span class="built_in">recover</span>(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">            fmt.Println(err)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    <span class="keyword">var</span> i <span class="keyword">interface</span>&#123;&#125; = <span class="string">"abc"</span></span><br><span class="line">    _ = i.([]<span class="keyword">string</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span>&#123;</span><br><span class="line">    foo()</span><br><span class="line">    fmt.Println(<span class="string">"exit"</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>输出：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">interface conversion: interface &#123;&#125; is string, not []string</span><br><span class="line"><span class="built_in">exit</span></span><br></pre></td></tr></table></figure><p>源码在 <code>src/runtime/iface.go</code> 中，如下两个函数：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// panicdottypeE is called when doing an e.(T) conversion and the conversion fails.</span></span><br><span class="line"><span class="comment">// have = the dynamic type we have.</span></span><br><span class="line"><span class="comment">// want = the static type we're trying to convert to.</span></span><br><span class="line"><span class="comment">// iface = the static type we're converting from.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">panicdottypeE</span><span class="params">(have, want, iface *_type)</span></span> &#123;</span><br><span class="line">    <span class="built_in">panic</span>(&amp;TypeAssertionError&#123;iface, have, want, <span class="string">""</span>&#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// panicdottypeI is called when doing an i.(T) conversion and the conversion fails.</span></span><br><span class="line"><span class="comment">// Same args as panicdottypeE, but "have" is the dynamic itab we have.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">panicdottypeI</span><span class="params">(have *itab, want, iface *_type)</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> t *_type</span><br><span class="line">    <span class="keyword">if</span> have != <span class="literal">nil</span> &#123;</span><br><span class="line">        t = have._type</span><br><span class="line">    &#125;</span><br><span class="line">    panicdottypeE(t, want, iface)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="更多的-panic"><a href="#更多的-panic" class="headerlink" title="更多的 panic"></a>更多的 panic</h2><p>前面提到的只是基本语法中常遇到的几种 <code>panic</code> 场景，Go 标准库中有更多使用 <code>panic</code> 的地方，大家可以在源码中搜索 <code>panic(</code> 找到调用的地方，以免后续用标准库函数的时候踩坑。</p><p>限于篇幅，本文暂不介绍填坑技巧，后面再开其他篇幅逐个介绍。感谢阅读！</p><h2 id="下回预告"><a href="#下回预告" class="headerlink" title="下回预告"></a>下回预告</h2><p>Go 语言踩坑记之 channel 与 goroutine。</p><hr><p><strong>作者</strong></p><p>易乐天，小米信息技术部海外商城组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com（武汉）</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Go-语言踩坑记——panic-与-recover&quot;&gt;&lt;a href=&quot;#Go-语言踩坑记——panic-与-recover&quot; class=&quot;headerlink&quot; title=&quot;Go 语言踩坑记——panic 与 recover&quot;&gt;&lt;/a&gt;Go 语言踩坑记——p
      
    
    </summary>
    
      <category term="分布式事务" scheme="https://xiaomi-info.github.io/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/"/>
    
    
      <category term="Go" scheme="https://xiaomi-info.github.io/tags/Go/"/>
    
  </entry>
  
  <entry>
    <title>投稿须知</title>
    <link href="https://xiaomi-info.github.io/2020/01/03/contribution/"/>
    <id>https://xiaomi-info.github.io/2020/01/03/contribution/</id>
    <published>2020-01-03T01:37:47.000Z</published>
    <updated>2021-05-24T03:39:30.220Z</updated>
    
    <content type="html"><![CDATA[<h1 id="投稿须知"><a href="#投稿须知" class="headerlink" title="投稿须知"></a>投稿须知</h1><p><strong>[作者简介]</strong> 郑伟，小米信息技术部架构组</p><p><strong>注：目前暂时只接受部门内部投稿，本文只是对部门内部同事提供投稿指导。</strong></p><p>目前我们维护了 <a href="https://xiaomi-info.github.io">https://xiaomi-info.github.io</a> 和信息技术部公众号两个平台，同时会定期的产出技术文章在这两个平台上发表。</p><p>为让大家明确投稿文章范围及文章格式等要求，我们整理了一份投稿指导方案以供大家参考。</p><h2 id="投稿方式"><a href="#投稿方式" class="headerlink" title="投稿方式"></a>投稿方式</h2><ul><li>目前 xiaomi-info、信息技术部公众号均可同时登出稿件。由信息技术部架构组统一评审、校正稿件。</li><li>如有技术文章需要投稿，可以先提前联系 jin.zhang(a)xiaomi.com、zhengwei6(a)xiaomi.com。</li><li>所有稿件我们尽量保证在 24 小时内审核完毕，业务方可以对提出的修改建议进行更新。</li></ul><h2 id="投稿流程"><a href="#投稿流程" class="headerlink" title="投稿流程"></a>投稿流程</h2><ul><li>投稿技术文章只接收 markdown 格式，相关语法可以参考 <a href="https://markdown-zh.readthedocs.io/en/latest/" target="_blank" rel="noopener">markdown-zh</a>；</li><li>目前所有技术文章托管于 info-arch/xiaomi-info.github.io 仓库。用户文章的编辑和发布流程一般如下：<ul><li>git clone 仓库到本地</li><li>git checkout -b feature/your-feature-name 分支，并在 source/_posts 目录下创建相应的 markdown 文件。<br>一个文章应该只包含一个 markdown 文件，markdown 文件名应当采用 my-article-about-something.md 的中划线命名法。<br>markdown 文件名应当尽量简短，使用英文和数字。（可参考 source/_posts/mysql-implicit-conversion.md）</li><li>技术文章内如果引用图片，可以在 source/_posts 目录下创建和 markdown 文件同名文件夹，将图片移动到该文件夹内。<br>同时在 markdown 中通过  方式引用。（可参考 source/_posts/mysql-implicit-conversion.md）</li><li>编辑完毕后，push 分支到仓库，并提交 Merge Request，请求合并到 master 分支。</li><li>我们收到 Merge Request 请求后，会对文章进行评审，并提出修改意见。</li><li>用户修改完毕确认无误后，可合并到 master 分支。随后我们会同步到 xiaomi-info 和微信公众号。</li><li>目前支持投稿的文章领域包括：大数据、架构设计、后台开发、编程语言、数据库、Web 前端、移动开发等。</li></ul></li><li>投稿文章应尽量做到<ul><li>内容整洁，结构明确，分段合理。</li><li>标点符号规范，比如全角符号和半角符号。</li><li>注意专有名词大小写，比如 OK（不要写成 ok）、HTTP（不要写成 http）、JSON（不要写成 json）、MySQL（不要写成 mysql）。</li><li>注意中英文混排，中英文之间、中文数字之间都要留有空格，可参考 <a href="https://github.com/sparanoid/chinese-copywriting-guidelines" target="_blank" rel="noopener">「中文排版指北」</a>。</li></ul></li><li>所有投稿文章必须是原创。</li></ul><p><strong>作者</strong></p><p>郑伟，小米信息技术部架构组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com（武汉）</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;投稿须知&quot;&gt;&lt;a href=&quot;#投稿须知&quot; class=&quot;headerlink&quot; title=&quot;投稿须知&quot;&gt;&lt;/a&gt;投稿须知&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;[作者简介]&lt;/strong&gt; 郑伟，小米信息技术部架构组&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注：目前暂时只接受
      
    
    </summary>
    
      <category term="投稿" scheme="https://xiaomi-info.github.io/categories/%E6%8A%95%E7%A8%BF/"/>
    
    
      <category term="投稿" scheme="https://xiaomi-info.github.io/tags/%E6%8A%95%E7%A8%BF/"/>
    
  </entry>
  
  <entry>
    <title>React Native 启动版本检查机制探究</title>
    <link href="https://xiaomi-info.github.io/2020/01/02/react-native-version-check/"/>
    <id>https://xiaomi-info.github.io/2020/01/02/react-native-version-check/</id>
    <published>2020-01-02T06:48:00.000Z</published>
    <updated>2021-05-24T03:39:30.312Z</updated>
    
    <content type="html"><![CDATA[<h1 id="React-Native-启动版本检查机制探究"><a href="#React-Native-启动版本检查机制探究" class="headerlink" title="React Native 启动版本检查机制探究"></a>React Native 启动版本检查机制探究</h1><p><strong>[作者简介]</strong> 陈久林，信息部前端组，主要负责服务体系前端开发。</p><h2 id="引子"><a href="#引子" class="headerlink" title="引子"></a>引子</h2><p>有同学反馈 React Native（简称 RN） 项目启动报错，提示版本不匹配，错误截图如下：</p><img src="/2020/01/02/react-native-version-check/mismatch.png"><p>经过一番排 (xia) 查 (gao)，最后发现是本地打包了老版本 js 文件，和项目本身依赖的版本不同导致，删除本地的老版本文件即可。</p><p>通过这个错误，我们可以发现 RN 在启动时是有版本检查的，具体机制如何呢，下面我们一起跟着源码走一遍。</p><blockquote><p>至于为何本地会打包一个老的 js 文件，以及为何这么多年过去了今天才出问题，这是另一个话题，暂且忽略</p></blockquote><h2 id="版本检测机制"><a href="#版本检测机制" class="headerlink" title="版本检测机制"></a>版本检测机制</h2><h3 id="报错的位置"><a href="#报错的位置" class="headerlink" title="报错的位置"></a>报错的位置</h3><p>通过搜索关键字 <code>React Native version mismatch</code> 可以发现检测的最终代码在 <code>Libraries/Core/ReactNativeVersionCheck.js</code> 中：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> Platform <span class="keyword">from</span> <span class="string">'../Utilities/Platform'</span>;</span><br><span class="line"><span class="keyword">const</span> ReactNativeVersion = <span class="built_in">require</span>(<span class="string">'./ReactNativeVersion'</span>);</span><br><span class="line"></span><br><span class="line">exports.checkVersions = <span class="function"><span class="keyword">function</span> <span class="title">checkVersions</span>(<span class="params"></span>): <span class="title">void</span> </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> nativeVersion = Platform.constants.reactNativeVersion;</span><br><span class="line">    <span class="keyword">if</span> (</span><br><span class="line">        ReactNativeVersion.version.major !== nativeVersion.major ||</span><br><span class="line">        ReactNativeVersion.version.minor !== nativeVersion.minor</span><br><span class="line">    ) &#123;</span><br><span class="line">        <span class="built_in">console</span>.error(</span><br><span class="line">            <span class="string">`React Native version mismatch.\n\nJavaScript version: <span class="subst">$&#123;_formatVersion(</span></span></span><br><span class="line"><span class="string"><span class="subst">        ReactNativeVersion.version,</span></span></span><br><span class="line"><span class="string"><span class="subst">      )&#125;</span>\n`</span> +</span><br><span class="line"><span class="string">`Native version: <span class="subst">$&#123;_formatVersion(nativeVersion)&#125;</span>\n\n`</span> +</span><br><span class="line">            <span class="string">'Make sure that you have rebuilt the native code. If the problem '</span> +</span><br><span class="line">            <span class="string">'persists try clearing the Watchman and packager caches with '</span> +</span><br><span class="line">            <span class="string">' `watchman watch-del-all &amp;&amp; react-native start --reset-cache` .'</span>,</span><br><span class="line">        );</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>该方法对比了 <code>ReactNativeVersion.version</code> 和 <code>Platform.constants.reactNativeVersion</code> 两个的 major 和 minor，当这两个值不匹配时即会抛出该异常。</p><blockquote><p>如果版本号是 0.59.9，那么 major 就是 59，minor 就是 9。感觉 RN 就没打算把最前面的 0 去掉（手动捂脸</p><p>同时， <code>checkVersion</code> 是在启动时候加载，这部分代码大家自行搜索即可看到，不做分析</p></blockquote><h3 id="ReactNativeVersion-version"><a href="#ReactNativeVersion-version" class="headerlink" title="ReactNativeVersion.version"></a>ReactNativeVersion.version</h3><p>那么这两个值分别代表的什么呢，首先查看 <code>ReactNativeVersion.version</code> ，它在同目录下的 <a href="https://github.com/facebook/react-native/blob/master/Libraries/Core/ReactNativeVersion.js" target="_blank" rel="noopener">Libraries/Core/ReactNativeVersion.js</a> 中声明：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">exports.version = &#123;</span><br><span class="line">    major: <span class="number">0</span>,</span><br><span class="line">    minor: <span class="number">0</span>,</span><br><span class="line">    patch: <span class="number">0</span>,</span><br><span class="line">    prerelease: <span class="literal">null</span>,</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>嗯，非常的清晰明了。简直写了跟没写一样嘛，不急，反正我们知道了，这个值是在 js 文件中，会随着最终的打包进入 bundle.js 中。</p><h3 id="Platform-constants-reactNativeVersion"><a href="#Platform-constants-reactNativeVersion" class="headerlink" title="Platform.constants.reactNativeVersion"></a>Platform.constants.reactNativeVersion</h3><p>根据引用，我们找到 <a href="https://github.com/facebook/react-native/blob/master/Libraries/Utilities/Platform.android.js" target="_blank" rel="noopener">Libraries/Utilities/Platform.android.js</a> 这个文件，关键内容如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> NativePlatformConstantsAndroid <span class="keyword">from</span> <span class="string">'./NativePlatformConstantsAndroid'</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> Platform = &#123;</span><br><span class="line"></span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    get constants() &#123;</span><br><span class="line">            <span class="keyword">if</span> (<span class="keyword">this</span>.__constants == <span class="literal">null</span>) &#123;</span><br><span class="line">                <span class="keyword">this</span>.__constants = NativePlatformConstantsAndroid.getConstants();</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">this</span>.__constants;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        ...</span><br><span class="line"></span><br><span class="line">&#125;;</span><br><span class="line"></span><br><span class="line"><span class="built_in">module</span>.exports = Platform;</span><br></pre></td></tr></table></figure><p>又导向了 <code>NativePlatformConstantsAndroid.getConstants()</code> ，在 <a href="https://github.com/facebook/react-native/blob/master/Libraries/Utilities/NativePlatformConstantsAndroid.js" target="_blank" rel="noopener">Libraries/Utilities/NativePlatformConstantsAndroid.js</a> 中，如下：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> * <span class="keyword">as</span> TurboModuleRegistry <span class="keyword">from</span> <span class="string">'../TurboModule/TurboModuleRegistry'</span>;</span><br><span class="line"></span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">default</span> (TurboModuleRegistry.getEnforcing &lt; Spec &gt; (</span><br><span class="line">    <span class="string">'PlatformConstants'</span>,</span><br><span class="line">): Spec);</span><br></pre></td></tr></table></figure><p>犹抱琵琶半遮面，通过 <code>TurboModuleRegistry.getEnforcing(&#39;PlatformConstants&#39;)</code> 获取到，继续往下 <a href="https://github.com/facebook/react-native/blob/master/Libraries/TurboModule/TurboModuleRegistry.js" target="_blank" rel="noopener">Libraries/TurboModule/TurboModuleRegistry.js</a>：</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> NativeModules = <span class="built_in">require</span>(<span class="string">'../BatchedBridge/NativeModules'</span>);</span><br><span class="line"><span class="keyword">const</span> turboModuleProxy = global.__turboModuleProxy;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">get</span> &lt; <span class="title">T</span>: <span class="title">TurboModule</span> &gt; (<span class="params">name: string</span>): ? <span class="title">T</span> </span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (!global.RN$Bridgeless) &#123;</span><br><span class="line">        <span class="comment">// Backward compatibility layer during migration.</span></span><br><span class="line">        <span class="keyword">const</span> legacyModule = NativeModules[name];</span><br><span class="line">        <span class="keyword">if</span> (legacyModule != <span class="literal">null</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> ((legacyModule: any): T);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> (turboModuleProxy != <span class="literal">null</span>) &#123;</span><br><span class="line">        <span class="keyword">const</span> <span class="built_in">module</span>: ? T = turboModuleProxy(name);</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">module</span>;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="function"><span class="keyword">function</span> <span class="title">getEnforcing</span> &lt; <span class="title">T</span>: <span class="title">TurboModule</span> &gt; (<span class="params">name: string</span>): <span class="title">T</span> </span>&#123;</span><br><span class="line">    <span class="keyword">const</span> <span class="built_in">module</span> = get(name);</span><br><span class="line">    <span class="keyword">return</span> <span class="built_in">module</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>一大坨东西，就一个目标，获取一个原生模块，名字叫 <code>PlatformConstants</code> ，那找到这个原生模块就能揭秘了，通过搜索 <code>PlatformConstants</code> ，可以找到它的原生实现在 <a href="https://github.com/facebook/react-native/blob/3b6f6ca4d5fcee6f1bc6d6242e3e2ef136e4d546/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java" target="_blank" rel="noopener">ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/AndroidInfoModule.java</a>，关键代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="meta">@Nullable</span> <span class="function">Map&lt;String, Object&gt; <span class="title">getConstants</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    HashMap&lt;String, Object&gt; constants = <span class="keyword">new</span> HashMap&lt;&gt;();</span><br><span class="line"></span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    constants.put(<span class="string">"reactNativeVersion"</span>, ReactNativeVersion.VERSION);</span><br><span class="line"></span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> constants;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>一步之遥了，继续看同目录下的 <a href="https://github.com/facebook/react-native/blob/3b6f6ca4d5/ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java" target="_blank" rel="noopener">ReactAndroid/src/main/java/com/facebook/react/modules/systeminfo/ReactNativeVersion.java</a> ：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">ReactNativeVersion</span> </span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> Map&lt;String, Object&gt; VERSION = MapBuilder.&lt;String, Object&gt;of(</span><br><span class="line">        <span class="string">"major"</span>, <span class="number">0</span>,</span><br><span class="line">        <span class="string">"minor"</span>, <span class="number">0</span>,</span><br><span class="line">        <span class="string">"patch"</span>, <span class="number">0</span>,</span><br><span class="line">        <span class="string">"prerelease"</span>, <span class="keyword">null</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>和 js 那边的一样，都是 0，这个待会再论。可以看出， <code>Platform.constants.reactNativeVersion</code> 是在 java 侧定义的，最终在原生代码中，我们在 build.gradle 文件中引用的 <code>com.facebook.react:react-native:0.59.9</code> 则包含了这部分代码。</p><h3 id="阶段性总结"><a href="#阶段性总结" class="headerlink" title="阶段性总结"></a>阶段性总结</h3><p>可以看出，在 js 侧有个版本号，同时在 java 侧也有个版本号，两者会在启动的时候进行判断，如果不相同就会抛出错误。</p><p>js 和 java 是两个依赖，js 部分在 <code>package.json</code> 中进行依赖，java 部分在 <code>android/app/build.gradle</code> 中依赖，两者必须匹配才能很好的工作，所以有了上述的检查工作。</p><blockquote><p>通过对启动源码分析，发现其实仅在开发环境才进行检查，生产环境则没有这段</p><p>一般而言，开发环境都会执行比生产环境更为严格的检测，确保开发阶段错误及时暴露，而在生产环境则会去掉与主功能无关的代码，保证运行时的最大效率。这可以说是大部分库的一个处理手段，严开发宽发布，值得我们学习借鉴</p></blockquote><h3 id="版本号如何设置"><a href="#版本号如何设置" class="headerlink" title="版本号如何设置"></a>版本号如何设置</h3><p>前面源码查看，发现版本号都是 0，那么具体版本号是如何设置上去的呢，大家可以查看下这个目录 <a href="https://github.com/facebook/react-native/tree/3b6f6ca4d5fcee6f1bc6d6242e3e2ef136e4d546/scripts/versiontemplates" target="_blank" rel="noopener">scripts/versiontemplates/</a>，其下则是版本号设置的模版，真正的操作则是在 <a href="https://github.com/facebook/react-native/blob/master/scripts/bump-oss-version.js#L60" target="_blank" rel="noopener">scripts/bump-oss-version.js#L60</a> 中进行的，这个脚本接受一个版本号，然后填充前面的模版，并覆盖项目中对应的文件。这个脚本是在发版的时候执行的，详情见 <a href="https://github.com/facebook/react-native/blob/master/Releases.md#step-2-cut-a-release-branch-and-push-to-github" target="_blank" rel="noopener">step-2-cut-a-release-branch-and-push-to-github</a>，至此一切就都清楚了。</p><p>所以版本号是在发布的时候通过脚本设置上去的，通过模版的方式进行统一设置，避免人工修改遗漏</p><blockquote><p>模版部分就是简单替换，并未引用额外的模版引擎，能简单处理就绝不搞复杂，这点值得我们学习</p><p>脚本很多都是 js 写的，这样非常容易阅读和修改，我们也可以多用 js 来处理脚本，不能提到脚本就 bash、python 的，其实 js 也很流行</p></blockquote><h3 id="什么情况下会发生这个错误"><a href="#什么情况下会发生这个错误" class="headerlink" title="什么情况下会发生这个错误"></a>什么情况下会发生这个错误</h3><p>我遇到的这例是因为该同学使用 RN 0.55.4 进行了手动打包，并将打包后的 js 文件上传了仓库，后来升级 RN 到 0.59.9，开发环境下，设备因为某些原因没有连接到对应的 packager，然后直接使用了本地的 js 文件，从而产生了该问题。</p><p>从前面源码分析来看，如果开发时 packager 启动了错误版本，也是可能产生该问题的。可以理解该机制就是确保当前运行的 App 从 packager 下载到的 js 文件版本是一致的，避免大家在错误的版本上继续开发，导致问题蔓延，不便于最后问题的排查。</p><p>当我们遇到这个问题时，一般都是 packager 启动了错误版本导致的。其次，除非你知道你在干什么，否则是严禁手动生成 js 的包，这部分都应该交由 RN 的打包脚本来执行和维护，并且是不能提交仓库的。</p><h3 id="为什么有这个检查"><a href="#为什么有这个检查" class="headerlink" title="为什么有这个检查"></a>为什么有这个检查</h3><p>并没有找到相关的说明，但可以推测下。个人认为是 RN 的开发模式导致的，在开发阶段，电脑上会启动一个 server，也就是上面提到的 packager，用来分发最新的 js 文件，这也是 RN 开发阶段可以快速更新代码的基础，因为分发是独立的，所以这部分是有可能发生版本不一致的的问题，而版本不一致是不影响大部分开发，因为 API 大部分是兼容性设计，如果放任这种行为，到了开发后期出现问题，排查将会非常艰难，所以这也是提前暴露问题。而在发布阶段，因为都是脚本自动执行，这部分相对安全很多。</p><blockquote><p>很多时候，一些疑难问题都是由低级错误导致的，只是问题在初期隐藏，到了中后期才爆发，这时再去排查就非常耗时了。特别对于 RN 这种开源项目，如果 issues 中有很多是低级错误导致的 “疑难杂症”，这是对资源的巨大浪费。从这点来看，这些基本检测还是很有必要的</p></blockquote><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>在开发环境，RN 启动阶段，会对 js 和 java 两边的版本号进行校验，匹配后才开始真正的系统启动流程。增加这一步检查，是确保开发基础环境的一致，保证开发顺利进行。</p><p>同时在追踪源码的过程中，也能学到很多知识，包括库的设计，开发环境与生产环境的差异化，模版设计等等。对于开源项目的错误，很多时候我们可以通过源码来了解问题的本质，这对于我们的开发和学习有很大的帮助。</p><hr><p><strong>作者</strong></p><p>陈久林，信息部前端组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com（武汉）</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;React-Native-启动版本检查机制探究&quot;&gt;&lt;a href=&quot;#React-Native-启动版本检查机制探究&quot; class=&quot;headerlink&quot; title=&quot;React Native 启动版本检查机制探究&quot;&gt;&lt;/a&gt;React Native 启动版本
      
    
    </summary>
    
      <category term="前端" scheme="https://xiaomi-info.github.io/categories/%E5%89%8D%E7%AB%AF/"/>
    
    
      <category term="React Native" scheme="https://xiaomi-info.github.io/tags/React-Native/"/>
    
      <category term="React" scheme="https://xiaomi-info.github.io/tags/React/"/>
    
      <category term="Javascript" scheme="https://xiaomi-info.github.io/tags/Javascript/"/>
    
  </entry>
  
  <entry>
    <title>分布式事务，这一篇就够了</title>
    <link href="https://xiaomi-info.github.io/2020/01/02/distributed-transaction/"/>
    <id>https://xiaomi-info.github.io/2020/01/02/distributed-transaction/</id>
    <published>2020-01-02T06:35:58.000Z</published>
    <updated>2021-05-24T03:39:30.220Z</updated>
    
    <content type="html"><![CDATA[<h1 id="分布式事务，这一篇就够了"><a href="#分布式事务，这一篇就够了" class="headerlink" title="分布式事务，这一篇就够了"></a>分布式事务，这一篇就够了</h1><p><strong>[作者简介]</strong> 李文华，小米信息技术部海外商城组</p><p>随着互联网技术的不断发展，系统越来越复杂，几乎所有 IT 公司的系统都已经完成从单体架构到分布式架构的转变，分布式系统几乎无处不在。谈到分布式系统，特别是微服务架构，我们不得不谈分布式事务。今天就跟大家一起聊聊分布式事务以及常用解决方案。</p><h2 id="基础理论"><a href="#基础理论" class="headerlink" title="基础理论"></a>基础理论</h2><p>在讲解具体方案之前，我们有必要了解一些分布式事务所涉及到的基础理论知识。</p><h3 id="事务"><a href="#事务" class="headerlink" title="事务"></a>事务</h3><p>事务是应用程序中一系列严密的操作，所有操作必须成功完成，否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性，一个事务中的一系列的操作要么全部成功，要么一个都不做。事务应该具有 4 个属性：原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。</p><h3 id="分布式事务"><a href="#分布式事务" class="headerlink" title="分布式事务"></a>分布式事务</h3><p>分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。例如在大型电商系统中，下单接口通常会扣减库存、减去优惠、生成订单 id, 而订单服务与库存、优惠、订单 id 都是不同的服务，下单接口的成功与否，不仅取决于本地的 db 操作，而且依赖第三方系统的结果，这时候分布式事务就保证这些操作要么全部成功，要么全部失败。本质上来说，分布式事务就是为了保证不同数据库的数据一致性。</p><h3 id="强一致性、弱一致性、最终一致性"><a href="#强一致性、弱一致性、最终一致性" class="headerlink" title="强一致性、弱一致性、最终一致性"></a>强一致性、弱一致性、最终一致性</h3><h4 id="强一致性"><a href="#强一致性" class="headerlink" title="强一致性"></a>强一致性</h4><p>任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程，看到的操作顺序，都和全局时钟下的顺序一致。简言之，在任意时刻，所有节点中的数据是一样的。</p><h4 id="弱一致性"><a href="#弱一致性" class="headerlink" title="弱一致性"></a>弱一致性</h4><p>数据更新后，如果能容忍后续的访问只能访问到部分或者全部访问不到，则是弱一致性。</p><h4 id="最终一致性"><a href="#最终一致性" class="headerlink" title="最终一致性"></a>最终一致性</h4><p>不保证在任意时刻任意节点上的同一份数据都是相同的，但是随着时间的迁移，不同节点上的同一份数据总是在向趋同的方向变化。简单说，就是在一段时间后，节点间的数据会最终达到一致状态。</p><h3 id="CAP-原则"><a href="#CAP-原则" class="headerlink" title="CAP 原则"></a>CAP 原则</h3><blockquote><p>CAP 原则又称 CAP 定理，指的是在一个分布式系统中， Consistency（一致性）、 Availability（可用性）、Partition tolerance（分区容错性），三者不可得兼。</p></blockquote><p>一致性（C）：</p><blockquote><p>在分布式系统中的所有数据备份，在同一时刻是否同样的值。（等同于所有节点访问同一份最新的数据副本）</p></blockquote><p>可用性（A）：</p><blockquote><p>在集群中一部分节点故障后，集群整体是否还能响应客户端的读写请求。（对数据更新具备高可用性）</p></blockquote><p>分区容错性（P）：</p><blockquote><p>以实际效果而言，分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性，就意味着发生了分区的情况，必须就当前操作在 C 和 A 之间做出选择。</p></blockquote><p>CAP 原则的精髓就是要么 AP，要么 CP，要么 AC，但是不存在 CAP。如果在某个分布式系统中数据无副本， 那么系统必然满足强一致性条件， 因为只有独一数据，不会出现数据不一致的情况，此时 C 和 P 两要素具备，但是如果系统发生了网络分区状况或者宕机，必然导致某些数据不可以访问，此时可用性条件就不能被满足，即在此情况下获得了 CP 系统，但是 CAP 不可同时满足。 </p><h3 id="BASE-理论"><a href="#BASE-理论" class="headerlink" title="BASE 理论"></a>BASE 理论</h3><p>BASE 理论指的是基本可用 Basically Available，软状态 Soft State，最终一致性 Eventual Consistency，核心思想是即便无法做到强一致性，但应该采用适合的方式保证最终一致性。</p><p>BASE，Basically Available Soft State Eventual Consistency 的简写：<br>BA：Basically Available 基本可用，分布式系统在出现故障的时候，允许损失部分可用性，即保证核心可用。<br>S：Soft State 软状态，允许系统存在中间状态，而该中间状态不会影响系统整体可用性。<br>E：Consistency 最终一致性，系统中的所有数据副本经过一定时间后，最终能够达到一致的状态。<br>BASE 理论本质上是对 CAP 理论的延伸，是对 CAP 中 AP 方案的一个补充。</p><h3 id="柔性事务"><a href="#柔性事务" class="headerlink" title="柔性事务"></a>柔性事务</h3><p>不同于 ACID 的刚性事务，在分布式场景下基于 BASE 理论，就出现了柔性事务的概念。要想通过柔性事务来达到最终的一致性，就需要依赖于一些特性，这些特性在具体的方案中不一定都要满足，因为不同的方案要求不一样；但是都不满足的话，是不可能做柔性事务的。</p><h3 id="幂等操作"><a href="#幂等操作" class="headerlink" title="幂等操作"></a>幂等操作</h3><p>在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数，或幂等方法，是指可以使用相同参数重复执行，并能获得相同结果的函数。这些函数不会影响系统状态，也不用担心重复执行会对系统造成改变。例如，支付流程中第三方支付系统告知系统中某个订单支付成功，接收该支付回调接口在网络正常的情况下无论操作多少次都应该返回成功。</p><h2 id="分布式事务使用场景"><a href="#分布式事务使用场景" class="headerlink" title="分布式事务使用场景"></a>分布式事务使用场景</h2><h3 id="转账"><a href="#转账" class="headerlink" title="转账"></a>转账</h3><p>转账是最经典那的分布式事务场景，假设用户 A 使用银行 app 发起一笔跨行转账给用户 B，银行系统首先扣掉用户 A 的钱，然后增加用户 B 账户中的余额。此时就会出现 2 种异常情况：1. 用户 A 的账户扣款成功，用户 B 账户余额增加失败 2. 用户 A 账户扣款失败，用户 B 账户余额增加成功。对于银行系统来说，以上 2 种情况都是不允许发生，此时就需要分布式事务来保证转账操作的成功。</p><h3 id="下单扣库存"><a href="#下单扣库存" class="headerlink" title="下单扣库存"></a>下单扣库存</h3><p>在电商系统中，下单是用户最常见操作。在下单接口中必定会涉及生成订单 id, 扣减库存等操作，对于微服务架构系统，订单 id 与库存服务一般都是独立的服务，此时就需要分布式事务来保证整个下单接口的成功。</p><h3 id="同步超时"><a href="#同步超时" class="headerlink" title="同步超时"></a>同步超时</h3><p>继续以电商系统为例，在微服务体系架构下，我们的支付与订单都是作为单独的系统存在。订单的支付状态依赖支付系统的通知，假设一个场景：我们的支付系统收到来自第三方支付的通知，告知某个订单支付成功，接收通知接口需要同步调用订单服务变更订单状态接口，更新订单状态为成功。流程图如下，从图中可以看出有两次调用，第三方支付调用支付服务，以及支付服务调用订单服务，这两步调用都可能出现调用超时的情况，此处如果没有分布式事务的保证，就会出现用户订单实际支付情况与最终用户看到的订单支付情况不一致的情况。<br><img src="/2020/01/02/distributed-transaction/notify-message.png"></p><h2 id="分布式事务的解决方案"><a href="#分布式事务的解决方案" class="headerlink" title="分布式事务的解决方案"></a>分布式事务的解决方案</h2><h3 id="两阶段提交-XA"><a href="#两阶段提交-XA" class="headerlink" title="两阶段提交/XA"></a>两阶段提交/XA</h3><p>两阶段提交，顾名思义就是要分两步提交。存在一个负责协调各个本地资源管理器的事务管理器，本地资源管理器一般是由数据库实现，事务管理器在第一阶段的时候询问各个资源管理器是否都就绪？如果收到每个资源的回复都是 yes，则在第二阶段提交事务，如果其中任意一个资源的回复是 no, 则回滚事务。</p><img src="/2020/01/02/distributed-transaction/XA-first.jpg"><img src="/2020/01/02/distributed-transaction/XA-second.jpg"><p>大致的流程：</p><p>第一阶段（prepare）：事务管理器向所有本地资源管理器发起请求，询问是否是 ready 状态，所有参与者都将本事务能否成功的信息反馈发给协调者；<br>第二阶段 (commit/rollback)：事务管理器根据所有本地资源管理器的反馈，通知所有本地资源管理器，步调一致地在所有分支上提交或者回滚。</p><p>存在的问题：</p><blockquote><p>同步阻塞：当参与事务者存在占用公共资源的情况，其中一个占用了资源，其他事务参与者就只能阻塞等待资源释放，处于阻塞状态。</p></blockquote><blockquote><p>单点故障：一旦事务管理器出现故障，整个系统不可用</p></blockquote><blockquote><p>数据不一致：在阶段二，如果事务管理器只发送了部分 commit 消息，此时网络发生异常，那么只有部分参与者接收到 commit 消息，也就是说只有部分参与者提交了事务，使得系统数据不一致。</p></blockquote><blockquote><p>不确定性：当协事务管理器发送 commit 之后，并且此时只有一个参与者收到了 commit，那么当该参与者与事务管理器同时宕机之后，重新选举的事务管理器无法确定该条消息是否提交成功。</p></blockquote><h3 id="TCC"><a href="#TCC" class="headerlink" title="TCC"></a>TCC</h3><p>关于 TCC（Try-Confirm-Cancel）的概念，最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。 TCC 事务机制相比于上面介绍的 XA，解决了其几个缺点：</p><ol><li>解决了协调者单点，由主业务方发起并完成这个业务活动。业务活动管理器也变成多点，引入集群。 </li><li>同步阻塞：引入超时，超时后进行补偿，并且不会锁定整个资源，将资源转换为业务逻辑形式，粒度变小。 </li><li>数据一致性，有了补偿机制之后，由业务活动管理器控制一致性</li></ol><p>TCC(Try Confirm Cancel)<br>Try 阶段：尝试执行，完成所有业务检查（一致性）, 预留必须业务资源（准隔离性）<br>Confirm 阶段：确认执行真正执行业务，不作任何业务检查，只使用 Try 阶段预留的业务资源，Confirm 操作满足幂等性。要求具备幂等设计，Confirm 失败后需要进行重试。<br>Cancel 阶段：取消执行，释放 Try 阶段预留的业务资源 Cancel 操作满足幂等性 Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致。</p><p>在 Try 阶段，是对业务系统进行检查及资源预览，比如订单和存储操作，需要检查库存剩余数量是否够用，并进行预留，预留操作的话就是新建一个可用库存数量字段，Try 阶段操作是对这个可用库存数量进行操作。<br>基于 TCC 实现分布式事务，会将原来只需要一个接口就可以实现的逻辑拆分为 Try、Confirm、Cancel 三个接口，所以代码实现复杂度相对较高。</p><h3 id="本地消息表"><a href="#本地消息表" class="headerlink" title="本地消息表"></a>本地消息表</h3><p>本地消息表这个方案最初是 ebay 架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章。该方案中会有消息生产者与消费者两个角色，假设系统 A 是消息生产者，系统 B 是消息消费者，其大致流程如下：<br><img src="/2020/01/02/distributed-transaction/native-message.jpg"></p><ol><li>当系统 A 被其他系统调用发生数据库表更操作，首先会更新数据库的业务表，其次会往相同数据库的消息表中插入一条数据，两个操作发生在同一个事务中</li><li>系统 A 的脚本定期轮询本地消息往 mq 中写入一条消息，如果消息发送失败会进行重试</li><li>系统 B 消费 mq 中的消息，并处理业务逻辑。如果本地事务处理失败，会在继续消费 mq 中的消息进行重试，如果业务上的失败，可以通知系统 A 进行回滚操作</li></ol><p>本地消息表实现的条件：</p><ol><li>消费者与生成者的接口都要支持幂等</li><li>生产者需要额外的创建消息表</li><li>需要提供补偿逻辑，如果消费者业务失败，需要生产者支持回滚操作</li></ol><p>容错机制：</p><ol><li>步骤 1 失败时，事务直接回滚</li><li>步骤 2、3 写 mq 与消费 mq 失败会进行重试</li><li>步骤 3 业务失败系统 B 向系统 A 发起事务回滚操作</li></ol><p>此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列，再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景，通过对账系统对事后问题的处理。</p><h3 id="可靠消息最终一致性"><a href="#可靠消息最终一致性" class="headerlink" title="可靠消息最终一致性"></a>可靠消息最终一致性</h3><p>大致流程如下：<br><img src="/2020/01/02/distributed-transaction/mq-message.jpg"></p><ol><li>A 系统先向 mq 发送一条 prepare 消息，如果 prepare 消息发送失败，则直接取消操作</li><li>如果消息发送成功，则执行本地事务</li><li>如果本地事务执行成功，则想 mq 发送一条 confirm 消息，如果发送失败，则发送回滚消息</li><li>B 系统定期消费 mq 中的 confirm 消息，执行本地事务，并发送 ack 消息。如果 B 系统中的本地事务失败，会一直不断重试，如果是业务失败，会向 A 系统发起回滚请求</li></ol><p>5.mq 会定期轮询所有 prepared 消息调用系统 A 提供的接口查询消息的处理情况，如果该 prepare 消息本地事务处理成功，则重新发送 confirm 消息，否则直接回滚该消息</p><p>该方案与本地消息最大的不同是去掉了本地消息表，其次本地消息表依赖消息表重试写入 mq 这一步由本方案中的轮询 prepare 消息状态来重试或者回滚该消息替代。其实现条件与余容错方案基本一致。目前市面上实现该方案的只有阿里的 RocketMq。</p><h3 id="尽最大努力通知"><a href="#尽最大努力通知" class="headerlink" title="尽最大努力通知"></a>尽最大努力通知</h3><p>最大努力通知是最简单的一种柔性事务，适用于一些最终一致性时间敏感度低的业务，且被动方处理结果 不影响主动方的处理结果。</p><p>这个方案的大致意思就是：</p><ol><li>系统 A 本地事务执行完之后，发送个消息到 MQ；</li><li>这里会有个专门消费 MQ 的服务，这个服务会消费 MQ 并调用系统 B 的接口；</li><li>要是系统 B 执行成功就 ok 了；要是系统 B 执行失败了，那么最大努力通知服务就定时尝试重新调用系统 B, 反复 N 次，最后还是不行就放弃。</li></ol><h2 id="分布式事务实战"><a href="#分布式事务实战" class="headerlink" title="分布式事务实战"></a>分布式事务实战</h2><h3 id="两阶段提交-XA-1"><a href="#两阶段提交-XA-1" class="headerlink" title="两阶段提交/XA"></a>两阶段提交/XA</h3><p>目前支付宝使用两阶段提交思想实现了分布式事务服务 (Distributed Transaction Service, DTS) ，它是一个分布式事务框架，用来保障在大规模分布式环境下事务的最终一致性。具体可参考支付宝官方文档：<a href="https://tech.antfin.com/docs/2/46887" target="_blank" rel="noopener">https://tech.antfin.com/docs/2/46887</a></p><h3 id="TCC-1"><a href="#TCC-1" class="headerlink" title="TCC"></a>TCC</h3><p>TCC 需要事务接口提供 try, confirm, cancel 三个接口，提高了编程的复杂性。依赖于业务方来配合提供这样的接口，推行难度大，所以一般不推荐使用这种方式。</p><h3 id="可靠消息最终一致性-1"><a href="#可靠消息最终一致性-1" class="headerlink" title="可靠消息最终一致性"></a>可靠消息最终一致性</h3><p>目前市面上支持该方案的 mq 只有阿里的 rocketmq, 该方案应用场景也比较多，比如用户注册成功后发送邮件、电商系统给用户发送优惠券等需要保证最终一致性的场景</p><h3 id="本地消息表-1"><a href="#本地消息表-1" class="headerlink" title="本地消息表"></a>本地消息表</h3><p>跨行转账可通过该方案实现。<br>用户 A 向用户 B 发起转账，首先系统会扣掉用户 A 账户中的金额，将该转账消息写入消息表中，如果事务执行失败则转账失败，如果转账成功，系统中会有定时轮询消息表，往 mq 中写入转账消息，失败重试。mq 消息会被实时消费并往用户 B 中账户增加转账金额，执行失败会不断重试。</p><img src="/2020/01/02/distributed-transaction/bank-transfer.jpg"><p>小米海外商城用户订单数据状态变更，会将变更状态记录消息表中，脚本将订单状态消息写入 mq，最终消费 mq 给用户发送邮件、短信、push 等。</p><h3 id="最大努力通知"><a href="#最大努力通知" class="headerlink" title="最大努力通知"></a>最大努力通知</h3><p>最大努力通知最常见的场景就是支付回调，支付服务收到第三方服务支付成功通知后，先更新自己库中订单支付状态，然后同步通知订单服务支付成功。如果此次同步通知失败，会通过异步脚步不断重试地调用订单服务的接口。</p><img src="/2020/01/02/distributed-transaction/try-best-notify.jpg"><p>小米海外商城目前除了支付回调外，最常用的场景是订单数据同步。例如系统 A、B 进行数据同步，当系统 A 发生订单数据变更，先将数据变更消息写入小米 notify 系统（作用等同 mq），然后 notify 系统异步处理该消息来调用系统 B 提供的接口并进行重试到最大次数。</p><img src="/2020/01/02/distributed-transaction/notify-callback.jpg"><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文介绍了分布式事务的一些基础理论，并对常用的分布式事务方案进行了讲解，在文章的后半部分主要给出了各种方案的常用场景。分布式事务本身就是一个技术难题，业务中具体使用哪种方案还是需要根据自身业务特点自行选择，每种方案在实际执行过程中需要考虑的点都非常多，复杂度较大，所以在非必要的情况下，分布式事务能不用就尽量不用。</p><p>参考：</p><ol><li><p>“分布式服务化系统一致性的“最佳实干” <a href="https://mp.weixin.qq.com/s/khAwfJvWcwgbAYbBHbU8aQ" target="_blank" rel="noopener">https://mp.weixin.qq.com/s/khAwfJvWcwgbAYbBHbU8aQ</a></p></li><li><p>“常用的分布式事务解决方案” <a href="https://blog.csdn.net/u010425776/article/details/79516298?tt_from=weixin&amp;utm_source=weixin&amp;utm_medium=toutiao_ios&amp;utm_campaign=client_share&amp;wxshare_count=1" target="_blank" rel="noopener">https://blog.csdn.net/u010425776/article/details/79516298?tt_from=weixin&amp;utm_source=weixin&amp;utm_medium=toutiao_ios&amp;utm_campaign=client_share&amp;wxshare_count=1</a></p></li><li><p>“深入分布式事务” <a href="http://www.codeceo.com/article/distributed-transaction.html" target="_blank" rel="noopener">http://www.codeceo.com/article/distributed-transaction.html</a></p></li><li><p>CAP 原则 <a href="https://baike.baidu.com/item/CAP%E5%8E%9F%E5%88%99" target="_blank" rel="noopener">https://baike.baidu.com/item/CAP%E5%8E%9F%E5%88%99</a></p></li><li><p>事务 <a href="https://baike.baidu.com/item/%E4%BA%8B%E5%8A%A1/5945882" target="_blank" rel="noopener">https://baike.baidu.com/item/%E4%BA%8B%E5%8A%A1/5945882</a></p></li><li><p>布式事务 <a href="https://baike.baidu.com/item/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1" target="_blank" rel="noopener">https://baike.baidu.com/item/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1</a></p></li><li><p>《Atomic Distributed Transactions: a RESTful Design》</p></li></ol><hr><p><strong>作者</strong></p><p>李文华，小米信息技术部海外商城组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com（武汉）</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;分布式事务，这一篇就够了&quot;&gt;&lt;a href=&quot;#分布式事务，这一篇就够了&quot; class=&quot;headerlink&quot; title=&quot;分布式事务，这一篇就够了&quot;&gt;&lt;/a&gt;分布式事务，这一篇就够了&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;[作者简介]&lt;/strong&gt; 李文华，小米
      
    
    </summary>
    
      <category term="分布式事务" scheme="https://xiaomi-info.github.io/categories/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/"/>
    
    
      <category term="分布式事务" scheme="https://xiaomi-info.github.io/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/"/>
    
      <category term="微服务" scheme="https://xiaomi-info.github.io/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
      <category term="MQ" scheme="https://xiaomi-info.github.io/tags/MQ/"/>
    
  </entry>
  
  <entry>
    <title>gRPC 系列——grpc 超时传递原理</title>
    <link href="https://xiaomi-info.github.io/2019/12/30/grpc-deadline/"/>
    <id>https://xiaomi-info.github.io/2019/12/30/grpc-deadline/</id>
    <published>2019-12-30T02:37:47.000Z</published>
    <updated>2021-05-24T03:39:30.258Z</updated>
    
    <content type="html"><![CDATA[<h1 id="gRPC-系列——grpc超时传递原理"><a href="#gRPC-系列——grpc超时传递原理" class="headerlink" title="gRPC 系列——grpc超时传递原理"></a>gRPC 系列——grpc超时传递原理</h1><p><strong>[作者简介]</strong> 郑伟，小米信息技术部架构组</p><h2 id="引子"><a href="#引子" class="headerlink" title="引子"></a>引子</h2><p>有个业务方反馈说日志中偶尔出现 xorm 抛出来的 <code>context deadline exceeded</code> 的报错，想咨询下是什么原因。业务方实现的 gRPC Handler 大概代码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s Svc)</span> <span class="title">BizHandler</span><span class="params">(ctx context.Context, r *projectv1.BizHandlerRequest)</span> <span class="params">(*projectv1.BizHandlerResponse, error)</span></span> &#123;</span><br><span class="line">  <span class="keyword">var</span> bean dao.Bean</span><br><span class="line">  <span class="comment">// 查询某个记录</span></span><br><span class="line">  <span class="keyword">if</span> err := db.W().Find(ctx, &amp;bean); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>目前业务方使用过的 xorm 是我们改造过的，函数签名中都添加了 ctx 参数，目的是为了接入 OpenTracing 做分布式追踪。</p><p>业务方反馈的这个 <code>context deadline execcded</code> 问题应该是出在查询 bean 的时候使用了带 timeout 的 ctx，如果这个 ctx 的 timeout 时间很短，有可能会在执行查询操作前就抛出 <code>context deadline execcded</code> 错误。</p><p>xorm 底层使用的标准包 <code>database/sql</code> ，最终执行查询的函数可能是 <code>ctxDriverQuery</code> 或 <code>ctxDriverStmtQuery</code> 这两个函数。以 <code>ctxDriverQuery</code> 为例：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ctxDriverQuery</span><span class="params">(ctx context.Context, queryerCtx driver.QueryerContext, queryer driver.Queryer, query <span class="keyword">string</span>, nvdargs []driver.NamedValue)</span> <span class="params">(driver.Rows, error)</span></span> &#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="keyword">select</span> &#123;</span><br><span class="line">  <span class="keyword">default</span>:</span><br><span class="line">  <span class="comment">// 若 ctx 超时或用户主动 cancel()，则抛出错误</span></span><br><span class="line">  <span class="comment">// 如果只因为 ctx 超时，此时错误就是 `context deadline execcded` </span></span><br><span class="line">  <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>, ctx.Err()</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 否则继续执行查询</span></span><br><span class="line">  <span class="keyword">return</span> queryer.Query(query, dargs)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>可以看到如果 <code>db.W().Find(ctx, &amp;bean)</code> 使用的 ctx 是设置了 timeout 的 ctx，那么是有可能在经过  xorm 的一些冗长的前置处理后，调用标准包的 <code>ctxDriver</code> 系列函数时产生 <code>context deadline execcded</code> 错误。</p><p>这个很好理解，但是业务方声称并未在 gRPC Handler 中主动为 context 设置 timeout。那么这个带 timeout 的 context 到底怎么产生的呢？</p><h2 id="谁构造的带-timeout-的-context？"><a href="#谁构造的带-timeout-的-context？" class="headerlink" title="谁构造的带 timeout 的 context？"></a>谁构造的带 timeout 的 context？</h2><p>业务方的 gRPC handler 中对传入的 ctx 明显未做 <code>context.WithTimeout()</code> 处理，我们把目光投向客户端。业务方的 service graph 是这样：</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ServiceA -&gt; ServiceB -&gt; ServiceC -&gt; xorm</span><br></pre></td></tr></table></figure><p>当前反馈查询 xorm 报错的是 ServiceC，我们找到 ServiceB 看了下调用 ServiceC gRPC Handler 代码。<strong>ServiceB 中 ctx 来自 ServiceA，ServiceB 中拿到 ctx 后，也并未设置 timeout</strong>。</p><p>看来设置 timeout 的只可能是整个调用链发起方（即 ServiceA），通过 Review 代码我们发现 ServiceA 发起 RPC 调用时，确实传入了带 timeout 的 ctx：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// InvokeServiceB 发起对 SerivceB 的 RPC 调用</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">InvokeServiceB</span><span class="params">()</span></span> &#123;</span><br><span class="line">...</span><br><span class="line">  ctx,_ := context.WithTimeout(ctx, <span class="number">3</span>*time.Second) <span class="comment">// 设置了 3 秒超时</span></span><br><span class="line">  response, err := grpcClient.ServicebBiz(ctx, request) <span class="comment">// 调用 ServiceB 的 RPC 时，使用的是上方定义的带 timeout 的 ctx</span></span><br><span class="line">...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们将 ServiceA 中发起 RPC 调用的 ctx 超时设置成 10 秒，再测试发现 ServiceC 反馈的 <code>context deadline execcded</code> 报错消失了。</p><h2 id="gRPC-超时如何做到跨进程传递？"><a href="#gRPC-超时如何做到跨进程传递？" class="headerlink" title="gRPC 超时如何做到跨进程传递？"></a>gRPC 超时如何做到跨进程传递？</h2><p>我们测试发现，不仅是 Go gRPC 服务之间超时可以传递（如果你拿到上游的 ctx 继续往下透传的话）。<strong>Go 和 Java 服务之间，超时也会随着调用链传递。</strong>那么 gRPC 的超时是如何做到跨进程跨语言传递的？</p><p>有朋友可能想到了 metadata，是否 gRPC 请求链上游设置了超时后，gRPC 框架底层将过期时间放在 metadata 里了？遗憾的是我们打印 metadata 后发现并未发现 timeout 字段踪迹。那么超时时间到底是怎样传递的呢？以 <code>grpc-go</code> 源码为例，我们来找下线索。</p><p>我们知道 gRPC 基于 HTTP2，HTTP2 传输的最小单位是 Frame（帧）。HTTP2 的帧包含很多类型：<code>DATA Frame</code>、<code>HEADERS Frame</code>、<code>PRIORITY Frame</code>、<code>RST_STREAM Frame</code>、<code>CONTINUATON Frame</code> 等。一个 HTTP2 请求/响应可以被拆成多个帧并行发送，每一帧都有一个 StreamID 来标记属于哪个 Stream。服务端收到 Frame 后，根据 StreamID 组装出原始请求数据。</p><img src="/2019/12/30/grpc-deadline/2019-12-30-17-53-41.png"><!-- [](2019-12-30-17-53-41.png) --><p>对于 gRPC 而言，Data Frame 用来存放请求的 response payload；Headers Frame 可用来存放一些需要进行跨进程传递的数据，比如 <code>grpc-status（RPC 请求状态码）</code> 、 <code>:path（RPC 完整路径）</code> 等。那么超时时间是否也通过 HEADERS Frame 传递呢？</p><h3 id="客户端设置-timeout"><a href="#客户端设置-timeout" class="headerlink" title="客户端设置 timeout"></a>客户端设置 timeout</h3><p>我们知道，用户定义好 protobuf 并通过 protoc 生成桩代码后，桩代码中已经包含了 gRPCCient 接口的实现，每一个在 protobuf 中定义的 RPC，底层都会通过 ClientConn. Invoke 向服务端发起调用：</p><p>比如对于这样的 protobuf：</p><figure class="highlight protobuf"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">syntax = &quot;proto3&quot;;</span><br><span class="line"></span><br><span class="line">package proto;</span><br><span class="line"></span><br><span class="line">service DemoService &#123;</span><br><span class="line">  rpc SayHi(HiRequest) returns (HiResponse);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">message HiRequest &#123;</span><br><span class="line">  string name = 1;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">message HiResponse &#123;</span><br><span class="line">  string message = 1;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>生成的桩代码中已经包含了 Client 实现：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> DemoServiceClient <span class="keyword">interface</span> &#123;</span><br><span class="line">  SayHiOK(ctx context.Context, in *HiRequest, opts ...grpc.CallOption) (*HiResponse, error)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> demoServiceClient <span class="keyword">struct</span> &#123;</span><br><span class="line">  cc *grpc.ClientConn</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewDemoServiceClient</span><span class="params">(cc *grpc.ClientConn)</span> <span class="title">DemoServiceClient</span></span> &#123;</span><br><span class="line">  <span class="keyword">return</span> &amp;demoServiceClient&#123;cc&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *demoServiceClient)</span> <span class="title">SayHiOK</span><span class="params">(ctx context.Context, in *HiRequest, opts ...grpc.CallOption)</span> <span class="params">(*HiResponse, error)</span></span> &#123;</span><br><span class="line">  out := <span class="built_in">new</span>(HiResponse)</span><br><span class="line">  <span class="comment">// 调用 grpc.ClientConn.Invoke() 函数，grpc.ClientConn.Invoke() 内部最终会调用 invoke() 函数</span></span><br><span class="line">  err := c.cc.Invoke(ctx, <span class="string">"/proto.DemoService/SayHi"</span>, in, out, opts...)</span><br><span class="line">  <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> out, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>客户端发起 gRPC 请求时，最终会调用 invoke() 方法，invoke() 源码大概如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">invoke</span><span class="params">(ctx context.Context, method <span class="keyword">string</span>, req, reply <span class="keyword">interface</span>&#123;&#125;, cc *ClientConn, opts ...CallOption)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">  <span class="comment">// 构造 clientStream</span></span><br><span class="line">  cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...)</span><br><span class="line">  <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 发送 RPC 请求</span></span><br><span class="line">  <span class="keyword">if</span> err := cs.SendMsg(req); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> err</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> cs.RecvMsg(reply)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们看下 newClientStream 源码，newClientStream 源码比较复杂，我们挑重点看：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">newClientStream</span><span class="params">(ctx context.Context, desc *StreamDesc, cc *ClientConn, method <span class="keyword">string</span>, opts ...CallOption)</span> <span class="params">(_ ClientStream, err error)</span></span> &#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// 等待 resolver 解析出可用地址</span></span><br><span class="line">  <span class="keyword">if</span> err := cc.waitForResolvedAddrs(ctx); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 构造 *clientStream</span></span><br><span class="line">  cs := &amp;clientStream&#123;</span><br><span class="line">    callHdr:      callHdr,</span><br><span class="line">    ctx:          ctx,</span><br><span class="line">    ...</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="comment">// 构造新的 *csAttempt，newAttemptLocked 内部会获取 grpc.ClientTransport 并赋值给 *csAttemp.t</span></span><br><span class="line">  <span class="keyword">if</span> err := cs.newAttemptLocked(sh, trInfo); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    cs.finish(err)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">  <span class="keyword">return</span> cs, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中 csAttempt.newStream 实现如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> csAttempt <span class="keyword">struct</span> &#123;</span><br><span class="line">  cs   *clientStream</span><br><span class="line">  t    transport.ClientTransport <span class="comment">// 客户端 Transport</span></span><br><span class="line">  s    *transport.Stream         <span class="comment">// 真正处理RPC 的 Stream</span></span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(a *csAttempt)</span> <span class="title">newStream</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// 通过 Transport.NewStream 构造RPC Stream</span></span><br><span class="line">  s, err := a.t.NewStream(cs.ctx, cs.callHdr)</span><br><span class="line">  cs.attempt.s = s</span><br><span class="line">  ...</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>transport.ClientTransport</code> 是一个接口，gRPC 内部 <code>internal/transport.http2Client</code> 实现了此接口。<br><code>http2Client.NewStream()</code> 源码如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t *http2Client)</span> <span class="title">NewStream</span><span class="params">(ctx context.Context, callHdr *CallHdr)</span> <span class="params">(_ *Stream, err error)</span></span> &#123;</span><br><span class="line">  ctx = peer.NewContext(ctx, t.getPeer())</span><br><span class="line">  headerFields, err := t.createHeaderFields(ctx, callHdr)</span><br><span class="line">  ...</span><br><span class="line">  hdr := &amp;headerFrame&#123;</span><br><span class="line">    hf:        headerFields,</span><br><span class="line">    endStream: <span class="literal">false</span>,</span><br><span class="line">    ...</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">  <span class="keyword">for</span> &#123;</span><br><span class="line">    success, err := t.controlBuf.executeAndPut(<span class="function"><span class="keyword">func</span><span class="params">(it <span class="keyword">interface</span>&#123;&#125;)</span> <span class="title">bool</span></span> &#123;</span><br><span class="line">      <span class="keyword">if</span> !checkForStreamQuota(it) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">if</span> !checkForHeaderListSize(it) &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">true</span></span><br><span class="line">    &#125;, hdr)</span><br><span class="line">    ...</span><br><span class="line">  <span class="keyword">return</span> s, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>createHeaderFields</code> 实现如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t *http2Client)</span> <span class="title">createHeaderFields</span><span class="params">(ctx context.Context, callHdr *CallHdr)</span> <span class="params">([]hpack.HeaderField, error)</span></span> &#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// 如果透传过来的 ctx 被设置了 timeout/deadline，则在 HTTP2 headers frame 中添加 grpc-timeout 字段，</span></span><br><span class="line">  <span class="comment">// grpc-timeout 字段值被转化成 XhYmZs 字符串形式的超时时间</span></span><br><span class="line">  <span class="keyword">if</span> dl, ok := ctx.Deadline(); ok &#123;</span><br><span class="line">    timeout := time.Until(dl)</span><br><span class="line">    headerFields = <span class="built_in">append</span>(headerFields, hpack.HeaderField&#123;Name: <span class="string">"grpc-timeout"</span>, Value: encodeTimeout(timeout)&#125;)</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">  <span class="keyword">return</span> headerFields, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">可以看到客户端发起请求时，如果设置了带 timeout 的ctx，则会导致底层 HTTP2 HEADERS Frame 中追加 <span class="string">`grpc-timeout`</span> 字段</span><br></pre></td></tr></table></figure><h3 id="服务端解析-timeout"><a href="#服务端解析-timeout" class="headerlink" title="服务端解析 timeout"></a>服务端解析 timeout</h3><p>服务端通过 <code>Serve</code> 方法启动 grpc Server，监听来自客户端连接。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Server)</span> <span class="title">Serve</span><span class="params">(lis net.Listener)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="keyword">for</span> &#123;</span><br><span class="line">    <span class="comment">// 接收客户端的连接</span></span><br><span class="line">    rawConn, err := lis.Accept()</span><br><span class="line">    ...</span><br><span class="line">    s.serveWG.Add(<span class="number">1</span>)</span><br><span class="line">    <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">      <span class="comment">// 对每一个客户端的连接单独开一个协程来处理</span></span><br><span class="line">      s.handleRawConn(rawConn)</span><br><span class="line">      s.serveWG.Done()</span><br><span class="line">    &#125;()</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Server)</span> <span class="title">handleRawConn</span><span class="params">(rawConn net.Conn)</span></span> &#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// 构造 HTTP2 Transport</span></span><br><span class="line">  st := s.newHTTP2Transport(conn, authInfo)</span><br><span class="line">  <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="comment">// 处理 HTTP2 Stream</span></span><br><span class="line">    s.serveStreams(st)</span><br><span class="line">    s.removeConn(st)</span><br><span class="line">  &#125;()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *Server)</span> <span class="title">serveStreams</span><span class="params">(st transport.ServerTransport)</span></span> &#123;</span><br><span class="line">  <span class="keyword">defer</span> st.Close()</span><br><span class="line">  <span class="keyword">var</span> wg sync.WaitGroup</span><br><span class="line">  <span class="comment">// http2Server 实现了 transport.ServerTransport 接口，此处会调用 http2Server.HandleSteams方法</span></span><br><span class="line">  <span class="comment">// st.HandleStreams 方法签名中第一个参数 handle func(stream *transport.Stream) &#123;&#125;为函数类型，</span></span><br><span class="line">  <span class="comment">// handle 随后会在 operateHeaders 中被调用</span></span><br><span class="line">  st.HandleStreams(<span class="function"><span class="keyword">func</span><span class="params">(stream *transport.Stream)</span></span> &#123;</span><br><span class="line">    wg.Add(<span class="number">1</span>)</span><br><span class="line">    <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">      <span class="keyword">defer</span> wg.Done()</span><br><span class="line">      <span class="comment">// 解析出 gPRC Service, gRPC method, gRPC request message，执行注册到 gRPC.Server 中的 RPC 方法</span></span><br><span class="line">      s.handleStream(st, stream, s.traceInfo(st, stream))</span><br><span class="line">    &#125;()</span><br><span class="line">  &#125;, ...)</span><br><span class="line">  wg.Wait()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// http2Server.HandleStreams 会调用传入的 handle 处理 HTTP2 Stream</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t *http2Server)</span> <span class="title">HandleStreams</span><span class="params">(handle <span class="keyword">func</span>(*Stream)</span>, <span class="title">traceCtx</span> <span class="title">func</span><span class="params">(context.Context, <span class="keyword">string</span>)</span> <span class="title">context</span>.<span class="title">Context</span>)</span> &#123;</span><br><span class="line">  <span class="keyword">defer</span> <span class="built_in">close</span>(t.readerDone)</span><br><span class="line">  <span class="keyword">for</span> &#123;</span><br><span class="line">    t.controlBuf.throttle()</span><br><span class="line">    frame, err := t.framer.fr.ReadFrame()</span><br><span class="line">    ...</span><br><span class="line">    <span class="keyword">switch</span> frame := frame.(<span class="keyword">type</span>) &#123;</span><br><span class="line">    <span class="comment">// 如果是 Headers 帧，则调用 operateHeaders 方法处理 Headers</span></span><br><span class="line">    <span class="keyword">case</span> *http2.MetaHeadersFrame:</span><br><span class="line">      <span class="keyword">if</span> t.operateHeaders(frame, handle, traceCtx) &#123;</span><br><span class="line">        t.Close()</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">      &#125;</span><br><span class="line">    <span class="comment">// 如果是 Data 帧，则调用 handleData 方法处理</span></span><br><span class="line">    <span class="keyword">case</span> *http2.DataFrame:</span><br><span class="line">      t.handleData(frame)</span><br><span class="line">      ...</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// operateHeaders 解析 Headers 帧</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(t *http2Server)</span> <span class="title">operateHeaders</span><span class="params">(frame *http2.MetaHeadersFrame, handle <span class="keyword">func</span>(*Stream)</span>, <span class="title">traceCtx</span> <span class="title">func</span><span class="params">(context.Context, <span class="keyword">string</span>)</span> <span class="title">context</span>.<span class="title">Context</span>) <span class="params">(fatal <span class="keyword">bool</span>)</span></span> &#123;</span><br><span class="line">  <span class="comment">// 从HTTP2 Headers 帧中获取 StreamID</span></span><br><span class="line">  streamID := frame.Header().StreamID</span><br><span class="line">  state := &amp;decodeState&#123;</span><br><span class="line">    serverSide: <span class="literal">true</span>,</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="comment">// 从HTTP2 Headers 帧中解析出Header。如果其中包含 grpc-timeout HEADER，</span></span><br><span class="line">  <span class="comment">// 则解析出其值并赋值给 state.data.timeout，并将 state.data.timeoutSet 设成 true</span></span><br><span class="line">  <span class="keyword">if</span> err := state.decodeHeader(frame); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> se, ok := status.FromError(err); ok &#123;</span><br><span class="line">      ...</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  buf := newRecvBuffer()</span><br><span class="line">  <span class="comment">// 构造 HTTP2 Stream</span></span><br><span class="line">  s := &amp;Stream&#123;</span><br><span class="line">    id:             streamID,</span><br><span class="line">    st:             t,</span><br><span class="line">    buf:            buf,</span><br><span class="line">    fc:             &amp;inFlow&#123;limit: <span class="keyword">uint32</span>(t.initialWindowSize)&#125;,</span><br><span class="line">    recvCompress:   state.data.encoding,</span><br><span class="line">    method:         state.data.method,</span><br><span class="line">    contentSubtype: state.data.contentSubtype,</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// 如果 state.data.timeoutSet 为 true，则构造一个新的带 timeout 的 ctx 覆盖原 s.ctx</span></span><br><span class="line">  <span class="comment">// s.ctx 最终会透传到用户实现的 gRPC Handler 中，参与业务逻辑处理</span></span><br><span class="line">  <span class="comment">// 见 server.go 中 processUnaryRPC 内:</span></span><br><span class="line">  <span class="comment">//    ctx := NewContextWithServerTransportStream(stream.Context(), stream)</span></span><br><span class="line">  <span class="comment">//    reply, appErr := md.Handler(srv.server, ctx, df, s.opts.unaryInt)</span></span><br><span class="line">  <span class="comment">// 此处不再赘述</span></span><br><span class="line">  <span class="keyword">if</span> state.data.timeoutSet &#123;</span><br><span class="line">    s.ctx, s.cancel = context.WithTimeout(t.ctx, state.data.timeout)</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    s.ctx, s.cancel = context.WithCancel(t.ctx)</span><br><span class="line">  &#125;</span><br><span class="line">  ...</span><br><span class="line">  t.controlBuf.put(&amp;registerStream&#123;</span><br><span class="line">    streamID: s.id,</span><br><span class="line">    wq:       s.wq,</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="comment">// 调用 serveStreams 定义好的 handle，执行gRPC调用</span></span><br><span class="line">  handle(s)</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">false</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p><code>decodeHeader</code> 会遍历 frame 中所有 Fields，并调用 <code>processHeaderField</code> 对 HTTP2 HEADERS 帧中的特定的 Field 进行处理。</p><ul><li>比如可以从 <code>:path</code> 中解析出包含 protobuf package、service name 和 RPC method name 的完整路径；</li><li>比如可以从 <code>grpc-timeout</code> 中解析出上游传递过来的 timeout；</li></ul><p><code>decodeHeader</code> 内部实现如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(d *decodeState)</span> <span class="title">decodeHeader</span><span class="params">(frame *http2.MetaHeadersFrame)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">  ...</span><br><span class="line">  <span class="comment">// 遍历Headers帧，解析Field</span></span><br><span class="line">  <span class="keyword">for</span> _, hf := <span class="keyword">range</span> frame.Fields &#123;</span><br><span class="line">    d.processHeaderField(hf)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(d *decodeState)</span> <span class="title">processHeaderField</span><span class="params">(f hpack.HeaderField)</span></span> &#123;</span><br><span class="line">  <span class="keyword">switch</span> f.Name &#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 解析出 grpc-timeout</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">"grpc-timeout"</span>:</span><br><span class="line">      d.data.timeoutSet = <span class="literal">true</span></span><br><span class="line">      <span class="keyword">var</span> err error</span><br><span class="line">      <span class="keyword">if</span> d.data.timeout, err = decodeTimeout(f.Value); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        d.data.grpcErr = status.Errorf(codes.Internal, <span class="string">"transport: malformed time-out: %v"</span>, err)</span><br><span class="line">      &#125;</span><br><span class="line">    ...</span><br><span class="line">    <span class="comment">// 解析出 grpc 带 protobuf package path、Service name、RPC method name 的完整路径</span></span><br><span class="line">    <span class="comment">// 形如 /package.service/method</span></span><br><span class="line">    <span class="keyword">case</span> <span class="string">":path"</span>:</span><br><span class="line">      d.data.method = f.Value</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>至此可以看到，gRPC 框架确实是通过 HTTP2 HEADERS Frame 中的 <code>grpc-timeout</code> 字段来实现跨进程传递超时时间。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><ul><li>客户端客户端发起 RPC 调用时传入了带 timeout 的 ctx</li><li>gRPC 框架底层通过 HTTP2 协议发送 RPC 请求时，将 timeout 值写入到 <code>grpc-timeout</code> HEADERS Frame 中</li><li>服务端接收 RPC 请求时，gRPC 框架底层解析 HTTP2 HEADERS 帧，读取 <code>grpc-timeout</code> 值，并覆盖透传到实际处理 RPC 请求的业务 gPRC Handle 中</li><li>如果此时服务端又发起对其他 gRPC 服务的调用，且使用的是透传的 ctx，这个 timeout 会减去在本进程中耗时，从而导致这个 timeout 传递到下一个 gRPC 服务端时变短，这样即实现了所谓的 <code>超时传递</code> 。目前这个功能测试发现在 <code>grpc-go</code> 和 <code>grpc-java</code> 中实现， <code>grpc-python</code> 貌似暂未实现此功能（见 <a href="https://github.com/grpc/grpc/issues/18358" target="_blank" rel="noopener">https://github.com/grpc/grpc/issues/18358</a>）。</li></ul><hr><p><strong>作者</strong></p><p>郑伟，小米信息技术部架构组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com（武汉）</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;gRPC-系列——grpc超时传递原理&quot;&gt;&lt;a href=&quot;#gRPC-系列——grpc超时传递原理&quot; class=&quot;headerlink&quot; title=&quot;gRPC 系列——grpc超时传递原理&quot;&gt;&lt;/a&gt;gRPC 系列——grpc超时传递原理&lt;/h1&gt;&lt;p&gt;&lt;s
      
    
    </summary>
    
      <category term="Golang" scheme="https://xiaomi-info.github.io/categories/Golang/"/>
    
    
      <category term="微服务" scheme="https://xiaomi-info.github.io/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/"/>
    
      <category term="Golang" scheme="https://xiaomi-info.github.io/tags/Golang/"/>
    
      <category term="gRPC" scheme="https://xiaomi-info.github.io/tags/gRPC/"/>
    
  </entry>
  
  <entry>
    <title>浅析 MySQL 的隐式转换</title>
    <link href="https://xiaomi-info.github.io/2019/12/24/mysql-implicit-conversion/"/>
    <id>https://xiaomi-info.github.io/2019/12/24/mysql-implicit-conversion/</id>
    <published>2019-12-24T02:19:20.000Z</published>
    <updated>2021-05-24T03:39:30.263Z</updated>
    
    <content type="html"><![CDATA[<h1 id="浅析-MySQL-的隐式转换"><a href="#浅析-MySQL-的隐式转换" class="headerlink" title="浅析 MySQL 的隐式转换"></a>浅析 MySQL 的隐式转换</h1><p><strong>[作者简介]</strong> 陈晓，信息部订单组研发工程师，目前主要负责小米订单中台业务。</p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>跟大家一块看下 MySQL 的隐式转换相关知识，主要是相等操作时，先看两个可能都遇到过的场景。</p><p>表</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> <span class="string">`t1`</span> (</span><br><span class="line">  <span class="string">`c1`</span> <span class="built_in">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">''</span>,</span><br><span class="line">  <span class="keyword">KEY</span> <span class="string">`idx_c1`</span> (<span class="string">`c1`</span>)</span><br><span class="line">) <span class="keyword">ENGINE</span>=<span class="keyword">InnoDB</span> <span class="keyword">DEFAULT</span> <span class="keyword">CHARSET</span>=utf8;</span><br><span class="line"></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t1 <span class="keyword">values</span>(<span class="string">'1234567890123456789'</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t1 <span class="keyword">values</span>(<span class="string">'123456789012345678'</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t1 <span class="keyword">values</span>(<span class="string">'123456789012345677'</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t1 <span class="keyword">values</span>(<span class="string">'12345678901234567'</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t1 <span class="keyword">values</span>(<span class="string">'12345678901234568'</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t1 <span class="keyword">values</span>(<span class="string">'123456789012345'</span>);</span><br></pre></td></tr></table></figure><p>数据</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">+<span class="comment">---------------------+</span></span><br><span class="line">| c1                  |</span><br><span class="line">+<span class="comment">---------------------+</span></span><br><span class="line">| 123456789012345     |</span><br><span class="line">| 12345678901234567   |</span><br><span class="line">| 12345678901234568   |</span><br><span class="line">| 123456789012345677  |</span><br><span class="line">| 123456789012345678  |</span><br><span class="line">| 1234567890123456789 |</span><br><span class="line">+<span class="comment">---------------------+</span></span><br></pre></td></tr></table></figure><ul><li>场景一</li></ul><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t1 <span class="keyword">where</span> c1=<span class="number">123456789012345678</span>;</span><br><span class="line"></span><br><span class="line">+<span class="comment">--------------------+</span></span><br><span class="line">| c1                 |</span><br><span class="line">+<span class="comment">--------------------+</span></span><br><span class="line">| 123456789012345677 |</span><br><span class="line">| 123456789012345678 |</span><br><span class="line">+<span class="comment">--------------------+</span></span><br></pre></td></tr></table></figure><p>查询出两条数据，并且 123456789012345677 不等于查询条件</p><ul><li>场景二</li></ul><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">explain</span> <span class="keyword">select</span> * <span class="keyword">from</span> t1 <span class="keyword">where</span> c1=<span class="number">12345678901234567</span>;</span><br><span class="line"></span><br><span class="line">+<span class="comment">----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+--------------------------+</span></span><br><span class="line">| id | select_type | table | partitions | type  | possible_keys | key    | key_len | ref  | rows | filtered | Extra                    |</span><br><span class="line">+<span class="comment">----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+--------------------------+</span></span><br><span class="line">|  1 | SIMPLE      | t1    | NULL       | index | idx_c1        | idx_c1 | 767     | NULL |    6 |    16.67 | Using where; Using index |</span><br><span class="line">+<span class="comment">----+-------------+-------+------------+-------+---------------+--------+---------+------+------+----------+--------------------------+</span></span><br></pre></td></tr></table></figure><p>全表扫描</p><h2 id="什么是隐式类型转换"><a href="#什么是隐式类型转换" class="headerlink" title="什么是隐式类型转换"></a>什么是隐式类型转换</h2><p>MySQL 中对隐式转换的定义：</p><blockquote><p>When an operator is used with operands of different types, type conversion occurs to make the operands compatible. Some conversions occur implicitly. </p></blockquote><p>翻译下<br> 当操作符与不同类型的操作数一起使用时，会发生类型转换以使操作数兼容。</p><p>举个例子，当操作数是字符跟数字时， MySQL 会根据使用的操作符，转换字符到数字或转换数字成字符。</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; SELECT 1+'1';</span><br><span class="line">        -&gt; 2</span><br><span class="line">mysql&gt; SELECT CONCAT(2,' test');</span><br><span class="line">        -&gt; '2 test'</span><br></pre></td></tr></table></figure><h3 id="比较操作时-MySQL-隐式类型转换规则"><a href="#比较操作时-MySQL-隐式类型转换规则" class="headerlink" title="比较操作时 MySQL 隐式类型转换规则"></a>比较操作时 MySQL 隐式类型转换规则</h3><p>官方文档中有一段对比较操作时， MySQL 的类型转换做了说明：</p><blockquote><p>If one or both arguments are NULL, the result of the comparison is NULL, except for the NULL-safe &lt;=&gt; equality comparison operator. For NULL &lt;=&gt; NULL, the result is true. No conversion is needed.</p></blockquote><blockquote><p>If both arguments in a comparison operation are strings, they are compared as strings.</p></blockquote><blockquote><p>If both arguments are integers, they are compared as integers.</p></blockquote><blockquote><p>Hexadecimal values are treated as binary strings if not compared to a number.</p></blockquote><blockquote><p>If one of the arguments is a TIMESTAMP or DATETIME column and the other argument is a constant, the constant is converted to a timestamp before the comparison is performed. This is done to be more ODBC-friendly. This is not done for the arguments to IN(). To be safe, always use complete datetime, date, or time strings when doing comparisons. For example, to achieve best results when using BETWEEN with date or time values, use CAST() to explicitly convert the values to the desired data type.</p></blockquote><blockquote><p>A single-row subquery from a table or tables is not considered a constant. For example, if a subquery returns an integer to be compared to a DATETIME value, the comparison is done as two integers. The integer is not converted to a temporal value. To compare the operands as DATETIME values, use CAST() to explicitly convert the subquery value to DATETIME.</p></blockquote><blockquote><p>If one of the arguments is a decimal value, comparison depends on the other argument. The arguments are compared as decimal values if the other argument is a decimal or integer value, or as floating-point values if the other argument is a floating-point value.</p></blockquote><blockquote><p>In all other cases, the arguments are compared as floating-point (real) numbers. For example, a comparison of string and numeric operands takes places as a comparison of floating-point numbers.</p></blockquote><p>翻译为中文就是：</p><ul><li>两个参数至少有一个是 NULL 时，比较的结果也是 NULL，例外是使用 &lt;=&gt; 对两个 NULL 做比较时会返回 1，这两种情况都不需要做类型转换</li><li>两个参数都是字符串，会按照字符串来比较，不做类型转换</li><li>两个参数都是整数，按照整数来比较，不做类型转换</li><li>十六进制的值和非数字做比较时，会被当做二进制串</li><li>有一个参数是 TIMESTAMP 或 DATETIME，并且另外一个参数是常量，常量会被转换为 timestamp</li><li>有一个参数是 decimal 类型，如果另外一个参数是 decimal 或者整数，会将整数转换为 decimal 后进行比较，如果另外一个参数是浮点数，则会把 decimal 转换为浮点数进行比较</li><li>所有其他情况下，两个参数都会被转换为浮点数再进行比较</li></ul><p>我们用十六进制的来验证看下</p><h3 id="十六进制"><a href="#十六进制" class="headerlink" title="十六进制"></a>十六进制</h3><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> <span class="string">`t5`</span> (</span><br><span class="line">  <span class="string">`c1`</span> <span class="built_in">varchar</span>(<span class="number">255</span>) <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">''</span>,</span><br><span class="line">  c2 <span class="built_in">int</span>,</span><br><span class="line">  <span class="keyword">KEY</span> <span class="string">`idx_c1`</span> (<span class="string">`c1`</span>),</span><br><span class="line">  <span class="keyword">KEY</span> idx_c2(c2)</span><br><span class="line">) <span class="keyword">ENGINE</span>=<span class="keyword">InnoDB</span> <span class="keyword">DEFAULT</span> <span class="keyword">CHARSET</span>=utf8;</span><br><span class="line"></span><br><span class="line"><span class="keyword">alter</span> <span class="keyword">table</span> t5 <span class="keyword">add</span> <span class="keyword">column</span> c3 <span class="built_in">bigint</span>;</span><br><span class="line"><span class="keyword">alter</span> <span class="keyword">table</span> t5 <span class="keyword">add</span> <span class="keyword">index</span> idx_c3(c3);</span><br><span class="line"><span class="keyword">alter</span> <span class="keyword">table</span> t5 <span class="keyword">drop</span> <span class="keyword">index</span> idx_c3;</span><br><span class="line"></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t5(c1, c2) <span class="keyword">values</span>(<span class="string">'$'</span>, <span class="number">16</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t5(c1, c2) <span class="keyword">values</span>(<span class="string">'!'</span>, <span class="number">12</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t5(c1, c2) <span class="keyword">values</span>(<span class="string">'1'</span>, <span class="number">16</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t5(c1, c2) <span class="keyword">values</span>(<span class="string">' 1'</span>, <span class="number">11</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t5(c1, c2) <span class="keyword">values</span>(<span class="string">'1a'</span>, <span class="number">10</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t5 <span class="keyword">where</span> c1 = <span class="number">0x24</span>;</span><br><span class="line">+<span class="comment">----+------+</span></span><br><span class="line">| c1 | c2   |</span><br><span class="line">+<span class="comment">----+------+</span></span><br><span class="line">| $  |   16 |</span><br><span class="line">+<span class="comment">----+------+</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">explain</span> <span class="keyword">select</span> * <span class="keyword">from</span> t5 <span class="keyword">where</span> c1 = <span class="number">0x24</span>;</span><br><span class="line">+<span class="comment">----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+</span></span><br><span class="line">| id | select_type | table | partitions | type | possible_keys | key    | key_len | ref   | rows | filtered | Extra |</span><br><span class="line">+<span class="comment">----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+</span></span><br><span class="line">|  1 | SIMPLE      | t5    | NULL       | ref  | idx_c1        | idx_c1 | 767     | const |    1 |   100.00 | NULL  |</span><br><span class="line">+<span class="comment">----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+</span></span><br></pre></td></tr></table></figure><p>通过查询</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> <span class="keyword">HEX</span>(<span class="string">'$'</span>);</span><br><span class="line">+<span class="comment">----------+</span></span><br><span class="line">| HEX('$') |</span><br><span class="line">+<span class="comment">----------+</span></span><br><span class="line">| 24       |</span><br><span class="line">+<span class="comment">----------+</span></span><br></pre></td></tr></table></figure><img src="/2019/12/24/mysql-implicit-conversion/asciifull.gif"><p>或者查 ASCII 码表可以看到，<strong>$</strong> 对应的 16 进制正是 0x24。</p><p>查询数字</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t5 <span class="keyword">where</span> c2 = <span class="number">0x10</span>;</span><br><span class="line">+<span class="comment">----+------+</span></span><br><span class="line">| c1 | c2   |</span><br><span class="line">+<span class="comment">----+------+</span></span><br><span class="line">| $  |   16 |</span><br><span class="line">| 1  |   16 |</span><br><span class="line">+<span class="comment">----+------+</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">explain</span> <span class="keyword">select</span> * <span class="keyword">from</span> t5 <span class="keyword">where</span> c2 = <span class="number">0x10</span>;</span><br><span class="line">+<span class="comment">----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+</span></span><br><span class="line">| id | select_type | table | partitions | type | possible_keys | key    | key_len | ref   | rows | filtered | Extra |</span><br><span class="line">+<span class="comment">----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+</span></span><br><span class="line">|  1 | SIMPLE      | t5    | NULL       | ref  | idx_c2        | idx_c2 | 5       | const |    2 |   100.00 | NULL  |</span><br><span class="line">+<span class="comment">----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+</span></span><br></pre></td></tr></table></figure><h3 id="根据规则看下示例"><a href="#根据规则看下示例" class="headerlink" title="根据规则看下示例"></a>根据规则看下示例</h3><p>根据上面的规则，分析开始的示例，c1 字段类型是 varchar，等号右边常量是数字，MySQL 会把 c1 字段的值和数字常量转换成浮点数比较。</p><p>当字段是字符串跟数字比较时，MySQL 不能使用索引来加快查询。比如下面的 SQL 语句，str_col 是 varchar 型的字段</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> * <span class="keyword">FROM</span> tbl_name <span class="keyword">WHERE</span> str_col=<span class="number">1</span>;</span><br></pre></td></tr></table></figure><p>正如示例的执行计划显示的那样，进行了全表扫描。原因在于字符串转浮点型的时候所使用的算法，在 MySQL 里以下’1’, ‘ 1’, ‘1a’字符串转换成数字后都是 1。</p><p>我们根据 t5 表做下查询</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t5 <span class="keyword">where</span> c1 = <span class="number">1</span>;</span><br><span class="line">+<span class="comment">----+------+</span></span><br><span class="line">| c1 | c2   |</span><br><span class="line">+<span class="comment">----+------+</span></span><br><span class="line">| 1  |   16 |</span><br><span class="line">|  1 |   11 |</span><br><span class="line">| 1a |   10 |</span><br><span class="line">+<span class="comment">----+------+</span></span><br></pre></td></tr></table></figure><p>看下官方文档</p><blockquote><p>The server includes dtoa, a conversion library that provides the basis for improved conversion &gt; between string or DECIMAL values and approximate-value (FLOAT/DOUBLE) numbers:</p><p>Consistent conversion results across platforms, which eliminates, for example, Unix versus &gt; Windows conversion differences.</p><p>Accurate representation of values in cases where results previously did not provide sufficient &gt; precision, such as for values close to IEEE limits.</p><p>Conversion of numbers to string format with the best possible precision. The precision of dtoa &gt; is always the same or better than that of the standard C library functions.</p></blockquote><p>MySQL 里使用了 dtoa 这个转换类库来实现字符跟浮点数的相互转换，一是提高了浮点数的精度（相比 c 标准库）, 二是保持平台一致性。</p><p>在 MySQL 里浮点数之间的比较是近似的。因为 MySQL 里浮点数是不精确的，它的精度位只有 53 比特，大于 53 比特的位被“四舍五入”。这会导致一些违反直观的结果。<br>比如下面两个 SQL</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; SELECT '1801537632024345812' = 1801537632024345812;</span><br><span class="line">        -&gt; 1</span><br><span class="line">mysql&gt; SELECT '1801537632024345812' = 1801537632024345813;</span><br><span class="line">        -&gt; 1</span><br></pre></td></tr></table></figure><p>比较结果都是相等，这里就发生了浮点数转换。<br>我们来看下到底发生了什么。</p><p>浮点数</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="string">'1801537632024345812'</span>+<span class="number">0.0</span>;</span><br><span class="line">+<span class="comment">---------------------------+</span></span><br><span class="line">| '1801537632024345812'+0.0 |</span><br><span class="line">+<span class="comment">---------------------------+</span></span><br><span class="line">|     1.8015376320243459e18 |</span><br><span class="line">+<span class="comment">---------------------------+</span></span><br></pre></td></tr></table></figure><p>转换成浮点数后对应的整数</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="keyword">cast</span>(<span class="string">'1801537632024345812'</span> + <span class="number">0.0</span> <span class="keyword">as</span> <span class="keyword">unsigned</span>);</span><br><span class="line">+<span class="comment">-----------------------------------------------+</span></span><br><span class="line">| cast('1801537632024345812' + 0.0 as unsigned) |</span><br><span class="line">+<span class="comment">-----------------------------------------------+</span></span><br><span class="line">|                           1801537632024345856 |</span><br><span class="line">+<span class="comment">-----------------------------------------------+</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">SELECT</span> <span class="keyword">cast</span>(<span class="string">'1801537632024345813'</span> + <span class="number">0.0</span> <span class="keyword">as</span> <span class="keyword">unsigned</span>);</span><br><span class="line">+<span class="comment">-----------------------------------------------+</span></span><br><span class="line">| cast('1801537632024345813' + 0.0 as unsigned) |</span><br><span class="line">+<span class="comment">-----------------------------------------------+</span></span><br><span class="line">|                           1801537632024345856 |</span><br><span class="line">+<span class="comment">-----------------------------------------------+</span></span><br></pre></td></tr></table></figure><p>转换成浮点数后在转换成整数，对应的都是<br><strong>1801537632024345856</strong></p><p>分别对应的二进制</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">1801537632024345812 -&gt; 11001000000000101100011101110011011100100111100111100（53）11010100</span><br><span class="line">1801537632024345856 -&gt; 11001000000000101100011101110011011100100111100111101（53）00000000</span><br></pre></td></tr></table></figure><p>对比可以看到 53 位后的数据被丢掉，因为第 54 位是 1，所以有一个进位。</p><p>再看下我们的示例数字转换成浮点数后分别对应的数字</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> c1, <span class="keyword">cast</span>(c1 + <span class="number">0.0</span> <span class="keyword">as</span> <span class="keyword">unsigned</span>) <span class="keyword">from</span> t1;</span><br><span class="line">+<span class="comment">---------------------+----------------------------+</span></span><br><span class="line">| c1                  | cast(c1 + 0.0 as unsigned) |</span><br><span class="line">+<span class="comment">---------------------+----------------------------+</span></span><br><span class="line">| $                   |                          0 |</span><br><span class="line">| 123456789012345     |            123456789012345 |</span><br><span class="line">| 12345678901234567   |          12345678901234568 |</span><br><span class="line">| 12345678901234568   |          12345678901234568 |</span><br><span class="line">| 123456789012345677  |         123456789012345680 |</span><br><span class="line">| 123456789012345678  |         123456789012345680 |</span><br><span class="line">| 1234567890123456789 |        1234567890123456768 |</span><br><span class="line">+<span class="comment">---------------------+----------------------------+</span></span><br></pre></td></tr></table></figure><h2 id="其他情况"><a href="#其他情况" class="headerlink" title="其他情况"></a>其他情况</h2><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">CREATE</span> <span class="keyword">TABLE</span> <span class="string">`t3`</span> (</span><br><span class="line">  <span class="string">`c1`</span> <span class="built_in">bigint</span> <span class="keyword">NOT</span> <span class="literal">NULL</span> <span class="keyword">DEFAULT</span> <span class="string">'0'</span></span><br><span class="line">) <span class="keyword">ENGINE</span>=<span class="keyword">InnoDB</span> <span class="keyword">DEFAULT</span> <span class="keyword">CHARSET</span>=utf8;</span><br><span class="line"></span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t3 <span class="keyword">values</span>(<span class="number">1234567890123456789</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t3 <span class="keyword">values</span>(<span class="number">123456789012345678</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t3 <span class="keyword">values</span>(<span class="number">123456789012345677</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t3 <span class="keyword">values</span>(<span class="number">12345678901234567</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t3 <span class="keyword">values</span>(<span class="number">12345678901234568</span>);</span><br><span class="line"><span class="keyword">insert</span> <span class="keyword">into</span> t3 <span class="keyword">values</span>(<span class="number">123456789012345</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">select</span> * <span class="keyword">from</span> t3 <span class="keyword">where</span> c1=<span class="string">'123456789012345678'</span>;</span><br><span class="line">+<span class="comment">--------------------+</span></span><br><span class="line">| c1                 |</span><br><span class="line">+<span class="comment">--------------------+</span></span><br><span class="line">| 123456789012345678 |</span><br><span class="line">+<span class="comment">--------------------+</span></span><br></pre></td></tr></table></figure><p>此处结果跟前面的规则貌似不一致？<br>在 MySQL 执行的 prepare 阶段，在设置比较方法之前，满足一些调教的情况下会做一次类型转换</p><p>mysql server 处理调用栈</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">Item_bool_func2::convert_constant_arg item_cmpfunc.cc:<span class="number">636</span></span><br><span class="line">Item_bool_func2::fix_length_and_dec item_cmpfunc.cc:<span class="number">700</span></span><br><span class="line">Item_func::fix_fields item_func.cc:<span class="number">253</span></span><br><span class="line">st_select_lex::setup_conds sql_resolver.cc:<span class="number">1190</span></span><br><span class="line">st_select_lex::prepare sql_resolver.cc:<span class="number">212</span></span><br><span class="line">handle_query sql_select.cc:<span class="number">139</span></span><br><span class="line">execute_sqlcom_select sql_parse.cc:<span class="number">5155</span></span><br><span class="line">mysql_execute_command sql_parse.cc:<span class="number">2826</span></span><br><span class="line">mysql_parse sql_parse.cc:<span class="number">5584</span></span><br><span class="line">dispatch_command sql_parse.cc:<span class="number">1491</span></span><br><span class="line">do_command sql_parse.cc:<span class="number">1032</span></span><br><span class="line">handle_connection connection_handler_per_thread.cc:<span class="number">313</span></span><br><span class="line">pfs_spawn_thread pfs.cc:<span class="number">2197</span></span><br><span class="line">start_thread <span class="number">0x00007f1bb48ce6ba</span></span><br><span class="line">clone <span class="number">0x00007f1bb3d6341d</span></span><br></pre></td></tr></table></figure><p>转换方法</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> Item_bool_func2::convert_constant_arg(THD *thd, Item *field, Item **item)</span><br><span class="line">&#123;</span><br><span class="line">  <span class="keyword">if</span> (field-&gt;real_item()-&gt;type() != FIELD_ITEM)</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">  Item_field *field_item= (Item_field*) (field-&gt;real_item());</span><br><span class="line">  <span class="keyword">if</span> (field_item-&gt;field-&gt;can_be_compared_as_longlong() &amp;&amp;</span><br><span class="line">      !(field_item-&gt;is_temporal_with_date() &amp;&amp;</span><br><span class="line">      (*item)-&gt;result_type() == STRING_RESULT))</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="keyword">if</span> (convert_constant_item(thd, field_item, item))</span><br><span class="line">    &#123;</span><br><span class="line">      cmp.set_cmp_func(<span class="keyword">this</span>, tmp_arg, tmp_arg + <span class="number">1</span>, INT_RESULT);</span><br><span class="line">      field-&gt;cmp_context= (*item)-&gt;cmp_context= INT_RESULT;</span><br><span class="line">      <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>item 和 field 分别对应的数据</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">item = &#123;Item ** | <span class="number">0x7f1b34006f88</span>&#125; <span class="number">0x7f1b34006f88</span></span><br><span class="line"> *item = &#123;PTI_text_literal_text_string * | <span class="number">0x7f1b34006538</span>&#125; <span class="number">0x7f1b34006538</span></span><br><span class="line"></span><br><span class="line"> </span><br><span class="line">field = &#123;Field_longlong * | <span class="number">0x7f1b34abfb20</span>&#125; <span class="number">0x7f1b34abfb20</span></span><br><span class="line"> Field_num = &#123;Field_num&#125;</span><br></pre></td></tr></table></figure><p>转换后 item 对应的数据</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">a2 = &#123;Item ** | <span class="number">0x7f2b3c006f88</span>&#125; <span class="number">0x7f2b3c006f88</span></span><br><span class="line"> *a2 = &#123;Item_int_with_ref * | <span class="number">0x7f2b3c007320</span>&#125; <span class="number">0x7f2b3c007320</span></span><br><span class="line">  Item_int = &#123;Item_int&#125; </span><br><span class="line">  cached_field_type = &#123;enum_field_types&#125; MYSQL_TYPE_LONGLONG</span><br><span class="line">  ref = &#123;PTI_text_literal_text_string * | <span class="number">0x7f2b3c006538</span>&#125; <span class="number">0x7f2b3c006538</span></span><br></pre></td></tr></table></figure><p>看下 <code>convert_constant_item</code> 的注释</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment">  Convert a constant item to an int and replace the original item.</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">    The function converts a constant expression or string to an integer.</span></span><br><span class="line"><span class="comment">    On successful conversion the original item is substituted for the</span></span><br><span class="line"><span class="comment">    result of the item evaluation.</span></span><br><span class="line"><span class="comment">    This is done when comparing DATE/TIME of different formats and</span></span><br><span class="line"><span class="comment">    also when comparing bigint to strings (in which case strings</span></span><br><span class="line"><span class="comment">    are converted to bigints).</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">  @param  thd             thread handle</span></span><br><span class="line"><span class="comment">  @param  field           item will be converted using the type of this field</span></span><br><span class="line"><span class="comment">  @param[in,out] item     reference to the item to convert</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">  @note</span></span><br><span class="line"><span class="comment">    This function is called only at prepare stage.</span></span><br><span class="line"><span class="comment">    As all derived tables are filled only after all derived tables</span></span><br><span class="line"><span class="comment">    are prepared we do not evaluate items with subselects here because</span></span><br><span class="line"><span class="comment">    they can contain derived tables and thus we may attempt to use a</span></span><br><span class="line"><span class="comment">    table that has not been populated yet.</span></span><br><span class="line"><span class="comment"></span></span><br><span class="line"><span class="comment">  @retval</span></span><br><span class="line"><span class="comment">    0  Can't convert item</span></span><br><span class="line"><span class="comment">  @retval</span></span><br><span class="line"><span class="comment">    1  Item was replaced with an integer version of the item</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">bool</span> <span class="title">convert_constant_item</span><span class="params">(THD *thd, Item_field *field_item,</span></span></span><br><span class="line"><span class="function"><span class="params">                                  Item **item)</span></span></span><br></pre></td></tr></table></figure><p>大体意思是把常量对象（表达式或者字符串）转换成整数并替换，此方法只有在比较不同的日期格式和比较 bigint 跟字符串时才有效。注意上面括号中的这句 <code>in which case strings    are converted to bigints</code></p><p>上面的转换规则并没有覆盖所有的情况，或者说一些情况 MySQL 在比较之前做了优化，比如此处的查询，已经不在是数字跟字符串比较了。</p><p>上面规则里面还有句 <code>This is not done for the arguments to IN()</code> ，此处不再分析，留下大家有兴趣自行看下， MySQL 又做了哪些优化。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>避免发生隐式类型转换，隐式转换的类型主要有字段类型不一致、in 参数包含多个类型、字符集类型或校对规则不一致等</p><p>隐式类型转换可能导致无法使用索引、查询结果不准确等，因此在使用时必须仔细甄别</p><p>数字类型的建议在字段定义时就定义为 int 或者 bigint，表关联时关联字段必须保持类型、字符集、校对规则都一致</p><p>由于历史原因，需要兼容旧的设计，可以使用 MySQL 的类型转换函数 cast 和 convert，来明确的进行转换。</p><p>如有错误之处，还望大家指正。</p><h2 id="附录"><a href="#附录" class="headerlink" title="附录"></a>附录</h2><h3 id="关于浮点数的问题"><a href="#关于浮点数的问题" class="headerlink" title="关于浮点数的问题"></a>关于浮点数的问题</h3><p><a href="https://dev.mysql.com/doc/refman/5.7/en/problems-with-float.html" target="_blank" rel="noopener">Problems with Floating-Point Values</a></p><h3 id="关于进制问题"><a href="#关于进制问题" class="headerlink" title="关于进制问题"></a>关于进制问题</h3><p> Round-to-Even for Floating Point</p><p>Round-To-Even 在于 To-Up , To-Down, To-towards-Zero 对比中，在一定数据量基础上，更加精准。To-Up 的平均值比真实数值偏大，To-Down 偏小。</p><p>例如有效数字超出规定数位的多余数字是 1001，它大于超出规定最低位的一半，故最低位进 1。<br>如果多余数字是 0111，它小于最低位的一半，则舍掉多余数字（截断尾数、截尾）即可。<br>对于多余数字是 1000、正好是最低位一半的特殊情况，最低位为 0 则舍掉多余位，最低位为 1 则进位 1、使得最低位仍为 0（偶数）。</p><p>注意这里说明的数位都是指二进制数。</p><p>对于第三种情况，来看下两个例子：</p><figure class="highlight"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="keyword">cast</span>(<span class="string">'1801537632024345728'</span> + <span class="number">0.0</span> <span class="keyword">as</span> <span class="keyword">unsigned</span>);</span><br><span class="line">+<span class="comment">-----------------------------------------------+</span></span><br><span class="line">| cast('1801537632024345728' + 0.0 as unsigned) |</span><br><span class="line">+<span class="comment">-----------------------------------------------+</span></span><br><span class="line">|                           1801537632024345600 |</span><br><span class="line">+<span class="comment">-----------------------------------------------+</span></span><br><span class="line"></span><br><span class="line">1801537632024345728 -&gt; 1100100000000010110001110111001101110010011110011110010000000</span><br><span class="line">1801537632024345600 -&gt; 1100100000000010110001110111001101110010011110011110000000000</span><br><span class="line"></span><br><span class="line"><span class="keyword">SELECT</span> <span class="keyword">cast</span>(<span class="string">'1801537632024345984'</span> + <span class="number">0.0</span> <span class="keyword">as</span> <span class="keyword">unsigned</span>);</span><br><span class="line">+<span class="comment">-----------------------------------------------+</span></span><br><span class="line">| cast('1801537632024345984' + 0.0 as unsigned) |</span><br><span class="line">+<span class="comment">-----------------------------------------------+</span></span><br><span class="line">|                           1801537632024346112 |</span><br><span class="line">+<span class="comment">-----------------------------------------------+</span></span><br><span class="line"></span><br><span class="line">1801537632024345984 -&gt; 1100100000000010110001110111001101110010011110011110110000000</span><br><span class="line">1801537632024346112 -&gt; 1100100000000010110001110111001101110010011110011111000000000</span><br></pre></td></tr></table></figure><hr><p><strong>作者</strong></p><p>陈晓，小米信息技术部订单组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com（武汉）</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;浅析-MySQL-的隐式转换&quot;&gt;&lt;a href=&quot;#浅析-MySQL-的隐式转换&quot; class=&quot;headerlink&quot; title=&quot;浅析 MySQL 的隐式转换&quot;&gt;&lt;/a&gt;浅析 MySQL 的隐式转换&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;[作者简介]&lt;/strong
      
    
    </summary>
    
    
      <category term="MySQL" scheme="https://xiaomi-info.github.io/tags/MySQL/"/>
    
      <category term="隐式转换" scheme="https://xiaomi-info.github.io/tags/%E9%9A%90%E5%BC%8F%E8%BD%AC%E6%8D%A2/"/>
    
  </entry>
  
  <entry>
    <title>一次线上线程池任务问题处理历程</title>
    <link href="https://xiaomi-info.github.io/2019/12/19/theadpool-rejected-task/"/>
    <id>https://xiaomi-info.github.io/2019/12/19/theadpool-rejected-task/</id>
    <published>2019-12-19T02:00:00.000Z</published>
    <updated>2021-05-24T03:39:30.325Z</updated>
    
    <content type="html"><![CDATA[<h1 id="一次线上线程池任务问题处理历程"><a href="#一次线上线程池任务问题处理历程" class="headerlink" title="一次线上线程池任务问题处理历程"></a>一次线上线程池任务问题处理历程</h1><p><strong>[作者简介]</strong> 王日华，小米信息技术部订单组研发工程师，目前主要负责小米订单中台业务。</p><h2 id="一、前言"><a href="#一、前言" class="headerlink" title="一、前言"></a>一、前言</h2><p>在一次新功能上线过程中，出现线程池提交任务抛出 RejectedExecutionException 异常，即任务提交执行了拒绝策略的操作。查看业务情况和线程池配置，发现并行执行的任务数是小于线程池最大线程数的，为此展开了一次线程池问题排查历程。</p><h2 id="二、业务情景"><a href="#二、业务情景" class="headerlink" title="二、业务情景"></a>二、业务情景</h2><h3 id="2-1-任务描述"><a href="#2-1-任务描述" class="headerlink" title="2.1. 任务描述"></a>2.1. 任务描述</h3><p>每次执行一组任务，一组任务最多有 15 个，多线程执行，每个线程处理一个任务；每次执行完一组任务后，再执行下一组，不存在上一组的任务和下一组一起执行的情况。</p><h3 id="2-2-任务提交流程"><a href="#2-2-任务提交流程" class="headerlink" title="2.2. 任务提交流程"></a>2.2. 任务提交流程</h3><img src="/2019/12/19/theadpool-rejected-task/theadpool-rejected-task-01.png"><h3 id="2-3-线程池配置"><a href="#2-3-线程池配置" class="headerlink" title="2.3. 线程池配置"></a>2.3. 线程池配置</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"executor"</span> <span class="attr">class</span>=<span class="string">"org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"corePoolSize"</span> <span class="attr">value</span>=<span class="string">"14"</span>/&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"maxPoolSize"</span> <span class="attr">value</span>=<span class="string">"30"</span>/&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"queueCapacity"</span> <span class="attr">value</span>=<span class="string">"1"</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure><h2 id="三、出现问题"><a href="#三、出现问题" class="headerlink" title="三、出现问题"></a>三、出现问题</h2><p>执行过程中出现 RejectedExecutionException 异常，由于是采用的是默认拒绝策略 AbortPolicy，因此，可以明确知道任务是提交到线程池后，线程池资源已满，导致任务被拒绝。</p><h2 id="四、问题排查"><a href="#四、问题排查" class="headerlink" title="四、问题排查"></a>四、问题排查</h2><h3 id="4-1-检查线程池配置"><a href="#4-1-检查线程池配置" class="headerlink" title="4.1. 检查线程池配置"></a>4.1. 检查线程池配置</h3><p>任务最多 15 个一组，核心线程有 14 个，阻塞队列是 1，最大线程 30，理论上 14 个核心线程+1 个阻塞队列即可完成一组任务，连非核心线程都无需使用，为什么会出现线程被占满的情况？</p><h3 id="4-2-检查业务代码"><a href="#4-2-检查业务代码" class="headerlink" title="4.2. 检查业务代码"></a>4.2. 检查业务代码</h3><p>检查是否存在线程池被多处使用，或者有多批任务被同时执行的情况，并没有发现错误；</p><h3 id="4-3-线下重现"><a href="#4-3-线下重现" class="headerlink" title="4.3. 线下重现"></a>4.3. 线下重现</h3><ul><li>配置线程池</li></ul><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"executor"</span> <span class="attr">class</span>=<span class="string">"org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"corePoolSize"</span> <span class="attr">value</span>=<span class="string">"14"</span>/&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"maxPoolSize"</span> <span class="attr">value</span>=<span class="string">"30"</span>/&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="name">property</span> <span class="attr">name</span>=<span class="string">"queueCapacity"</span> <span class="attr">value</span>=<span class="string">"1"</span>/&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">bean</span>&gt;</span></span><br></pre></td></tr></table></figure><ul><li>建立 demo 代码</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@RunWith</span>(SpringRunner.class)</span><br><span class="line"><span class="meta">@SpringBootTest</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">SpringBootStartApplicationTests</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Resource</span></span><br><span class="line">    <span class="keyword">private</span> ThreadPoolTaskExecutor executor;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Test</span></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">contextLoads</span><span class="params">()</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">        <span class="comment">// 一共 10 批任务</span></span><br><span class="line">        <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; <span class="number">10</span>; i++) &#123;</span><br><span class="line">            <span class="comment">// 每次执行一批任务</span></span><br><span class="line">            doOnceTasks();</span><br><span class="line">            System.out.println(<span class="string">"---------------------------------------"</span> + i);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 每次完成 15 个任务后，再进行下一次任务</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">doOnceTasks</span><span class="params">()</span></span>&#123;</span><br><span class="line">        List&lt;Future&gt; futureList = Lists.newArrayListWithCapacity(<span class="number">15</span>);</span><br><span class="line">        <span class="keyword">for</span>(<span class="keyword">int</span> i = <span class="number">0</span>; i &lt; <span class="number">15</span>; ++i)&#123;</span><br><span class="line">            Future future = executor.submit(()-&gt;&#123;</span><br><span class="line">                <span class="comment">// 随机睡 0-5 秒</span></span><br><span class="line">                <span class="keyword">int</span> sec = <span class="keyword">new</span> Double(Math.random() * <span class="number">5</span>).intValue();</span><br><span class="line">                LockSupport.parkNanos(sec * <span class="number">1000</span> * <span class="number">1000</span> * <span class="number">1000</span>);</span><br><span class="line">                System.out.println(Thread.currentThread().getName() + <span class="string">"  end"</span>);</span><br><span class="line">            &#125;);</span><br><span class="line">            futureList.add(future);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 等待所有任务执行结束</span></span><br><span class="line">        <span class="keyword">for</span>(Future future : futureList)&#123;</span><br><span class="line">            <span class="keyword">try</span> &#123;</span><br><span class="line">                future.get();</span><br><span class="line">            &#125; <span class="keyword">catch</span> (Exception e) &#123;</span><br><span class="line">                e.printStackTrace();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>异常重现</li></ul><img src="/2019/12/19/theadpool-rejected-task/theadpool-rejected-task-06.png"><h2 id="五、线程池源码阅读"><a href="#五、线程池源码阅读" class="headerlink" title="五、线程池源码阅读"></a>五、线程池源码阅读</h2><h3 id="5-1-线程池执行任务流程"><a href="#5-1-线程池执行任务流程" class="headerlink" title="5.1. 线程池执行任务流程"></a>5.1. 线程池执行任务流程</h3><ul><li>当工作线程数 &lt; corePoolSize 时，新创建一个新线程执行新提交任务，即使此时线程池中存在空闲线程；</li><li>当工作线程数 == corePoolSize 时，新提交任务将被放入 workQueue 中；</li><li>当 workQueue 已满，且工作线程数 &lt; maximumPoolSize 时，新提交任务会创建新的非核心线程执行任务；</li><li>当 workQueue 已满，且 工作线程数==maximumPoolSize 时，新提交任务由 RejectedExecutionHandler 处理；</li></ul><img src="/2019/12/19/theadpool-rejected-task/theadpool-rejected-task-02.png"><h3 id="5-2-execute-线程池提交任务源码"><a href="#5-2-execute-线程池提交任务源码" class="headerlink" title="5.2. execute 线程池提交任务源码"></a>5.2. execute 线程池提交任务源码</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ThreadPoolExecutor</span></span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">execute</span><span class="params">(Runnable command)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 提交任务不能为 null</span></span><br><span class="line">        <span class="keyword">if</span> (command == <span class="keyword">null</span>)</span><br><span class="line">            <span class="keyword">throw</span> <span class="keyword">new</span> NullPointerException();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 获取控制位 ctl 的值</span></span><br><span class="line">        <span class="keyword">int</span> c = ctl.get();</span><br><span class="line">        <span class="comment">// work 线程数 &lt; 核心线程数</span></span><br><span class="line">        <span class="keyword">if</span> (workerCountOf(c) &lt; corePoolSize) &#123;</span><br><span class="line">            <span class="comment">// 直接创建核心线程，执行任务</span></span><br><span class="line">            <span class="keyword">if</span> (addWorker(command, <span class="keyword">true</span>))</span><br><span class="line">                <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line">            <span class="comment">/*</span></span><br><span class="line"><span class="comment">                因为没有使用锁，可能会出现并发创建核心线程；</span></span><br><span class="line"><span class="comment">                走到这里，说明核心线程已经创建满了，此时，重新获取控制位 ctl 的值</span></span><br><span class="line"><span class="comment">              */</span></span><br><span class="line">            c = ctl.get();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 如果线程池还是 RUNNING 状态，并且任务成功提交到阻塞队列中</span></span><br><span class="line">        <span class="keyword">if</span> (isRunning(c) &amp;&amp; workQueue.offer(command)) &#123;</span><br><span class="line">            <span class="keyword">int</span> recheck = ctl.get();</span><br><span class="line">            <span class="comment">// double-check，再检查一次线程池状态</span></span><br><span class="line">            <span class="comment">// 如果线程池变成非 RUNNING 状态，则回滚刚才新加的任务</span></span><br><span class="line">            <span class="keyword">if</span> (! isRunning(recheck) &amp;&amp; remove(command))</span><br><span class="line">                <span class="comment">// 从阻塞队列中移除任务成功，使用拒绝策略执行任务</span></span><br><span class="line">                reject(command);</span><br><span class="line"></span><br><span class="line">            <span class="comment">// 如果工作线程数==0，则添加一个线程</span></span><br><span class="line">            <span class="comment">// 主要是兼容核心线程数==0 的情况</span></span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span> (workerCountOf(recheck) == <span class="number">0</span>)</span><br><span class="line">                addWorker(<span class="keyword">null</span>, <span class="keyword">false</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">/*</span></span><br><span class="line"><span class="comment">            到达这里，则说明核心线程数已满，且阻塞队列已满</span></span><br><span class="line"><span class="comment">            尝试创建非核心线程执行任务</span></span><br><span class="line"><span class="comment">        */</span></span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span> (!addWorker(command, <span class="keyword">false</span>))</span><br><span class="line">            <span class="comment">// 非核心线程创建失败了，说明是线程数以达到 maximumPoolSize，此时执行拒绝策略</span></span><br><span class="line">            reject(command);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="5-3-addWorker-添加-worker-线程"><a href="#5-3-addWorker-添加-worker-线程" class="headerlink" title="5.3. addWorker 添加 worker 线程"></a>5.3. addWorker 添加 worker 线程</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ThreadPoolExecutor</span></span>&#123;</span><br><span class="line">    <span class="comment">/**</span></span><br><span class="line"><span class="comment">     * 添加一个 worker 线程</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> firstTask 第一个要执行的 task</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@param</span> core 是否是核心线程</span></span><br><span class="line"><span class="comment">     * <span class="doctag">@return</span> 创建成功还是失败</span></span><br><span class="line"><span class="comment">     */</span></span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">boolean</span> <span class="title">addWorker</span><span class="params">(Runnable firstTask, <span class="keyword">boolean</span> core)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// 定义了一个 retry 标签</span></span><br><span class="line">        retry:</span><br><span class="line">        <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">            <span class="comment">// 获取控制位</span></span><br><span class="line">            <span class="keyword">int</span> c = ctl.get();</span><br><span class="line">            <span class="comment">// 获取运行状态</span></span><br><span class="line">            <span class="keyword">int</span> rs = runStateOf(c);</span><br><span class="line"></span><br><span class="line">            <span class="comment">/**</span></span><br><span class="line"><span class="comment">             * rs &gt;= SHUTDOWN：即非 RUNNING 状态，只有 RUNNING &lt; SHUTDOWN</span></span><br><span class="line"><span class="comment">             * ! (rs == SHUTDOWN &amp;&amp; firstTask == null &amp;&amp; ! workQueue.isEmpty())</span></span><br><span class="line"><span class="comment">             *      等价于 非 SHUTDOWN 态 ||  firstTask != null || workQueue.isEmpty()</span></span><br><span class="line"><span class="comment">             *      非 SHUTDOWN 态 == true：SHUTDOWN 态之后的状态，都不允许再添加 worker 线程了，直接返回 false；</span></span><br><span class="line"><span class="comment">             *      非 SHUTDOWN 态 == false || (firstTask != null) == true：SHUTDOWN 状态下，不允许再添加任务了，返回 false；</span></span><br><span class="line"><span class="comment">             *      非 SHUTDOWN 态 == false || (firstTask != null) == false || workQueue.isEmpty() == true：SHUTDOWN 状态，没提交新任务，阻塞队列又是空的，没必要再添加线程了</span></span><br><span class="line"><span class="comment">             */</span></span><br><span class="line">            <span class="keyword">if</span> (rs &gt;= SHUTDOWN &amp;&amp;</span><br><span class="line">                ! (rs == SHUTDOWN &amp;&amp;</span><br><span class="line">                   firstTask == <span class="keyword">null</span> &amp;&amp;</span><br><span class="line">                   ! workQueue.isEmpty()))</span><br><span class="line">                <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"></span><br><span class="line">            <span class="comment">// CAS 创建 worker 线程</span></span><br><span class="line">            <span class="keyword">for</span> (;;) &#123;</span><br><span class="line">                <span class="comment">// 获取线程数</span></span><br><span class="line">                <span class="keyword">int</span> wc = workerCountOf(c);</span><br><span class="line">                <span class="comment">/*</span></span><br><span class="line"><span class="comment">                当前线程数大于最大值</span></span><br><span class="line"><span class="comment">                    或</span></span><br><span class="line"><span class="comment">                当前创建的是核心线程，但线程数量已经&gt;=核心线程数</span></span><br><span class="line"><span class="comment">                    或</span></span><br><span class="line"><span class="comment">                当前创建非核心线程，但线程数量已经&gt;=maximumPoolSize</span></span><br><span class="line"><span class="comment">                */</span></span><br><span class="line">                <span class="keyword">if</span> (wc &gt;= CAPACITY ||</span><br><span class="line">                    wc &gt;= (core ? corePoolSize : maximumPoolSize))</span><br><span class="line">                    <span class="comment">// 不创建，直接返回 false</span></span><br><span class="line">                    <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line"></span><br><span class="line">                <span class="comment">// cas 修改 ctl 中的线程数，线程数+1</span></span><br><span class="line">                <span class="keyword">if</span> (compareAndIncrementWorkerCount(c))</span><br><span class="line">                    <span class="comment">// cas 修改成功，break goto 结束循环（不会再进入标签下的循环）</span></span><br><span class="line">                    <span class="keyword">break</span> retry;</span><br><span class="line"></span><br><span class="line">                <span class="comment">// 达到这里，说明 cas 增加线程数 1 失败了，此时进行尝试</span></span><br><span class="line">                c = ctl.get();</span><br><span class="line">                <span class="comment">// 先判断一下线程池状态有没有改变，如果改变了，则 continue goto（会再进入标签下的循环）</span></span><br><span class="line">                <span class="comment">// 跳转到最外层的循环，重新检测线程池的状态值</span></span><br><span class="line">                <span class="keyword">if</span> (runStateOf(c) != rs)</span><br><span class="line">                    <span class="keyword">continue</span> retry;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">boolean</span> workerStarted = <span class="keyword">false</span>;</span><br><span class="line">        <span class="keyword">boolean</span> workerAdded = <span class="keyword">false</span>;</span><br><span class="line">        Worker w = <span class="keyword">null</span>;</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 创建 worker 对象</span></span><br><span class="line">            w = <span class="keyword">new</span> Worker(firstTask);</span><br><span class="line">            <span class="comment">// 获取 worker 的线程</span></span><br><span class="line">            <span class="keyword">final</span> Thread t = w.thread;</span><br><span class="line">            <span class="keyword">if</span> (t != <span class="keyword">null</span>) &#123;</span><br><span class="line">                <span class="comment">// 加锁</span></span><br><span class="line">                <span class="keyword">final</span> ReentrantLock mainLock = <span class="keyword">this</span>.mainLock;</span><br><span class="line">                mainLock.lock();</span><br><span class="line">                <span class="keyword">try</span> &#123;</span><br><span class="line">                    <span class="comment">// 获取线程池状态</span></span><br><span class="line">                    <span class="keyword">int</span> rs = runStateOf(ctl.get());</span><br><span class="line"></span><br><span class="line">                    <span class="comment">/*</span></span><br><span class="line"><span class="comment">                     线程池是 RUNNING 状态</span></span><br><span class="line"><span class="comment">                        或</span></span><br><span class="line"><span class="comment">                     SHUTDOWN 态 且 firstTask == null（这种情况是需要创建线程，消费队列中剩余的任务）</span></span><br><span class="line"><span class="comment">                      */</span></span><br><span class="line">                    <span class="keyword">if</span> (rs &lt; SHUTDOWN ||</span><br><span class="line">                        (rs == SHUTDOWN &amp;&amp; firstTask == <span class="keyword">null</span>)) &#123;</span><br><span class="line">                        <span class="comment">// 线程是活动状态，则不合法，因为线程是刚创建的，应该是 NEW 状态</span></span><br><span class="line">                        <span class="keyword">if</span> (t.isAlive())</span><br><span class="line">                            <span class="keyword">throw</span> <span class="keyword">new</span> IllegalThreadStateException();</span><br><span class="line"></span><br><span class="line">                        <span class="comment">// 将 worker 添加到 list 中</span></span><br><span class="line">                        workers.add(w);</span><br><span class="line">                        <span class="comment">// largestPoolSize 记录该线程池使用过程中，达到最大的线程数</span></span><br><span class="line">                        <span class="keyword">int</span> s = workers.size();</span><br><span class="line">                        <span class="keyword">if</span> (s &gt; largestPoolSize)</span><br><span class="line">                            largestPoolSize = s;</span><br><span class="line">                        <span class="comment">// worker 添加成功，workerAdded 置为 true</span></span><br><span class="line">                        workerAdded = <span class="keyword">true</span>;</span><br><span class="line">                    &#125;</span><br><span class="line">                &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">                    mainLock.unlock();</span><br><span class="line">                &#125;</span><br><span class="line"></span><br><span class="line">                <span class="comment">// worker 添加成功，此时就可以启动线程</span></span><br><span class="line">                <span class="keyword">if</span> (workerAdded) &#123;</span><br><span class="line">                    t.start();</span><br><span class="line">                    <span class="comment">// 启动线程成功，workerStarted 置为 true</span></span><br><span class="line">                    workerStarted = <span class="keyword">true</span>;</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            <span class="comment">// 如果 worker 启动失败，则移除它</span></span><br><span class="line">            <span class="keyword">if</span> (! workerStarted)</span><br><span class="line">                <span class="comment">// workers 移除新加的 worker，并在 ctl 中将 work 线程数量-1</span></span><br><span class="line">                addWorkerFailed(w);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> workerStarted;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="六、问题定位"><a href="#六、问题定位" class="headerlink" title="六、问题定位"></a>六、问题定位</h2><h3 id="6-1-定位执行拒绝策略入口"><a href="#6-1-定位执行拒绝策略入口" class="headerlink" title="6.1. 定位执行拒绝策略入口"></a>6.1. 定位执行拒绝策略入口</h3><p>执行拒绝策略的位置只有这两个地方，在这两个地方打上断点，执行 demo，结果发现拒绝策略是在第二处执行的；</p><img src="/2019/12/19/theadpool-rejected-task/theadpool-rejected-task-07.png"><h3 id="6-2-定位执行拒绝策略原因"><a href="#6-2-定位执行拒绝策略原因" class="headerlink" title="6.2. 定位执行拒绝策略原因"></a>6.2. 定位执行拒绝策略原因</h3><p>进入 addWorker 方法，只有这两个地方返回 false，创建线程失败，打断点，执行 demo，发现是在第二处返回 false 的；</p><img src="/2019/12/19/theadpool-rejected-task/theadpool-rejected-task-08.png"><h2 id="七、问题确认"><a href="#七、问题确认" class="headerlink" title="七、问题确认"></a>七、问题确认</h2><p>确实是创建的 worker 线程已经达到最大线程数，无法再创建，然后执行拒绝策略的，为什么会被创建到最大呢，每组任务最大只有 15 个，为什么会用到非核心线程？</p><h2 id="八、定位原因"><a href="#八、定位原因" class="headerlink" title="八、定位原因"></a>八、定位原因</h2><h3 id="8-1-分析-execute-方法"><a href="#8-1-分析-execute-方法" class="headerlink" title="8.1. 分析 execute 方法"></a>8.1. 分析 execute 方法</h3><p>在添加非核心线程前，先尝试将任务放到阻塞队列中，如果阻塞队列已满，则尝试添加非核心线程，也就是说，创建非核心线程时：workQueue.offer(command) == false，即阻塞队列已满；</p><img src="/2019/12/19/theadpool-rejected-task/theadpool-rejected-task-09.png"><h3 id="8-2-猜测原因"><a href="#8-2-猜测原因" class="headerlink" title="8.2. 猜测原因"></a>8.2. 猜测原因</h3><p>因为我们阻塞队列只有 1，会不会提交任务的速度比线程从阻塞队列取任务的速度快，进而导致创建非核心线程执行任务，最终的结果就是：在多批任务之后，再无非核心线程可创建，导致执行拒绝策略。</p><img src="/2019/12/19/theadpool-rejected-task/theadpool-rejected-task-03.png"><h3 id="8-3-原因验证"><a href="#8-3-原因验证" class="headerlink" title="8.3. 原因验证"></a>8.3. 原因验证</h3><h4 id="8-3-1-阻塞队列选择"><a href="#8-3-1-阻塞队列选择" class="headerlink" title="8.3.1 阻塞队列选择"></a>8.3.1 阻塞队列选择</h4><p>查看 Spring 的 ThreadPoolTaskExecutor 源码，发现如果阻塞队列数量&gt;0，则使用 LinkedBlockingQueue，否则使用 SynchronousQueue。</p><img src="/2019/12/19/theadpool-rejected-task/theadpool-rejected-task-10.png"><h4 id="8-3-2-LinkedBlockingQueue"><a href="#8-3-2-LinkedBlockingQueue" class="headerlink" title="8.3.2 LinkedBlockingQueue"></a>8.3.2 LinkedBlockingQueue</h4><ul><li>查看 LinkedBlockingQueue#take 方法，如果队列已空，则所有取元素的线程会阻塞在一个 Lock 的 notEmpty 等待条件上，等有元素入队时，只会调用 signal 方法唤醒一个线程取元素，而不是所有线程。</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">LinkedBlockingQueue</span></span>&#123;</span><br><span class="line">    <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">signalNotEmpty</span><span class="params">()</span> </span>&#123;</span><br><span class="line">        <span class="keyword">final</span> ReentrantLock takeLock = <span class="keyword">this</span>.takeLock;</span><br><span class="line">        <span class="comment">// 加锁</span></span><br><span class="line">        takeLock.lock();</span><br><span class="line">        <span class="keyword">try</span> &#123;</span><br><span class="line">            <span class="comment">// 唤醒一个 take 线程</span></span><br><span class="line">            notEmpty.signal();</span><br><span class="line">        &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">            takeLock.unlock();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><ul><li>因为一个线程从唤醒到执行是有一段时间间隔的，阻塞被唤醒后，还要等待获取 cpu 时间片，而主线程一直在发布任务，此时就会造成队列中的元素来不及消费，只能创建非核心线程消费的现象。</li></ul><h2 id="九、解决方式"><a href="#九、解决方式" class="headerlink" title="九、解决方式"></a>九、解决方式</h2><h3 id="9-1-使用-SynchronousQueue"><a href="#9-1-使用-SynchronousQueue" class="headerlink" title="9.1. 使用 SynchronousQueue"></a>9.1. 使用 SynchronousQueue</h3><p>使用 SynchronousQueue，即阻塞队列大小设置为 0，原因在于：SynchronousQueue 和 LinkedBlockingQueue 维度不一致，SynchronousQueue 是根据是否有等待线程而决定是否入队成功，而 LinkedBlockingQueue 是根据缓冲区，而不管是否已经有等待线程。</p><ul><li>SynchronousQueue</li></ul><img src="/2019/12/19/theadpool-rejected-task/theadpool-rejected-task-04.png"><ul><li>LinkedBlockingQueue</li></ul><img src="/2019/12/19/theadpool-rejected-task/theadpool-rejected-task-05.png"><h3 id="9-2-根据业务情况配置阻塞队列"><a href="#9-2-根据业务情况配置阻塞队列" class="headerlink" title="9.2. 根据业务情况配置阻塞队列"></a>9.2. 根据业务情况配置阻塞队列</h3><p>对于我们的业务情况，因为任务最多只有 15 个，将阻塞队列大小设置为 15，这样就保证了不会出现任务被拒绝。</p><hr><p><strong>作者</strong></p><p>王日华，小米信息技术部订单组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com（武汉）</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;一次线上线程池任务问题处理历程&quot;&gt;&lt;a href=&quot;#一次线上线程池任务问题处理历程&quot; class=&quot;headerlink&quot; title=&quot;一次线上线程池任务问题处理历程&quot;&gt;&lt;/a&gt;一次线上线程池任务问题处理历程&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;[作者简介]&lt;/st
      
    
    </summary>
    
    
      <category term="Java" scheme="https://xiaomi-info.github.io/tags/Java/"/>
    
      <category term="Spring" scheme="https://xiaomi-info.github.io/tags/Spring/"/>
    
      <category term="线程池" scheme="https://xiaomi-info.github.io/tags/%E7%BA%BF%E7%A8%8B%E6%B1%A0/"/>
    
      <category term="阻塞队列" scheme="https://xiaomi-info.github.io/tags/%E9%98%BB%E5%A1%9E%E9%98%9F%E5%88%97/"/>
    
  </entry>
  
  <entry>
    <title>分布式锁的实现之 redis 篇</title>
    <link href="https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/"/>
    <id>https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/</id>
    <published>2019-12-17T11:19:20.000Z</published>
    <updated>2021-05-24T03:39:30.313Z</updated>
    
    <content type="html"><![CDATA[<h1 id="分布式锁的实现之-redis-篇"><a href="#分布式锁的实现之-redis-篇" class="headerlink" title="分布式锁的实现之 redis 篇"></a>分布式锁的实现之 redis 篇</h1><p><strong>[作者简介]</strong> 钟梦浩，信息部订单组研发工程师，目前主要负责小米订单中台业务。</p><h2 id="一、引言"><a href="#一、引言" class="headerlink" title="一、引言"></a>一、引言</h2><p>我们在系统中修改已有数据时，需要先读取，然后进行修改保存，此时很容易遇到并发问题。由于修改和保存不是原子操作，在并发场景下，部分对数据的操作可能会丢失。在单服务器系统我们常用本地锁来避免并发带来的问题，然而，当服务采用集群方式部署时，本地锁无法在多个服务器之间生效，这时候保证数据的一致性就需要分布式锁来实现。</p><img src="/2019/12/17/redis-distributed-lock/redis-lock-01.png"><h2 id="二、实现"><a href="#二、实现" class="headerlink" title="二、实现"></a>二、实现</h2><p>Redis 锁主要利用 Redis 的 setnx 命令。</p><ul><li>加锁命令：SETNX key value，当键不存在时，对键进行设置操作并返回成功，否则返回失败。KEY 是锁的唯一标识，一般按业务来决定命名。</li><li>解锁命令：DEL key，通过删除键值对释放锁，以便其他线程可以通过 SETNX 命令来获取锁。</li><li>锁超时：EXPIRE key timeout, 设置 key 的超时时间，以保证即使锁没有被显式释放，锁也可以在一定时间后自动释放，避免资源被永远锁住。</li></ul><p>则加锁解锁伪代码如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">if (setnx(key, 1) == 1)&#123;</span><br><span class="line">    expire(key, 30)</span><br><span class="line">    try &#123;</span><br><span class="line">        //TODO 业务逻辑</span><br><span class="line">    &#125; finally &#123;</span><br><span class="line">        del(key)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上述锁实现方式存在一些问题：</p><h3 id="1-SETNX-和-EXPIRE-非原子性"><a href="#1-SETNX-和-EXPIRE-非原子性" class="headerlink" title="1. SETNX 和 EXPIRE 非原子性"></a>1. SETNX 和 EXPIRE 非原子性</h3><p>如果 SETNX 成功，在设置锁超时时间后，服务器挂掉、重启或网络问题等，导致 EXPIRE 命令没有执行，锁没有设置超时时间变成死锁。</p><img src="/2019/12/17/redis-distributed-lock/redis-lock-02.png"><p>有很多开源代码来解决这个问题，比如使用 lua 脚本。示例：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (redis.call(<span class="string">'setnx'</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">1</span>]) &lt; <span class="number">1</span>)</span><br><span class="line"><span class="keyword">then</span> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"><span class="keyword">end</span>;</span><br><span class="line">redis.call(<span class="string">'expire'</span>, KEYS[<span class="number">1</span>], <span class="built_in">tonumber</span>(ARGV[<span class="number">2</span>]));</span><br><span class="line"><span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line">// 使用实例</span><br><span class="line">EVAL <span class="string">"if (redis.call('setnx',KEYS[1],ARGV[1]) &lt; 1) then return 0; end; redis.call('expire',KEYS[1],tonumber(ARGV[2])); return 1;"</span> <span class="number">1</span> key value <span class="number">100</span></span><br></pre></td></tr></table></figure><h3 id="2-锁误解除"><a href="#2-锁误解除" class="headerlink" title="2. 锁误解除"></a>2. 锁误解除</h3><p>如果线程 A 成功获取到了锁，并且设置了过期时间 30 秒，但线程 A 执行时间超过了 30 秒，锁过期自动释放，此时线程 B 获取到了锁；随后 A 执行完成，线程 A 使用 DEL 命令来释放锁，但此时线程 B 加的锁还没有执行完成，线程 A 实际释放的线程 B 加的锁。</p><img src="/2019/12/17/redis-distributed-lock/redis-lock-03.png"><p>通过在 value 中设置当前线程加锁的标识，在删除之前验证 key 对应的 value 判断锁是否是当前线程持有。可生成一个 UUID 标识当前线程，使用 lua 脚本做验证标识和解锁操作。</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">// 加锁</span><br><span class="line">String uuid = UUID.randomUUID().toString().replaceAll(<span class="string">"-"</span>,<span class="string">""</span>);</span><br><span class="line">SET key uuid NX EX <span class="number">30</span></span><br><span class="line">// 解锁</span><br><span class="line"><span class="keyword">if</span> (redis.call(<span class="string">'get'</span>, KEYS[<span class="number">1</span>]) == ARGV[<span class="number">1</span>])</span><br><span class="line">    <span class="keyword">then</span> <span class="keyword">return</span> redis.call(<span class="string">'del'</span>, KEYS[<span class="number">1</span>])</span><br><span class="line"><span class="keyword">else</span> <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><h3 id="3-超时解锁导致并发"><a href="#3-超时解锁导致并发" class="headerlink" title="3. 超时解锁导致并发"></a>3. 超时解锁导致并发</h3><p>如果线程 A 成功获取锁并设置过期时间 30 秒，但线程 A 执行时间超过了 30 秒，锁过期自动释放，此时线程 B 获取到了锁，线程 A 和线程 B 并发执行。</p><img src="/2019/12/17/redis-distributed-lock/redis-lock-04.png"><p>A、B 两个线程发生并发显然是不被允许的，一般有两种方式解决该问题：</p><ul><li>将过期时间设置足够长，确保代码逻辑在锁释放之前能够执行完成。</li><li>为获取锁的线程增加守护线程，为将要过期但未释放的锁增加有效时间。</li></ul><img src="/2019/12/17/redis-distributed-lock/redis-lock-05.png"><h3 id="4-不可重入"><a href="#4-不可重入" class="headerlink" title="4. 不可重入"></a>4. 不可重入</h3><p>当线程在持有锁的情况下再次请求加锁，如果一个锁支持一个线程多次加锁，那么这个锁就是可重入的。如果一个不可重入锁被再次加锁，由于该锁已经被持有，再次加锁会失败。Redis 可通过对锁进行重入计数，加锁时加 1，解锁时减 1，当计数归 0 时释放锁。</p><p>在本地记录记录重入次数，如 Java 中使用 ThreadLocal 进行重入次数统计，简单示例代码：</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> ThreadLocal&lt;Map&lt;String, Integer&gt;&gt; LOCKERS = ThreadLocal.withInitial(HashMap::<span class="keyword">new</span>);</span><br><span class="line"><span class="comment">// 加锁</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">lock</span><span class="params">(String key)</span> </span>&#123;</span><br><span class="line">  Map&lt;String, Integer&gt; lockers = LOCKERS.get();</span><br><span class="line">  <span class="keyword">if</span> (lockers.containsKey(key)) &#123;</span><br><span class="line">    lockers.put(key, lockers.get(key) + <span class="number">1</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">if</span> (SET key uuid NX EX <span class="number">30</span>) &#123;</span><br><span class="line">      lockers.put(key, <span class="number">1</span>);</span><br><span class="line">      <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 解锁</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">unlock</span><span class="params">(String key)</span> </span>&#123;</span><br><span class="line">  Map&lt;String, Integer&gt; lockers = LOCKERS.get();</span><br><span class="line">  <span class="keyword">if</span> (lockers.getOrDefault(key, <span class="number">0</span>) &lt;= <span class="number">1</span>) &#123;</span><br><span class="line">    lockers.remove(key);</span><br><span class="line">    DEL key</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    lockers.put(key, lockers.get(key) - <span class="number">1</span>);</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>本地记录重入次数虽然高效，但如果考虑到过期时间和本地、Redis 一致性的问题，就会增加代码的复杂性。另一种方式是 Redis Map 数据结构来实现分布式锁，既存锁的标识也对重入次数进行计数。Redission 加锁示例：</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">// 如果 lock_key 不存在</span><br><span class="line"><span class="keyword">if</span> (redis.call(<span class="string">'exists'</span>, KEYS[<span class="number">1</span>]) == <span class="number">0</span>)</span><br><span class="line"><span class="keyword">then</span></span><br><span class="line">    // 设置 lock_key 线程标识 <span class="number">1</span> 进行加锁</span><br><span class="line">    redis.call(<span class="string">'hset'</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">2</span>], <span class="number">1</span>);</span><br><span class="line">    // 设置过期时间</span><br><span class="line">    redis.call(<span class="string">'pexpire'</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">1</span>]);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line">    <span class="keyword">end</span>;</span><br><span class="line">// 如果 lock_key 存在且线程标识是当前欲加锁的线程标识</span><br><span class="line"><span class="keyword">if</span> (redis.call(<span class="string">'hexists'</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">2</span>]) == <span class="number">1</span>)</span><br><span class="line">    // 自增</span><br><span class="line">    <span class="keyword">then</span> redis.call(<span class="string">'hincrby'</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">2</span>], <span class="number">1</span>);</span><br><span class="line">    // 重置过期时间</span><br><span class="line">    redis.call(<span class="string">'pexpire'</span>, KEYS[<span class="number">1</span>], ARGV[<span class="number">1</span>]);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>;</span><br><span class="line">    <span class="keyword">end</span>;</span><br><span class="line">// 如果加锁失败，返回锁剩余时间</span><br><span class="line"><span class="keyword">return</span> redis.call(<span class="string">'pttl'</span>, KEYS[<span class="number">1</span>]);</span><br></pre></td></tr></table></figure><h3 id="5-无法等待锁释放"><a href="#5-无法等待锁释放" class="headerlink" title="5. 无法等待锁释放"></a>5. 无法等待锁释放</h3><p>上述命令执行都是立即返回的，如果客户端可以等待锁释放就无法使用。</p><ul><li>可以通过客户端轮询的方式解决该问题，当未获取到锁时，等待一段时间重新获取锁，直到成功获取锁或等待超时。这种方式比较消耗服务器资源，当并发量比较大时，会影响服务器的效率。</li><li>另一种方式是使用 Redis 的发布订阅功能，当获取锁失败时，订阅锁释放消息，获取锁成功后释放时，发送锁释放消息。如下：</li></ul><img src="/2019/12/17/redis-distributed-lock/redis-lock-06.png"><h2 id="三、集群"><a href="#三、集群" class="headerlink" title="三、集群"></a>三、集群</h2><h3 id="1-主备切换"><a href="#1-主备切换" class="headerlink" title="1. 主备切换"></a>1. 主备切换</h3><p>为了保证 Redis 的可用性，一般采用主从方式部署。主从数据同步有异步和同步两种方式，Redis 将指令记录在本地内存 buffer 中，然后异步将 buffer 中的指令同步到从节点，从节点一边执行同步的指令流来达到和主节点一致的状态，一边向主节点反馈同步情况。</p><p>在包含主从模式的集群部署方式中，当主节点挂掉时，从节点会取而代之，但客户端无明显感知。当客户端 A 成功加锁，指令还未同步，此时主节点挂掉，从节点提升为主节点，新的主节点没有锁的数据，当客户端 B 加锁时就会成功。</p><img src="/2019/12/17/redis-distributed-lock/redis-lock-07.png"><h3 id="2-集群脑裂"><a href="#2-集群脑裂" class="headerlink" title="2. 集群脑裂"></a>2. 集群脑裂</h3><p>集群脑裂指因为网络问题，导致 Redis master 节点跟 slave 节点和 sentinel 集群处于不同的网络分区，因为 sentinel 集群无法感知到 master 的存在，所以将 slave 节点提升为 master 节点，此时存在两个不同的 master 节点。Redis Cluster 集群部署方式同理。</p><p>当不同的客户端连接不同的 master 节点时，两个客户端可以同时拥有同一把锁。如下：</p><img src="/2019/12/17/redis-distributed-lock/redis-lock-08.png"><h2 id="四、结语"><a href="#四、结语" class="headerlink" title="四、结语"></a>四、结语</h2><p>Redis 以其高性能著称，但使用其实现分布式锁来解决并发仍存在一些困难。Redis 分布式锁只能作为一种缓解并发的手段，如果要完全解决并发问题，仍需要数据库的防并发手段。</p><p>参考：</p><p> 1.“Redis 分布式锁的正确实现方式（ Java 版 ）”  <a href="https://mp.weixin.qq.com/s/qJK61ew0kCExvXrqb7-RSg" target="_blank" rel="noopener">https://mp.weixin.qq.com/s/qJK61ew0kCExvXrqb7-RSg</a><br> 2.“漫画：什么是分布式锁？” <a href="https://mp.weixin.qq.com/s/8fdBKAyHZrfHmSajXT_dnA" target="_blank" rel="noopener">https://mp.weixin.qq.com/s/8fdBKAyHZrfHmSajXT_dnA</a><br> 3.“搞懂“分布式锁”，看这篇文章就对了” <a href="https://mp.weixin.qq.com/s/hoZB0wdwXfG3ECKlzjtPdw" target="_blank" rel="noopener">https://mp.weixin.qq.com/s/hoZB0wdwXfG3ECKlzjtPdw</a><br> 4.《Redis 深度历险：核心原理与应用实践》<br> 5.《逆流而上：阿里巴巴技术成长之路》</p><hr><p><strong>作者</strong></p><p>钟梦浩，小米信息部订单组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com（武汉）</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;分布式锁的实现之-redis-篇&quot;&gt;&lt;a href=&quot;#分布式锁的实现之-redis-篇&quot; class=&quot;headerlink&quot; title=&quot;分布式锁的实现之 redis 篇&quot;&gt;&lt;/a&gt;分布式锁的实现之 redis 篇&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;[作者简介]
      
    
    </summary>
    
    
      <category term="redis" scheme="https://xiaomi-info.github.io/tags/redis/"/>
    
      <category term="分布式锁" scheme="https://xiaomi-info.github.io/tags/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/"/>
    
  </entry>
  
  <entry>
    <title>走进 NSQ 源码细节</title>
    <link href="https://xiaomi-info.github.io/2019/12/06/nsq-src/"/>
    <id>https://xiaomi-info.github.io/2019/12/06/nsq-src/</id>
    <published>2019-12-06T02:37:47.000Z</published>
    <updated>2021-05-24T03:39:30.264Z</updated>
    
    <content type="html"><![CDATA[<h2 id="1-前言：为什么要使用-MQ-消息队列"><a href="#1-前言：为什么要使用-MQ-消息队列" class="headerlink" title="1. 前言：为什么要使用 MQ 消息队列"></a>1. 前言：为什么要使用 MQ 消息队列</h2><p>随着互联网技术在各行各业的应用高速普及与发展，各层应用之间调用关系越来越复杂，架构、开发、运维成本越来越高，高内聚、低耦合、可扩展、高可用已成为了行业需求。</p><p>一提到消息队列 <code>MQ(Message Queue)</code>，我们会想到很多应用场景，比如消息通知、用户积分增减、抽奖中奖等，可以看出来 <code>MQ</code> 的作用有：<br>流程异步化、代码解耦合、流量削峰、高可用、高吞吐量、广播分发，达到数据的最终一致性，满足具体的业务场景需求。</p><p>本文将从 <code>MQ</code> 比较、<code>NSQ</code> 介绍、源代码逻辑、亮点小结等方面进行解析，以期对 <code>NSQ</code> 有较为深入的理解。</p><h2 id="2-主流-MQ-比较"><a href="#2-主流-MQ-比较" class="headerlink" title="2. 主流 MQ 比较"></a>2. 主流 MQ 比较</h2><p>目前主流的 <code>MQ</code> 有 <code>Kafka</code>, <code>RabbitMQ</code>, <code>NSQ</code>, <code>RocketMQ</code>, <code>ActiveMQ</code>，它们的对比如下：</p><img src="/2019/12/06/nsq-src/mq_compare.png"><h2 id="3-NSQ-初识"><a href="#3-NSQ-初识" class="headerlink" title="3. NSQ 初识"></a>3. NSQ 初识</h2><p><code>NSQ</code> 最初是由 <code>bitly</code> 公司开源出来的一款简单易用的分布式消息中间件，它可用于大规模系统中的实时消息服务，并且每天能够处理数亿级别的消息。</p><img src="/2019/12/06/nsq-src/logo.png"><h3 id="3-1-NSQ-特性"><a href="#3-1-NSQ-特性" class="headerlink" title="3.1 NSQ 特性"></a>3.1 NSQ 特性</h3><p><strong>分布式：</strong> 它提供了分布式的、去中心化且没有单点故障的拓扑结构，稳定的消息传输发布保障，能够具有高容错和高可用特性。</p><p><strong>易于扩展：</strong> 它支持水平扩展，没有中心化的消息代理（ <code>Broker</code> ），内置的发现服务让集群中增加节点非常容易。</p><p><strong>运维方便：</strong> 它非常容易配置和部署，灵活性高。</p><p><strong>高度集成：</strong> 现在已经有官方的 <code>Golang</code>、<code>Python</code> 和 <code>JavaScript</code> 客户端，社区也有了其他各个语言的客户端库方便接入，自定义客户端也非常容易。</p><h3 id="3-2-NSQ-组件"><a href="#3-2-NSQ-组件" class="headerlink" title="3.2 NSQ 组件"></a>3.2 NSQ 组件</h3><img src="/2019/12/06/nsq-src/nsq.gif"><p><code>Topic</code>：一个 <code>topic</code> 就是程序发布消息的一个逻辑键，当程序第一次发布消息时就会创建 <code>topic</code>。</p><p><code>Channels</code>： <code>channel</code> 与消费者相关，是消费者之间的负载均衡， <code>channel</code> 在某种意义上来说是一个“队列”。每当一个发布者发送一条消息到一个 <code>topic</code>，消息会被复制到所有消费者连接的 channel 上，消费者通过这个特殊的 channel 读取消息，实际上，在消费者第一次订阅时就会创建 <code>channel</code>。 <code>Channel</code> 会将消息进行排列，如果没有消费者读取消息，消息首先会在内存中排队，当量太大时就会被保存到磁盘中。</p><p><code>Messages</code>：消息构成了我们数据流的中坚力量，消费者可以选择结束消息，表明它们正在被正常处理，或者重新将他们排队待到后面再进行处理。每个消息包含传递尝试的次数，当消息传递超过一定的阀值次数时，我们应该放弃这些消息，或者作为额外消息进行处理。</p><p><code>nsqd</code>： <code>nsqd</code> 是一个守护进程，负责接收（生产者 <code>producer</code> ）、排队（最小堆 <code>min heap</code> 实现）、投递（消费者 <code>consumer</code> ）消息给客户端。它可以独立运行，不过通常它是由 <code>nsqlookupd</code> 实例所在集群配置的（它在这能声明 <code>topics</code> 和 <code>channels</code>，以便大家能找到）。</p><p><code>nsqlookupd</code>： <code>nsqlookupd</code> 是守护进程负责管理拓扑信息。客户端通过查询 <code>nsqlookupd</code> 来发现指定话题（ <code>topic</code> ）的生产者，并且 <code>nsqd</code> 节点广播话题（<code>topic</code>）和通道（ <code>channel</code> ）信息。有两个接口： <code>TCP</code> 接口， <code>nsqd</code> 用它来广播。 <code>HTTP</code> 接口，客户端用它来发现和管理。</p><img src="/2019/12/06/nsq-src/topology.png"><p><code>nsqadmin</code>： <code>nsqadmin</code> 是一套 <code>WEB UI</code>，用来汇集集群的实时统计，并执行不同的管理任务。 常用工具类：</p><p><code>nsq_to _file</code>：消费指定的话题（<code>topic</code>）/通道（<code>channel</code>），并写到文件中，有选择的滚动和/或压缩文件。</p><p><code>nsq_to _http</code>：消费指定的话题（<code>topic</code>）/通道（<code>channel</code>）和执行 <code>HTTP requests (GET/POST)</code> 到指定的端点。</p><p><code>nsq_to _nsq</code>：消费者指定的话题/通道和重发布消息到目的地 <code>nsqd</code> 通过 <code>TCP</code>。</p><h2 id="4-nsqd-源码解析"><a href="#4-nsqd-源码解析" class="headerlink" title="4. nsqd 源码解析"></a>4. nsqd 源码解析</h2><h3 id="4-1-nsqd-执行入口"><a href="#4-1-nsqd-执行入口" class="headerlink" title="4.1 nsqd 执行入口"></a>4.1 nsqd 执行入口</h3><p>在 <code>nsq/apps/nsqd/main.go</code> 可以找到执行入口文件，如下：</p><img src="/2019/12/06/nsq-src/nsqd_main.png"><h3 id="4-2-nsqd-执行主逻辑源码"><a href="#4-2-nsqd-执行主逻辑源码" class="headerlink" title="4.2 nsqd 执行主逻辑源码"></a>4.2 nsqd 执行主逻辑源码</h3><p>a. 通过第三方 <code>svc</code> 包进行优雅的后台进程管理，<code>svc.Run() -&gt; svc.Init() -&gt; svc.Start()</code>，启动 <code>nsqd</code> 实例；</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  prg := &amp;program&#123;&#125;</span><br><span class="line">  <span class="keyword">if</span> err := svc.Run(prg, syscall.SIGINT, syscall.SIGTERM); err != <span class="literal">nil</span> &#123;</span><br><span class="line">    logFatal(<span class="string">"%s"</span>, err)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p *program)</span> <span class="title">Init</span><span class="params">(env svc.Environment)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">  <span class="keyword">if</span> env.IsWindowsService() &#123;</span><br><span class="line">    dir := filepath.Dir(os.Args[<span class="number">0</span>])</span><br><span class="line">    <span class="keyword">return</span> os.Chdir(dir)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p *program)</span> <span class="title">Start</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line">  opts := nsqd.NewOptions()</span><br><span class="line"></span><br><span class="line">  flagSet := nsqdFlagSet(opts)</span><br><span class="line">  flagSet.Parse(os.Args[<span class="number">1</span>:])</span><br><span class="line">  ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>b. 初始化配置项（ <code>opts, cfg</code> ），加载历史数据（ <code>nsqd.LoadMetadata</code> ）、持久化最新数据（ <code>nsqd.PersistMetadata</code> ），然后开启协程，进入 <code>nsqd.Main()</code> 主函数；</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">options.Resolve(opts, flagSet, cfg)</span><br><span class="line">  nsqd, err := nsqd.New(opts)</span><br><span class="line">  <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    logFatal(<span class="string">"failed to instantiate nsqd - %s"</span>, err)</span><br><span class="line">  &#125;</span><br><span class="line">  p.nsqd = nsqd</span><br><span class="line"></span><br><span class="line">  err = p.nsqd.LoadMetadata()</span><br><span class="line">  <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    logFatal(<span class="string">"failed to load metadata - %s"</span>, err)</span><br><span class="line">  &#125;</span><br><span class="line">  err = p.nsqd.PersistMetadata()</span><br><span class="line">  <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    logFatal(<span class="string">"failed to persist metadata - %s"</span>, err)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">    err := p.nsqd.Main()</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">      p.Stop()</span><br><span class="line">      os.Exit(<span class="number">1</span>)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;()</span><br></pre></td></tr></table></figure><p>c. 初始化 <code>tcpServer, httpServer, httpsServer</code>，然后循环监控队列信息（ <code>n.queueScanLoop</code> ）、节点信息管理（ <code>n.lookupLoop</code> ）、统计信息（ <code>n.statsdLoop</code> ）输出；</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">tcpServer := &amp;tcpServer&#123;ctx: ctx&#125;</span><br><span class="line">  n.waitGroup.Wrap(<span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">    exitFunc(protocol.TCPServer(n.tcpListener, tcpServer, n.logf))</span><br><span class="line">  &#125;)</span><br><span class="line">  httpServer := newHTTPServer(ctx, <span class="literal">false</span>, n.getOpts().TLSRequired == TLSRequired)</span><br><span class="line">  n.waitGroup.Wrap(<span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">    exitFunc(http_api.Serve(n.httpListener, httpServer, <span class="string">"HTTP"</span>, n.logf))</span><br><span class="line">  &#125;)</span><br><span class="line">  <span class="keyword">if</span> n.tlsConfig != <span class="literal">nil</span> &amp;&amp; n.getOpts().HTTPSAddress != <span class="string">""</span> &#123;</span><br><span class="line">    httpsServer := newHTTPServer(ctx, <span class="literal">true</span>, <span class="literal">true</span>)</span><br><span class="line">    n.waitGroup.Wrap(<span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">      exitFunc(http_api.Serve(n.httpsListener, httpsServer, <span class="string">"HTTPS"</span>, n.logf))</span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  n.waitGroup.Wrap(n.queueScanLoop)</span><br><span class="line">  n.waitGroup.Wrap(n.lookupLoop)</span><br><span class="line">  <span class="keyword">if</span> n.getOpts().StatsdAddress != <span class="string">""</span> &#123;</span><br><span class="line">    n.waitGroup.Wrap(n.statsdLoop)</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>d. 分别处理 <code>tcp/http</code> 请求，开启 <code>handler</code> 协程进行并发处理，其中 <code>newHTTPServer</code> 注册路由采用了 <code>Decorate</code> 装饰器模式（后面会进一步解析）；</p><p><strong><code>http-Decorate</code> 路由分发</strong></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">router := httprouter.New()</span><br><span class="line">  router.HandleMethodNotAllowed = <span class="literal">true</span></span><br><span class="line">  router.PanicHandler = http_api.LogPanicHandler(ctx.nsqd.logf)</span><br><span class="line">  router.NotFound = http_api.LogNotFoundHandler(ctx.nsqd.logf)</span><br><span class="line">  router.MethodNotAllowed = http_api.LogMethodNotAllowedHandler(ctx.nsqd.logf)</span><br><span class="line">  s := &amp;httpServer&#123;</span><br><span class="line">    ctx:         ctx,</span><br><span class="line">    tlsEnabled:  tlsEnabled,</span><br><span class="line">    tlsRequired: tlsRequired,</span><br><span class="line">    router:      router,</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  router.Handle(<span class="string">"GET"</span>, <span class="string">"/ping"</span>, http_api.Decorate(s.pingHandler, log, http_api.PlainText))</span><br><span class="line">  router.Handle(<span class="string">"GET"</span>, <span class="string">"/info"</span>, http_api.Decorate(s.doInfo, log, http_api.V1))</span><br><span class="line"></span><br><span class="line">  <span class="comment">// v1 negotiate</span></span><br><span class="line">  router.Handle(<span class="string">"POST"</span>, <span class="string">"/pub"</span>, http_api.Decorate(s.doPUB, http_api.V1))</span><br><span class="line">  router.Handle(<span class="string">"POST"</span>, <span class="string">"/mpub"</span>, http_api.Decorate(s.doMPUB, http_api.V1))</span><br><span class="line">  router.Handle(<span class="string">"GET"</span>, <span class="string">"/stats"</span>, http_api.Decorate(s.doStats, log, http_api.V1))</span><br><span class="line"></span><br><span class="line">  <span class="comment">// only v1</span></span><br><span class="line">  router.Handle(<span class="string">"POST"</span>, <span class="string">"/topic/create"</span>, http_api.Decorate(s.doCreateTopic, log, http_api.V1))</span><br><span class="line">  router.Handle(<span class="string">"POST"</span>, <span class="string">"/topic/delete"</span>, http_api.Decorate(s.doDeleteTopic, log, http_api.V1))</span><br></pre></td></tr></table></figure><p><strong><code>tcp-handler</code> 处理</strong></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">    clientConn, err := listener.Accept()</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">      <span class="keyword">if</span> nerr, ok := err.(net.Error); ok &amp;&amp; nerr.Temporary() &#123;</span><br><span class="line">        logf(lg.WARN, <span class="string">"temporary Accept() failure - %s"</span>, err)</span><br><span class="line">        runtime.Gosched()</span><br><span class="line">        <span class="keyword">continue</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="comment">// theres no direct way to detect this error because it is not exposed</span></span><br><span class="line">      <span class="keyword">if</span> !strings.Contains(err.Error(), <span class="string">"use of closed network connection"</span>) &#123;</span><br><span class="line">        <span class="keyword">return</span> fmt.Errorf(<span class="string">"listener.Accept() error - %s"</span>, err)</span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">break</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">go</span> handler.Handle(clientConn)</span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>e. <code>TCP</code> 解析 <code>V2</code> 协议，走内部协议封装的 <code>prot.IOLoop(conn)</code> 进行处理；</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> prot protocol.Protocol</span><br><span class="line">  <span class="keyword">switch</span> protocolMagic &#123;</span><br><span class="line">  <span class="keyword">case</span> <span class="string">"  V2"</span>:</span><br><span class="line">    prot = &amp;protocolV2&#123;ctx: p.ctx&#125;</span><br><span class="line">  <span class="keyword">default</span>:</span><br><span class="line">    protocol.SendFramedResponse(clientConn, frameTypeError, []<span class="keyword">byte</span>(<span class="string">"E_BAD_PROTOCOL"</span>))</span><br><span class="line">    clientConn.Close()</span><br><span class="line">    p.ctx.nsqd.logf(LOG_ERROR, <span class="string">"client(%s) bad protocol magic '%s'"</span>,</span><br><span class="line">      clientConn.RemoteAddr(), protocolMagic)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  err = prot.IOLoop(clientConn)</span><br><span class="line">  <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    p.ctx.nsqd.logf(LOG_ERROR, <span class="string">"client(%s) - %s"</span>, clientConn.RemoteAddr(), err)</span><br><span class="line">    <span class="keyword">return</span></span><br><span class="line">  &#125;</span><br></pre></td></tr></table></figure><p>f. 通过内部协议进行 <code>p.Exec</code> （执行命令）、 <code>p.Send</code> （发送结果），保证每个 <code>nsqd</code> 节点都能正确的进行消息生成与消费，一旦上述过程有 <code>error</code> 都会被捕获处理，确保分布式投递的可靠性；</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">params := bytes.Split(line, separatorBytes)</span><br><span class="line"></span><br><span class="line">    p.ctx.nsqd.logf(LOG_DEBUG, <span class="string">"PROTOCOL(V2): [%s] %s"</span>, client, params)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> response []<span class="keyword">byte</span></span><br><span class="line">    response, err = p.Exec(client, params)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">      ctx := <span class="string">""</span></span><br><span class="line">      <span class="keyword">if</span> parentErr := err.(protocol.ChildErr).Parent(); parentErr != <span class="literal">nil</span> &#123;</span><br><span class="line">        ctx = <span class="string">" - "</span> + parentErr.Error()</span><br><span class="line">      &#125;</span><br><span class="line">      p.ctx.nsqd.logf(LOG_ERROR, <span class="string">"[%s] - %s%s"</span>, client, err, ctx)</span><br><span class="line"></span><br><span class="line">      sendErr := p.Send(client, frameTypeError, []<span class="keyword">byte</span>(err.Error()))</span><br><span class="line">      <span class="keyword">if</span> sendErr != <span class="literal">nil</span> &#123;</span><br><span class="line">        p.ctx.nsqd.logf(LOG_ERROR, <span class="string">"[%s] - %s%s"</span>, client, sendErr, ctx)</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">      &#125;</span><br><span class="line"></span><br><span class="line">      <span class="comment">// errors of type FatalClientErr should forceably close the connection</span></span><br><span class="line">      <span class="keyword">if</span> _, ok := err.(*protocol.FatalClientErr); ok &#123;</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">      &#125;</span><br><span class="line">      <span class="keyword">continue</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> response != <span class="literal">nil</span> &#123;</span><br><span class="line">      err = p.Send(client, frameTypeResponse, response)</span><br><span class="line">      <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        err = fmt.Errorf(<span class="string">"failed to send response - %s"</span>, err)</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">      &#125;</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure><h2 id="4-3-nsqd-流程图小结"><a href="#4-3-nsqd-流程图小结" class="headerlink" title="4.3 nsqd 流程图小结"></a>4.3 nsqd 流程图小结</h2><p>上述流程小结如下：</p><img src="/2019/12/06/nsq-src/nsqd.png"><h2 id="5-nsqlookupd-源码解析"><a href="#5-nsqlookupd-源码解析" class="headerlink" title="5. nsqlookupd 源码解析"></a>5. nsqlookupd 源码解析</h2><p><code>nsqlookupd</code> 代码执行逻辑与 <code>nsqd</code> 大体相似，小结流程图如下：</p><img src="/2019/12/06/nsq-src/nsqlookupd.png"><h2 id="6-源码亮点"><a href="#6-源码亮点" class="headerlink" title="6. 源码亮点"></a>6. 源码亮点</h2><h3 id="6-1-使用装饰器"><a href="#6-1-使用装饰器" class="headerlink" title="6.1 使用装饰器"></a>6.1 使用装饰器</h3><p>从路由 <code>router.Handle(&quot;GET&quot;, &quot;/ping&quot;, http_api.Decorate(s.pingHandler, log, http_api.PlainText))</code>，可以看出 <code>httpServer</code> 通过 <code>http_api.Decorate</code> 装饰器实现对各 <code>http</code> 路由进行 <code>handler</code> 装饰，如加 <code>log</code> 日志、<code>V1</code> 协议版本号的统一格式输出等；</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Decorate</span><span class="params">(f APIHandler, ds ...Decorator)</span> <span class="title">httprouter</span>.<span class="title">Handle</span></span> &#123;</span><br><span class="line">  decorated := f</span><br><span class="line">  <span class="keyword">for</span> _, decorate := <span class="keyword">range</span> ds &#123;</span><br><span class="line">    decorated = decorate(decorated)</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(w http.ResponseWriter, req *http.Request, ps httprouter.Params)</span></span> &#123;</span><br><span class="line">    decorated(w, req, ps)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-2-锁与原子操作-RWMutex-atomic-Value"><a href="#6-2-锁与原子操作-RWMutex-atomic-Value" class="headerlink" title="6.2 锁与原子操作 RWMutex/atomic.Value"></a>6.2 锁与原子操作 RWMutex/atomic.Value</h3><p>从下面的代码中可以看到，当需要获取一个 <code>topic</code> 的时候，先用读锁去读(此时如果有写锁将被阻塞)，若存在则直接返回，若不存在则使用写锁新建一个；另外，使用 <code>atomic.Value</code> 进行结构体某些字段的并发存取值，保证原子性。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(n *NSQD)</span> <span class="title">GetTopic</span><span class="params">(topicName <span class="keyword">string</span>)</span> *<span class="title">Topic</span></span> &#123;</span><br><span class="line">  <span class="comment">// most likely, we already have this topic, so try read lock first.</span></span><br><span class="line">  n.RLock()</span><br><span class="line">  t, ok := n.topicMap[topicName]</span><br><span class="line">  n.RUnlock()</span><br><span class="line">  <span class="keyword">if</span> ok &#123;</span><br><span class="line">    <span class="keyword">return</span> t</span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  n.Lock()</span><br><span class="line"></span><br><span class="line">  t, ok = n.topicMap[topicName]</span><br><span class="line">  <span class="keyword">if</span> ok &#123;</span><br><span class="line">    n.Unlock()</span><br><span class="line">    <span class="keyword">return</span> t</span><br><span class="line">  &#125;</span><br><span class="line">  deleteCallback := <span class="function"><span class="keyword">func</span><span class="params">(t *Topic)</span></span> &#123;</span><br><span class="line">    n.DeleteExistingTopic(t.name)</span><br><span class="line">  &#125;</span><br><span class="line">  t = NewTopic(topicName, &amp;context&#123;n&#125;, deleteCallback)</span><br><span class="line">  n.topicMap[topicName] = t</span><br><span class="line"></span><br><span class="line">  n.Unlock()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="6-3-消息多路分发-amp-负载均衡"><a href="#6-3-消息多路分发-amp-负载均衡" class="headerlink" title="6.3 消息多路分发 &amp; 负载均衡"></a>6.3 消息多路分发 &amp; 负载均衡</h3><p><code>Topic</code> 和 <code>Channel</code> 都没有预先配置。<code>Topic</code> 由第一次发布消息到命名的 <code>Topic</code> 或第一次通过订阅一个命名 <code>Topic</code> 来创建。<code>Channel</code> 被第一次订阅到指定的 <code>Channel</code> 创建。<code>Topic</code> 和 <code>Channel</code> 的所有缓冲的数据相互独立，防止缓慢消费者造成对其他 <code>Channel</code> 的积压（同样适用于 <code>Topic</code> 级别）。</p><p><strong>多路分发</strong> - <code>producer</code> 会同时连上 <code>nsq</code> 集群中所有 <code>nsqd</code> 节点，当然这些节点的地址是在初始化时，通过外界传递进去；当发布消息时，<code>producer</code> 会随机选择一个 <code>nsqd</code> 节点发布某个 <code>Topic</code> 的消息；<code>consumer</code> 在订阅 <code>subscribe</code> 某个<code>Topic/Channel</code>时，会首先连上 <code>nsqlookupd</code> 获取最新可用的 <code>nsqd</code> 节点，然后通过 <code>TCP</code> 长连接方式连上所有发布了指定 <code>Topic</code> 的 <code>producer</code> 节点，并在本地用 <code>tornado</code> 轮询每个连接，当某个连接有可读事件时，即有消息达到，处理即可。</p><p><strong>负载均衡</strong> - 当向某个 <code>Topic</code> 发布一个消息时，该消息会被复制到所有的 <code>Channel</code>，如果 <code>Channel</code> 只有一个客户端，那么 <code>Channel</code> 就将消息投递给这个客户端；如果 <code>Channel</code> 的客户端不止一个，那么 <code>Channel</code> 将把消息随机投递给任何一个客户端，这也可以看做是客户端的负载均衡；</p><h3 id="6-4-最小堆-优先级队列"><a href="#6-4-最小堆-优先级队列" class="headerlink" title="6.4 最小堆 - 优先级队列"></a>6.4 最小堆 - 优先级队列</h3><p><strong>优先级队列（ <code>Priority Queue</code> ）</strong> - 通过数据结构最小堆（ <code>min heap</code> ）实现，<code>pub</code> 一条消息时立即就排好序（优先级通过 <code>Priority-timeout</code> 时间戳排序），最近到期的放到最小堆根节点；取出一条消息直接从最小堆的根节点取出，时间复杂度很低。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Item <span class="keyword">struct</span> &#123;</span><br><span class="line">  Value    <span class="keyword">interface</span>&#123;&#125;</span><br><span class="line">  Priority <span class="keyword">int64</span></span><br><span class="line">  Index    <span class="keyword">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// this is a priority queue as implemented by a min heap</span></span><br><span class="line"><span class="comment">// ie. the 0th element is the *lowest* value</span></span><br><span class="line"><span class="keyword">type</span> PriorityQueue []*Item</span><br></pre></td></tr></table></figure><h3 id="6-5-队列设计-延时-运行队列"><a href="#6-5-队列设计-延时-运行队列" class="headerlink" title="6.5 队列设计 - 延时/运行队列"></a>6.5 队列设计 - 延时/运行队列</h3><p><strong>延时队列（ <code>deferredPQ</code> ）</strong> - 通过 <code>DPUB</code> 发布消息时带有 <code>timeout</code> 属性值实现，表示从当前时间戳多久后可以取出来消费；</p><p><strong>运行队列（ <code>inFlightPQ</code> ）</strong> - 正在被消费者 <code>consumer</code> 消费的消息放入运行队列中，若处理失败或超时则自动重新放入（ <code>Requeue</code> ）队列，待下一次取出再次消费；消费成功（ <code>Finish</code> ）则删除对应的消息。</p><h3 id="6-6-分布式-去中心化-无-SPOF"><a href="#6-6-分布式-去中心化-无-SPOF" class="headerlink" title="6.6 分布式 - 去中心化/无 SPOF"></a>6.6 分布式 - 去中心化/无 SPOF</h3><p><code>nsq</code> 被设计以分布的方式被使用，客户端连接到指定 <code>topic</code> 的所有生产者 <code>producer</code> 实例。没有中间人，没有消息代理 <code>broker</code> ，也没有单点故障（ <code>SPOF - single point of failure</code> ）。<br>这种拓扑结构消除单链，聚合，消费者直接连接所有生产者。从技术上讲，哪个客户端连接到哪个 <code>nsq</code> 不重要，只要有足够的消费者 <code>consumer</code> 连接到所有生产者 <code>producer</code>，以满足大量的消息，保证所有东西最终将被处理。</p><p>对于 <code>nsqlookupd</code>，高可用性是通过运行多个实例来实现。他们不直接相互通信和数据被认为是最终一致。如果某个 <code>nsqd</code> 出现问题，<code>down</code> 机了，会和 <code>nsqlookupd</code> 断开，这样客户端从 <code>nsqlookupd</code> 得到的 <code>nsqd</code> 的列表永远是可用的。客户端连接的是所有的 <code>nsqd</code>，一个出问题了就用其他的连接，所以也不会受影响。</p><h3 id="6-7-高可用、大吞吐量"><a href="#6-7-高可用、大吞吐量" class="headerlink" title="6.7 高可用、大吞吐量"></a>6.7 高可用、大吞吐量</h3><p><strong>高可用性（ <code>HA</code> ）</strong> - 通过集群化部署多个 <code>nsqd, nsqlookupd</code> 节点，可实现同时多生产者、多消费者运行，单一节点出现故障不影响系统运行；每个节点启动时都会先从磁盘读取未处理的消息，极端情况下，会丢失少量还未来得及存盘的内存中消息。</p><p><strong>10 亿/天</strong> - 通过 <code>goroutine, channel</code> 充分利用 <code>golang</code> 语言的协程并发特性，可高并发处理大量消息的生产与消费。例如 <code>message</code> 为 <code>10 byte</code> 大小，则 50( <code>nsq</code> 节点数) * 10(字节) <em> 86400(一天秒数) </em> 25(每秒处理消息数) = 10 亿，可见达到十亿级别的吞吐量，通过快速部署节点即可实现。</p><h3 id="6-8-协议规范"><a href="#6-8-协议规范" class="headerlink" title="6.8 协议规范"></a>6.8 协议规范</h3><p>自定义 <code>protocol</code>、魔法字符串 <code>magicStr</code> 进行通信、版本控制：</p><p><strong>通信协议</strong></p><p>nsqd</p><blockquote><p>FIN - 消息消费完成<br>RDY - 客户端连接就绪<br>REQ - 消息重放入队<br>PUB - 发布一条消息<br>MPUB - 发布多条消息<br>DPUB - 发布一条延时消息<br>NOP - 空操作<br>TOUCH - 重置消息过期时间<br>SUB - 消费者订阅 <code>Topic/Channel</code><br>CLS - 超时关闭连接<code>CLOSE_WAIT</code><br>AUTH - 权限认证</p></blockquote><p>nsqlookupd</p><blockquote><p>PING - 心跳检测<br>IDENTIFY - 权限与协议校验<br>REGISTER - <code>nsqd</code>节点注册<br>UNREGISTER - <code>nsqd</code>节点注销</p></blockquote><p><strong>版本控制</strong></p><blockquote><p>nsqd - “ V2” (4 byte)<br>nsqlookupd - “ V1” (4 byte)</p></blockquote><h3 id="6-9-快速扩缩容"><a href="#6-9-快速扩缩容" class="headerlink" title="6.9 快速扩缩容"></a>6.9 快速扩缩容</h3><p><code>nsq</code> 集群很容易配置（多种参数设定方式：命令行 &gt; 配置文件 &gt; 默认值）和部署（编译的二进制可执行文件没有运行时依赖），通过简单设置初始化参数，运维 <code>Ops</code> 就可以快速增加 <code>nsqd</code> 或 <code>nsqlookupd</code> 节点，为 <code>Topic</code> 引入一个新的消费者，只需启动一个配置了 <code>nsqlookup</code> 实例地址的 <code>nsq</code> 客户端。无需为添加任何新的消费者或生产者更改配置，大大降低了开销和复杂性。</p><p>通过容器化管理多个实例将非常快速进行生产者、消费者的扩缩容，加上容器的流量监控、熔断、最低节点数等功能，保证了集群中 <code>nsqd</code> 的高效运行。</p><h2 id="7-小结"><a href="#7-小结" class="headerlink" title="7. 小结"></a>7. 小结</h2><p>从源码可以看到，<code>nsqd</code> 的作用就是实际干活的组件，生产者 <code>producer</code>、消费者 <code>consumer</code> 利用 <code>nsqlookupd</code> 获取最新可用的节点，当连接上对应的 <code>Topic/Channel</code> 后，将消息 <code>message</code> 发送到客户端进行消费，处理成功则 <code>FIN(finish)</code>，或失败/超时后重新放回队列 <code>REQ(requeue)</code>，待下一次再消费处理。<code>nsqlookupd</code> 的作用就是管理 <code>nsqd</code> 节点的认证、注册、注销、心跳检测，动态维护分布式集群中最新可用的 <code>nsqd</code> 节点列表供客户端取用。</p><p>在可靠性、有序性方便， <code>nsq</code> 保证消息至少被投递消费一次（幂等消费），当某个 <code>nsqd</code> 节点出现故障时，极端情况下内存里面的消息还未来得及存入磁盘，这部分消息将丢失；通过分布式多个 <code>consumer</code> 消费，会因为消息处理时长、网络延迟等导致消息重排，再次消费顺序与写入顺序不一致，因此在高可靠性、顺序性方面略存在不足，应根据具体的业务场景进行取舍。</p><p><strong>综上：</strong> 源代码实现逻辑清晰明了，源码中使用了很多读写锁 <code>RWMutex</code>、原子值 <code>atomic.Value</code>、<code>interface</code> 接口复用、自定义通信协议 <code>protocol</code>、<code>http-decorator</code>装饰器、<code>goroutine/channel</code> 协程间并发通信，优先从内存（ <code>msqChan</code> ）存取消息，从而保证了高可用、高吞吐量的应用能力。快速高效的节点配置与扩展，配合容器云编排技术，可以高效实现集群的 scale 化。</p><h3 id="参考资料"><a href="#参考资料" class="headerlink" title="参考资料"></a>参考资料</h3><ul><li><a href="https://nsq.io/overview/design.html" target="_blank" rel="noopener">nsq 官方文档</a></li><li><a href="https://www.cnblogs.com/xifengxiaoma/p/9391647.html" target="_blank" rel="noopener">常用消息队列介绍和对比</a></li><li><a href="https://juejin.im/post/5d68cce2f265da039d32e39e" target="_blank" rel="noopener">nsq 去中心化原理</a></li><li><a href="http://luodw.cc/2016/12/08/nsq01/#more" target="_blank" rel="noopener">NSQ 源码分析之概述</a></li></ul><hr><p><strong>作者</strong></p><p>王成，小米信息技术部海外商城组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com(武汉)</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;1-前言：为什么要使用-MQ-消息队列&quot;&gt;&lt;a href=&quot;#1-前言：为什么要使用-MQ-消息队列&quot; class=&quot;headerlink&quot; title=&quot;1. 前言：为什么要使用 MQ 消息队列&quot;&gt;&lt;/a&gt;1. 前言：为什么要使用 MQ 消息队列&lt;/h2&gt;&lt;p&gt;
      
    
    </summary>
    
      <category term="Golang" scheme="https://xiaomi-info.github.io/categories/Golang/"/>
    
    
      <category term="Golang" scheme="https://xiaomi-info.github.io/tags/Golang/"/>
    
      <category term="NSQ" scheme="https://xiaomi-info.github.io/tags/NSQ/"/>
    
  </entry>
  
  <entry>
    <title>如何高效对接第三方支付</title>
    <link href="https://xiaomi-info.github.io/2019/12/04/third-party-pay/"/>
    <id>https://xiaomi-info.github.io/2019/12/04/third-party-pay/</id>
    <published>2019-12-04T08:56:43.000Z</published>
    <updated>2021-05-24T03:39:30.331Z</updated>
    
    <content type="html"><![CDATA[<h1 id="如何高效对接第三方支付"><a href="#如何高效对接第三方支付" class="headerlink" title="如何高效对接第三方支付"></a>如何高效对接第三方支付</h1><p><strong>[作者简介]</strong> 逄志强，海外小米网研发工程师，目前主要负责小米网服务构建和研发工作。</p><p>海外小米网是小米集团自建的电商网站，是小米集团重要的销售渠道之一。电商购物流程中核心的一环是用户支付。目前我们已经服务 30 个国家和地区，不同国家往往需要对接不同的第三方支付公司，所以最近两年，海外研发组对接了大量的第三方支付公司，积累了一定的经验。</p><p>本文主要分享如何对接第三方支付，以及在生产上实际遇到的一些问题，避免大家重复踩坑。</p><h2 id="一、五个接口"><a href="#一、五个接口" class="headerlink" title="一、五个接口"></a>一、五个接口</h2><p>先简单阐述一下，对接第三方支付时，需要对接如下 5 个核心接口。</p><h3 id="1-发起支付"><a href="#1-发起支付" class="headerlink" title="1.发起支付"></a>1.发起支付</h3><p>该接口主要用于从第三方获取 token，当用户跳转到第三方网站进行支付时，第三方支付公司用来校验是否是合法的支付请求。</p><h3 id="2-支付结果查询"><a href="#2-支付结果查询" class="headerlink" title="2.支付结果查询"></a>2.支付结果查询</h3><p>商户用该接口来判断支付结果成功与否。</p><h3 id="3-退款"><a href="#3-退款" class="headerlink" title="3.退款"></a>3.退款</h3><p>商户用该接口进行退款。</p><h3 id="4-退款查询"><a href="#4-退款查询" class="headerlink" title="4.退款查询"></a>4.退款查询</h3><p>商户用该接口来判断退款结果成功与否。</p><h3 id="5-获取支付成功订单列表接口"><a href="#5-获取支付成功订单列表接口" class="headerlink" title="5.获取支付成功订单列表接口"></a>5.获取支付成功订单列表接口</h3><p>商户用该接口获取第三方某日所有成功支付订单列表，用于对账时使用。</p><h2 id="二、四个流程"><a href="#二、四个流程" class="headerlink" title="二、四个流程"></a>二、四个流程</h2><p>只需要对接完如下四个流程，便可完成第三方支付的对接。</p><ul><li>发起支付请求</li><li>同步回调和异步回调</li><li>退款和退款查询</li><li>对账</li></ul><h3 id="1-发起支付请求"><a href="#1-发起支付请求" class="headerlink" title="1.发起支付请求"></a>1.发起支付请求</h3><p>下面的时序图中有几个名词，此处先给大家介绍一下</p><ul><li>电商系统：海外商城的服务系统，负责提供整个购物流程</li><li>支付网关：我们将支付抽离为一个单独的系统，该系统用来对接所有支付</li></ul><h4 id="时序图"><a href="#时序图" class="headerlink" title="时序图"></a>时序图</h4><img src="/2019/12/04/third-party-pay/image-20191103164155709.png"><h4 id="伪代码"><a href="#伪代码" class="headerlink" title="伪代码"></a>伪代码</h4><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#商城系统</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">pay</span><span class="params">()</span></span>&#123;</span><br><span class="line">    <span class="comment">#1.请求支付网关，获取发起支付必要信息（token，跳转url）</span></span><br><span class="line">    $res = <span class="keyword">$this</span>-&gt;payRequest($orderInfo,$sign);</span><br><span class="line">    <span class="comment">#2.根据返回结果，跳转到第三方，进行支付</span></span><br><span class="line">    redirect($res[<span class="string">'url'</span>]);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">#支付网关</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">payment</span><span class="params">()</span></span>&#123;</span><br><span class="line">    <span class="comment">#1.检查请求数据的签名</span></span><br><span class="line">    <span class="keyword">$this</span>-&gt;checkSign();</span><br><span class="line">    <span class="comment">#2.初始化订单数据，将传入参数格式化为需要的结构</span></span><br><span class="line">    <span class="keyword">$this</span>-&gt;initData();</span><br><span class="line">    <span class="comment">#3.参数检查</span></span><br><span class="line">    <span class="keyword">$this</span>-&gt;checkParam();</span><br><span class="line">    <span class="comment">#4.记录请求日志</span></span><br><span class="line">    <span class="keyword">$this</span>-&gt;savePayLog();</span><br><span class="line">    <span class="comment">#5.发起支付</span></span><br><span class="line">    <span class="comment">#5.1不是首次支付，则检查是否支付成功过</span></span><br><span class="line">    <span class="keyword">if</span>(!$isFirstRequest)&#123;</span><br><span class="line">        <span class="keyword">$this</span>-&gt;checkPaymentStatus();</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">#5.2生成新的支付号</span></span><br><span class="line">    $transactionCode = <span class="keyword">$this</span>-&gt;getNewTransactionCode();</span><br><span class="line">    <span class="comment">#5.3请求第三方，获取token，跳转链接等信息</span></span><br><span class="line">    $res = <span class="keyword">$this</span>-&gt;pay($transactionCode);</span><br><span class="line">    <span class="keyword">return</span> $res;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="说明点"><a href="#说明点" class="headerlink" title="说明点"></a>说明点</h4><ul><li>签名校验：请求支付接口域名为内网域名，使用签名校验，让系统更加安全</li><li>记录请求日志：请求除了记录到 log 文件中，建议落库，方便日后查找</li><li>非首次支付判断支付状态：防止重复支付，该判断可在流量高峰期降配掉</li><li><strong>生成新的支付号</strong>：部分第三方支付公司，规定同一个支付号，无论支付成功与否只能使用一次。目前，在小米网，当订单创建成功后，订单号不会改变，所以每次用户发起支付后，支付网关会生成新的支付号，使用该支付号请求第三方。该方案会引入<strong>重复支付问题</strong>，在后面章节阐述解决方案。</li></ul><h4 id="实例"><a href="#实例" class="headerlink" title="实例"></a>实例</h4><img src="/2019/12/04/third-party-pay/image-20191103220819084.png"><img src="/2019/12/04/third-party-pay/image-20191103221755220.png"><img src="/2019/12/04/third-party-pay/image-20191103221520717.png"><ul><li>surl：支付后，第三方会调用该 url，告知支付网关支付结果，这个流程叫同步回调</li><li>pay_url：需要跳转到第三方的 url</li><li>hash：第三方用 hash 来检验该请求是否合法</li></ul><p>​</p><h3 id="2-同步回调和异步回调"><a href="#2-同步回调和异步回调" class="headerlink" title="2.同步回调和异步回调"></a>2.同步回调和异步回调</h3><p>支付后，第三方支付会通知支付网关，支付结果。通知的实现一般会有两种方案</p><ul><li><strong>同步回调</strong>：支付后，立即回调支付网关提供的回调接口。该接口 url 一般在发起支付时，作为参数传递给第三方</li><li><strong>异步回调</strong>：支付后，第三方支付会调用对接方提供的 API，该 API 一般是对接的时候提供给第三方，第三方配置在自己系统中的。异步回调有重试机制，如果对接方没有返回指定结果，如 httpcode 不为 200，则会在一段时间后重试，直到达到指定重试上线后，会停止重试。</li></ul><p><strong>同步回调和异步回调是支付系统稳定性和准确性的重要保证。</strong>这两个流程核心逻辑一致，所以此处放在一起讲述。</p><h4 id="时序图-1"><a href="#时序图-1" class="headerlink" title="时序图"></a>时序图</h4><img src="/2019/12/04/third-party-pay/image-20191103225425658.png"><h4 id="伪代码-1"><a href="#伪代码-1" class="headerlink" title="伪代码"></a>伪代码</h4><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#2.同步回调 异步回调</span></span><br><span class="line"><span class="comment">#支付网关</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">callback</span><span class="params">()</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="comment">#1.记录请求日志</span></span><br><span class="line">    <span class="keyword">$this</span>-&gt;savePayLog();</span><br><span class="line">    <span class="comment">#2.检查第三方传入数据是否合法</span></span><br><span class="line">    <span class="keyword">$this</span>-&gt;checkCallbackData();</span><br><span class="line">    <span class="comment">#3.检查第三方签名是否合法</span></span><br><span class="line">    <span class="keyword">$this</span>-&gt;checkign();</span><br><span class="line">    <span class="comment">#4.根据传入数据/调用第三方查询接口判断该订单在第三方系统里是否支付成功</span></span><br><span class="line">    <span class="keyword">$this</span>-&gt;checkPaymentStatus();</span><br><span class="line">    <span class="comment">#5.记录返回数据</span></span><br><span class="line">    <span class="keyword">$this</span>-&gt;saveResponseLog();</span><br><span class="line">    <span class="comment">#6.更改订单状态</span></span><br><span class="line">    <span class="comment">#6.1如果订单是待支付状态，则更新支付网关内的订单状态，同时通知订单中心支付完成，可出库</span></span><br><span class="line">    <span class="keyword">if</span> ($orderStatus == <span class="string">"待支付"</span>) &#123;</span><br><span class="line">        <span class="keyword">$this</span>-&gt;updateOrderStatusFinish();</span><br><span class="line">        <span class="keyword">$this</span>-&gt;notifyOC();</span><br><span class="line">        <span class="comment">#6.2如果订单已经支付完成</span></span><br><span class="line">    &#125; <span class="keyword">elseif</span>($orderStatus == <span class="string">"已经支付完成"</span>) &#123;</span><br><span class="line">        <span class="keyword">if</span>(<span class="string">"相同的支付方式，第三方交易号也相同"</span>)&#123;</span><br><span class="line">            <span class="comment">#6.2.1重复通知，幂等处理</span></span><br><span class="line">            <span class="keyword">$this</span>-&gt;notifyOC();</span><br><span class="line">        &#125;<span class="keyword">elseif</span>(<span class="string">"相同的支付方式，不同的交易号"</span>)&#123;</span><br><span class="line">            <span class="comment">#6.2.2同渠道重复支付,将该信息记入重复支付表，进行退款</span></span><br><span class="line">            <span class="keyword">$this</span>-&gt;addRepeatPayment();</span><br><span class="line">        &#125;<span class="keyword">elseif</span>(<span class="string">"不同的支付方式"</span>)&#123;</span><br><span class="line">            <span class="comment">#6.2.3不同渠道重复支付，将该信息记入重复支付表，进行退款</span></span><br><span class="line">            <span class="keyword">$this</span>-&gt;addRepeatPayment();</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">#7.根据订单状态，跳转到支付成功/失败页面</span></span><br><span class="line">    <span class="keyword">if</span>(<span class="string">"是同步回调"</span>)&#123;</span><br><span class="line">        redirect(<span class="string">"电商支付成功/失败页面"</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="说明点-1"><a href="#说明点-1" class="headerlink" title="说明点"></a>说明点</h4><ul><li>日志：传入和返回数据都做记录</li><li>签名校验：校验第三方签名十分重要，这是防止他人攻击的方案之一</li><li>判断支付成功：情况允许条件下，建议请求第三方查询接口来判断是否支付成功，这是防止他人攻击的方案之二</li><li>跳转支付成功/失败页面：当发起支付时，支付成功或者失败页面的 url 会传给支付网关。之所以请求支付时传递该 url，原因在于不同终端，url 不同。</li><li>该流程需要两个定时脚本进行支撑<ul><li>通知订单中心支付完成。因为更改支付系统的订单状态和通知订单中心未必会同时成功。</li><li>重复支付订单自动退款</li></ul></li></ul><h4 id="实例-1"><a href="#实例-1" class="headerlink" title="实例"></a>实例</h4><img src="/2019/12/04/third-party-pay/image-20191103234746041.png"><img src="/2019/12/04/third-party-pay/image-20191103234833065.png"><img src="/2019/12/04/third-party-pay/image-20191103235105865.png"><h3 id="3-退款-1"><a href="#3-退款-1" class="headerlink" title="3.退款"></a>3.退款</h3><p>退款整体流程一般为，财务系统向支付网关发起退款请求，支付网关将退款记录到表。支付系统定时进行退款，财务系统定时查询退款状态。</p><h4 id="时序图-2"><a href="#时序图-2" class="headerlink" title="时序图"></a>时序图</h4><img src="/2019/12/04/third-party-pay/image-20191104202629804.png"><h4 id="伪代码-2"><a href="#伪代码-2" class="headerlink" title="伪代码"></a>伪代码</h4><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#3.退款</span></span><br><span class="line"><span class="comment">#支付网关</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">refund</span><span class="params">()</span></span>&#123;</span><br><span class="line">    <span class="comment">#1.获取待退款列表</span></span><br><span class="line">    $refundList = <span class="keyword">$this</span>-&gt;getNotRefundList();</span><br><span class="line">    <span class="keyword">foreach</span> ($refundList <span class="keyword">as</span> $item)&#123;</span><br><span class="line">        <span class="comment">#2.检查数据，订单是否支付成功，退款金额是否小于订单金额</span></span><br><span class="line">        <span class="keyword">$this</span>-&gt;checkRefundParams();</span><br><span class="line">        <span class="comment">#3.记录退款请求数据</span></span><br><span class="line">        <span class="keyword">$this</span>-&gt;saveRefundReqData();</span><br><span class="line">        <span class="comment">#4.查询退款结果</span></span><br><span class="line">        <span class="keyword">$this</span>-&gt;checkRefundStatus();</span><br><span class="line">        <span class="comment">#5.调用第三方接口进行退款</span></span><br><span class="line">        <span class="keyword">$this</span>-&gt;refund();</span><br><span class="line">        <span class="comment">#6.记录第三方返回数据</span></span><br><span class="line">        <span class="keyword">$this</span>-&gt;saveRefundResponse();</span><br><span class="line">        <span class="comment">#7.更新退款状态</span></span><br><span class="line">        <span class="keyword">$this</span>-&gt;updateRefundStatus();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="说明点-2"><a href="#说明点-2" class="headerlink" title="说明点"></a>说明点</h4><ul><li><p><strong>退款类型</strong>：退款一般分为两种类型</p><ul><li><p>部分退款：有的第三方公司不支持，需要提前咨询</p></li><li><p>全额退款</p></li></ul></li><li><p><strong>待退款列表</strong>：建议同一个订单的多个退款单，确认处理完一个之后，再处理另一个</p></li><li><strong>退款相关状态</strong>：调用第三方接口状态 callStatus，退款结果状态 refundStatus。callStatus 成功，refundStatus 未成功，可定时查退款状态</li><li>查询退款结果：可选，根据第三方 API 情况做判断</li><li>日志：请求日志和返回日志都进行记录</li></ul><h3 id="4-对账"><a href="#4-对账" class="headerlink" title="4.对账"></a>4.对账</h3><p>对账的完成，需要第三方提供前一天的支付成功数据，然后和支付网关系统中当天的支付成功数据进行对比。</p><p>对不上账的类型有：</p><ul><li><strong>支付网关有数据，第三方没有数据</strong><ul><li>可能被黑客攻击了，用户没有真正支付，但是我们发货了</li><li>代码有问题，用户没有完成支付，但是系统认为支付成功了</li><li>第三方提供数据不全</li></ul></li><li>支付网关没有数据，第三方有数据<ul><li>用户支付成功，但是同步或者异步通知都失败了</li></ul></li><li>金额不一致<ul><li>代码有问题，电商发起支付金额和真正调用第三方金额不一致</li><li>第三方提供数据有问题</li></ul></li></ul><p>对账是验证支付系统准确的重要一环，可以帮助开发人员今早发现很多问题，建议每一种支付方式都要支持，并且及时对账。</p><h4 id="实例-2"><a href="#实例-2" class="headerlink" title="实例"></a>实例</h4><img src="/2019/12/04/third-party-pay/image-20191104204616206.png"><h2 id="三、支付进阶"><a href="#三、支付进阶" class="headerlink" title="三、支付进阶"></a>三、支付进阶</h2><p>完成上述四个流程后，就实现了支付渠道的对接。然后需要考虑支付网关的实现。支付网关包含多个支付渠道，对外部提供统一的接口。支付网关一般需要考虑如下问题：</p><ul><li><p>支付网关中接入多个第三方支付，如何设计使变动最小？</p></li><li><p>相同的银行，多个第三方支付支持，如何设计分配流量的系统？</p></li><li><p>如何监控每个第三方支付的成功率，并动态切换第三方支付？</p></li><li><p>如何做好风控？</p></li></ul><p>这些问题的讲解涉及到很大的篇幅，今后会进行讲解。大家可以先自己思考一些解决方案。</p><h3 id="下回预告"><a href="#下回预告" class="headerlink" title="下回预告"></a>下回预告</h3><p>到目前为止，已经介绍了对接支付渠道需要对接哪些接口，以及整体流程如何搭建。接下来将会介绍支付网关的设计以及如何灵活分配支付流量到各个支付渠道。</p><hr><p><strong>作者</strong></p><p>逄志强，小米信息技术部海外商城组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com(武汉)</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;如何高效对接第三方支付&quot;&gt;&lt;a href=&quot;#如何高效对接第三方支付&quot; class=&quot;headerlink&quot; title=&quot;如何高效对接第三方支付&quot;&gt;&lt;/a&gt;如何高效对接第三方支付&lt;/h1&gt;&lt;p&gt;&lt;strong&gt;[作者简介]&lt;/strong&gt; 逄志强，海外小米网研
      
    
    </summary>
    
    
  </entry>
  
  <entry>
    <title>走进Golang之运行与Plan9汇编</title>
    <link href="https://xiaomi-info.github.io/2019/11/27/golang-compiler-plan9/"/>
    <id>https://xiaomi-info.github.io/2019/11/27/golang-compiler-plan9/</id>
    <published>2019-11-27T02:37:47.000Z</published>
    <updated>2021-05-24T03:39:30.235Z</updated>
    
    <content type="html"><![CDATA[<p>通过上一篇<a href="https://xiaomi-info.github.io/2019/11/13/golang-compiler-principle/">走进 Golang 之汇编原理</a>，我们知道了目标代码的生成经历了那些过程。今天我们一起来学习一下生成的目标代码如何在计算机上执行。以及通过查阅 <code>Golang</code> 的 Plan9 汇编来了解 Golang 的一些内部秘密。</p><h2 id="Golang-的运行环境"><a href="#Golang-的运行环境" class="headerlink" title="Golang 的运行环境"></a>Golang 的运行环境</h2><p>当我们把编译后的 Go 代码运行起来，它会以进程的方式出现在系统中。然后开始处理请求、数据，我们会看到这个进程占用了内存消耗、cpu 占比等等信息。本文就是要来解释在程序的运行过程中，内存、CPU、操作系统（当然还有其它的硬件，文中关系不大，就不说了）是如何进行配合，完成了我们代码所指定的事情。</p><h3 id="内存"><a href="#内存" class="headerlink" title="内存"></a>内存</h3><p>首先，我们先来说说内存。先来看一个我们运行的 go 进程。</p><p>代码如下：</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">  <span class="string">"fmt"</span></span><br><span class="line">  <span class="string">"log"</span></span><br><span class="line">  <span class="string">"net/http"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  http.HandleFunc(<span class="string">"/"</span>, sayHello)</span><br><span class="line"></span><br><span class="line">  err := http.ListenAndServe(<span class="string">":9999"</span>, <span class="literal">nil</span>)</span><br><span class="line">  <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    log.Fatal(<span class="string">"ListenAndServe: "</span>, err)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">sayHello</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">  fmt.Printf(<span class="string">"fibonacci: %d\n"</span>, fibonacci(<span class="number">1000</span>))</span><br><span class="line">  _, _ = fmt.Fprint(w, <span class="string">"Hello World!"</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">fibonacci</span><span class="params">(num <span class="keyword">int</span>)</span> <span class="title">int</span></span> &#123;</span><br><span class="line">  <span class="keyword">if</span> num &lt; <span class="number">2</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">1</span></span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> fibonacci(num<span class="number">-1</span>) + fibonacci(num<span class="number">-2</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>来看一下执行情况</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">dayu.com &gt;ps aux</span><br><span class="line"></span><br><span class="line">USER               PID   %CPU  %MEM      VSZ     RSS    TT  STAT   STARTED    TIME     COMMAND</span><br><span class="line">xxxxx              3584  99.2  0.1     4380456  4376   s003  R+    8:33下午   0:05.81  ./myhttp</span><br></pre></td></tr></table></figure><p>这里我们先来不关注其它指标，先来看 <code>VSZ</code> 与 <code>RSS</code>。</p><ul><li>VSZ: 是指虚拟地址，他是程序实际操作的内存。包含了分配还没有使用的内存。</li><li>RSS: 是实际的物理内存，包含了栈内存与堆内存。</li></ul><p>每一个进程都是运行在自己的内存沙盒里，程序被分配的地址都是 “虚拟内存”，物理内存对程序开发者来说实际是不可见的，而且虚拟地址比进程实际的物理地址要大的多。我们经常编程中取指针对应的地址实际就是虚拟地址。这里一定要注意区分虚拟内存与物理内存。来一张图感受一下。</p><img src="/2019/11/27/golang-compiler-plan9/go-hb-0.jpg"><p>这张图主要是为了说明两个问题：</p><ol><li>程序使用的是虚拟内存，但是操作系统会把虚拟内存映射到物理内存；你会发现自己机器上所有进程的 VSZ 总和要比实际物理内存大得多；</li><li>物理内存可以被多个进程共享，甚至一个进程内的不同地址可能映射的都是同一个物理内存地址。</li></ol><p>上面搞明白了程序中的内存具体是指什么，接下来说明程序是如何使用内存的（虚拟内存），内存说白了就是比硬盘存取速度更快的一个硬件，为了方便内存的管理，操作系统把分配给进程的内存划分成了不同的功能块。像我们经常说的：代码区，静态数据区，堆区，栈区等。</p><p>这里借用一张网络上的图来看一下。</p><img src="/2019/11/27/golang-compiler-plan9/go-hb-1.jpg"><p>这里就是我们程序（进程）在虚拟内存中的分布。</p><p>代码区：存放的就是我们编译后的机器码，一般来说这个区域只能是只读。</p><p>静态数据区：存放的是全局变量与常量。这些变量的地址编译的时候就确定了（这也是使用虚拟地址的好处，如果是物理地址，这些地址编译的时候是不可能确定的）。Data 与 BSS 都属于这一部分。这部分只有程序中止（kill 掉、crasg 掉等）才会被销毁。</p><p>栈区：主要是 <code>Golang</code> 里边的函数、方法以及其本地变量存储的地方。这部分伴随函数、方法开始执行而分配，运行完后就被释放，特别注意这里的释放并不会清空内存。后面文章讲内存分配的时候再详细说；还有一个点需要记住栈一般是从高地址向低地址方向分配，换句话说：高地址属于栈低，低地址属于栈顶，它分配方向与堆是相反的。</p><p>堆区：像 <code>C/C++</code> 语言，堆完全是程序员自己控制的。但是 <code>Golang</code> 里边由于有 GC 机制，我们写代码的时候并不需要关心内存是在栈还是堆上分配。<code>Golang</code> 会自己判断如果变量的生命周期在函数退出后还不能销毁或者栈上资源不够分配等等情况，就会被放到堆上。堆的性能会比栈要差一些。原因也留到内存分配相关的文章再给大家介绍。</p><p>内存的结构搞明白了，我们的程序被加载到内存还需要操作系统来指挥才能正确运行。</p><p>补充一个比较重要的概念：</p><blockquote><p>寻址空间：一般指的是 CPU 对于内存寻址的能力，通俗地说，就是能最多用到多少内存的一个问题。比如：32 条地址线（32 位机器），那么总的地址空间就有 2^32 个，如果是 64 位机器，就是 2^64 个寻址空间。可以使用 <code>uname -a</code> 来查看自己系统支持的位数字。</p></blockquote><h3 id="操作系统、CPU、内存互相配合"><a href="#操作系统、CPU、内存互相配合" class="headerlink" title="操作系统、CPU、内存互相配合"></a>操作系统、CPU、内存互相配合</h3><p>为了讲清楚程序运行与调用，我们得先理清楚操作系统、内存、CPU、寄存器这几者之间的关系。</p><ul><li>CPU: 计算机的大脑，它才能理解并执行指令；</li><li>寄存器：严格讲寄存器是 CPU 的组成部分，它主要负责 CPU 在计算时临时存储数据；当然 CPU 还有多级的高速缓存，与我们这里相关度不大，就略过，大家知道其目的是为了弥补内存与 CPU 速度的差距即可；</li><li>内存：像上面内存被划分成不同区，每一部分存了不同的数据；当然这些区的划分、以及虚拟内存与物理内存的映射都是操作系统来做的；</li><li>操作系统：控制各种硬件资源，为其它运行的程序提供操作接口（系统调用）及管理。</li></ul><p>这里操作系统是一个软件，CPU、寄存器、内存（物理内存）都是实打实的硬件。操作系统虽然也是一堆代码写出来的。但是她是硬件对其它应用程序的接口。总的来讲操作系统通过系统调用控制所有的硬件资源，他把其它的程序调度到 CPU 上让其它程序执行，但是为了让每个程序都有机会使用 CPU，CPU 又通过时间中断把控制权交给操作系统。</p><p>让操作系统可以控制我们的程序，我们编写的程序需要遵循操作系统的规定。这样操作系统才能控制程序执行、切换进程等操作。</p><p>最后我们的代码被编译成机器码之后，本质就是一条条的指令。我们期望的就是 CPU 去执行完这些指令进而完成任务。而操作系统又能够帮助我们让 CPU 来执行代码以及提供所需资源的调用接口（系统调用）。是不是非常简单？</p><h2 id="Go-程序的调用规约"><a href="#Go-程序的调用规约" class="headerlink" title="Go 程序的调用规约"></a>Go 程序的调用规约</h2><p>在上面我们知道整个虚拟内存被我们划分为：代码区、静态数据区、栈区、堆区。接下来要讲的 Go 程序的调用规约（其实就是函数、方法运行的规则），主要是涉及上面所说的栈部分（堆部分会在内存分配的文章里边去讲）。以及计算机软硬各个部分如何配合。接下来我们就来看一下程序的基本单位函数跟方法是怎么执行与相互调用的。</p><h3 id="函数在栈上的分布"><a href="#函数在栈上的分布" class="headerlink" title="函数在栈上的分布"></a>函数在栈上的分布</h3><p>这一部分，我们先来了解一些理论，然后接着用一个实际的例子来分析一下。先通过一张图来看一下在 <code>Golang</code> 中函数是如何在栈上分布的。</p><p>几个涉及到的专业用语：</p><ul><li>栈：这里说的栈跟上面的解释含义一致。无论是进程、线程、goroutine 都有自己的调用栈；</li><li>栈帧：可以理解是函数调用时在栈上为函数所分配的区域；</li><li>调用者：caller，比如：a 函数调用了 b 函数，那么 a 就是调用者</li><li>被调者：callee，还是上面的例子，b 就是被调者</li></ul><img src="/2019/11/27/golang-compiler-plan9/go-hb-2.jpg"><p>这幅图所展示的就是一个 <code>栈帧</code> 的结构。也可以说栈桢是栈给一个函数分配的栈空间，它包括了函数调用者地址、本地变量、返回值地址、调用者参数等信息。</p><p>这里有几个注意点，图中的 <code>BP</code>、<code>SP</code>都表示对应的寄存器。</p><ul><li>BP：基址指针寄存器(extended base pointer)，也叫帧指针，存放着一个指针，表示函数栈开始的地方。</li><li>SP：栈指针寄存器(extended stack pointer)，存放着一个指针，存储的是函数栈空间的栈顶，也就是函数栈空间分配结束的地方，注意这里是硬件寄存器，不是 Plan9 中的伪寄存器。</li></ul><p><code>BP</code> 与 <code>SP</code> 放在一起，一个表示开始（栈顶）、一个表示结束（栈低）。</p><p>有了上面的基础知识，接着下面用实际的例子来验证一下。</p><h3 id="Go-的调用实例"><a href="#Go-的调用实例" class="headerlink" title="Go 的调用实例"></a>Go 的调用实例</h3><p>才开始，我们就从一个简单的函数开始来分析一下整个函数的调用过程（下面涉及到 <code>Plan9</code> 汇编，请别慌，大部分都能够看懂，并且我也会写注释）。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">  a := <span class="number">3</span></span><br><span class="line">  b := <span class="number">2</span></span><br><span class="line">  returnTwo(a, b)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">returnTwo</span><span class="params">(a, b <span class="keyword">int</span>)</span> <span class="params">(c, d <span class="keyword">int</span>)</span></span> &#123;</span><br><span class="line">  tmp := <span class="number">1</span> <span class="comment">// 这一行的主要目的是保证栈桢不为0，方便分析</span></span><br><span class="line">  c = a + b</span><br><span class="line">  d = b - tmp</span><br><span class="line">  <span class="keyword">return</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>上面有两个函数，<code>main</code> 定义了两个本地变量，然后调用 <code>returnTwo</code> 函数。<code>returnTwo</code> 函数有两个参数与两个返回值。设计两个返回值主要是一起来看一下 <code>golang</code> 的多返回值是如何实现的。接下来我们把上面的代码对应的汇编代码展示出来。</p><img src="/2019/11/27/golang-compiler-plan9/go-hb-3.jpg"><p>有几行代码需要特别解释下，</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">0x0000 00000 (test1.go:3)       TEXT    &quot;&quot;.main(SB), ABIInternal, $56-0</span><br></pre></td></tr></table></figure><p>这一行中的重点信息：<code>$56-0</code>。<strong>56</strong> 表示的该函数栈桢大小（两个本地变量，两个参数是 int 类型，两个返回值是 int 类型，1 个保存 base pointer，合计 7 * 8 = 56）；0 表示 <code>mian</code> 函数的参数与返回值大小。待会可以在 <code>returnTwo</code> 中去看一下它的返回值又是多少。</p><p>接下来在看一下计算机是怎么在栈上分配大小的。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">0x000f 00015 (test1.go:3)       SUBQ    $56, SP // 分配，56的大小在上面第一行定义了</span><br><span class="line">... ...</span><br><span class="line">0x004b 00075 (test1.go:7)       ADDQ    $56, SP // 释放掉，但是并未清空</span><br></pre></td></tr></table></figure><p>这两行，一个是分配，一个是释放。为什么用了 <code>SUBQ</code> 指令就能进行分配呢？而 <code>ADDQ</code> 是释放？记得我们前面说过吗？ <code>SP</code> 是一个指针寄存器，并且指向栈顶，栈又是从高地址向低地址分配。那么对它做一次减法，是不是表示从高地址向低地址方向移动指针了呢？释放也是同样的道理，一次加法操作又把 <code>SP</code> 恢复到初始状态。</p><p>再来看一下对 <code>BP</code> 寄存器的操作。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">0x0013 00019 (test1.go:3)       MOVQ    BP, 48(SP) // 保存BP</span><br><span class="line">0x0018 00024 (test1.go:3)       LEAQ    48(SP), BP // BP存放了新的地址</span><br><span class="line">... ...</span><br><span class="line">0x0046 00070 (test1.go:7)       MOVQ    48(SP), BP // 恢复BP的地址</span><br></pre></td></tr></table></figure><p>这三行代码是不是感觉很变扭？写来写去让人云里雾里的。我先用文字描述一下，后面再用图来解释。</p><blockquote><p>我们先做如下假设：此时 BP 指向的 <strong>值</strong> 是：0x00ff，48(SP) 的 <strong>地址</strong> 是：0x0008。</p></blockquote><ul><li>第一条指令 <code>MOVQ BP, 48(SP)</code> 是把 <code>0x00ff</code> 写入到 <code>48(SP)</code>的位置；</li><li>第二条指令 <code>LEAQ 48(SP), BP</code> 是更新寄存器指针，让 <code>BP</code> 保存 <code>48(SP)</code> 这个位置的地址，也就是 <code>0x00ff</code> 这个值。</li><li>第三条指令 <code>MOVQ 48(SP), BP</code> ，因为一开始 <code>48(SP)</code> 保存了最开始 <code>BP</code> 的所存的值 <code>0x00ff</code>，所以这里是又把 <code>BP</code> 恢复回去了。</li></ul><p>这几行代码的作用至关重要，正因为如此在执行的时候，我们才能找到函数开始的地方以及回到调用函数的位置，它才可以继续往下执行（如果觉得饶，先放过，后面有图，看完后再回来理解）。接着来看一下 <code>returnTwo</code> 函数。</p><img src="/2019/11/27/golang-compiler-plan9/go-hb-4.jpg"><p>这里 <code>NOSPLIT|ABIInternal, $0-32</code> 说明，该函数的栈桢大小是 0，由于有两个 int 参数，以及 2 个 int 返回值，合计为 <code>4*8 = 32</code> 字节大小，是不是跟上面的 <code>main</code> 函数对上了？。</p><p>这里有没有对 <code>returnTwo</code> 函数的栈桢大小是 0 表示迷惑呢？难道这个函数不需要栈空间吗？其实主要原因是：golang 的参数传递与返回值都是要求使用栈来进行的（这也是为什么 go 能够支持多参数返回的原因）。所以参数与返回值所需空间都由 <code>caller</code> 来提供。</p><p>接下来，我们用完整的图来演示一下这个调用过程。</p><img src="/2019/11/27/golang-compiler-plan9/go-hb-5.jpg"><blockquote><p>这个图就画了将近 1 个小时，希望对大家理解有帮助。</p></blockquote><p>整个的流程是：初始化 —-&gt; call main function —-&gt; call returnTwo function —-&gt; returnTwo return —-&gt; main return。</p><p>通过这张图，在结合我上面的文字解释，相信大家能够理解了。不过这里还有几个注意点：</p><ul><li><strong>BP</strong> 与 <strong>SP</strong> 是寄存器，它保存的是栈上的地址，所以执行中可以对 <code>SP</code> 做运算找到下一个指令的位置；</li><li>栈被回收 <code>ADDQ $56, SP</code> ，只是改变了 <code>SP</code> 指向的位置，内存中的数据并不会清空，只有下次被分配使用的时候才会清空；</li><li>callee 的参数、返回值内存都是 caller 分配的；</li><li>returnTwo ret 的时候，<strong>call returnTwo 的 next 指令</strong> 所在栈位置会被弹出，也就是图中 <code>0x0d00</code> 地址所保存的指令，所以 returnTwo 函数返回后，<code>SP</code> 又指向了 <code>0x0d08</code> 地址。</li></ul><hr><p>由于上面涉及到一些 <code>Plan9</code> 的知识，就顺带一起介绍一些它的语法，如果直接讲语法会很枯燥，下面会结合一些实际中会用到的情况来介绍。既有收获又能学会语法。</p><h2 id="Go-的汇编-plan9"><a href="#Go-的汇编-plan9" class="headerlink" title="Go 的汇编 plan9"></a>Go 的汇编 plan9</h2><p>我们整个程序的编译最终会被翻译成机器码，而汇编可以算是机器码的文本形式，他们之间可以一一对应。所以如果我们能够看懂汇编一点点就能够分析出很多实际问题。</p><p>开发 go 语言的都是当前世界最 TOP 的那群程序员，他们选择了持续装逼，不用标准的 <strong>AT&amp;T</strong> 也不用 <strong>Intel</strong> 汇编器，偏要自己搞一套，没办法，谁让人家牛呢！Golang 的汇编是基于 <code>Plan9</code> 汇编的，个人觉得要完全学懂太复杂了，因为这涉及到很多底层知识。不过如果只是要求看懂还是能够做到的。下面我们就举一些例子来试试看。</p><blockquote><p>PS: 这东西完全学懂也没有必要，投入产出比太低了，对于一个应用工程师能够看懂就行。</p></blockquote><p>在正式开始前，我们还是补充一些必要信息，上文已经涉及过一些，为了完整这里在整体介绍一下。</p><p><strong>几个重要的伪寄存器：</strong></p><ul><li>SB：是一个虚拟寄存器，保存了静态基地址(static-base) 指针，即我们程序地址空间的开始地址；</li><li>NOSPLIT：向编译器表明不应该插入 <code>stack-split</code> 的用来检查栈需要扩张的前导指令；</li><li>FP：使用形如 symbol+offset(FP) 的方式，引用函数的输入参数；</li><li>SP：plan9 的这个 SP 寄存器指向当前栈帧的局部变量的开始位置，使用形如 symbol+offset(SP) 的方式，引用函数的局部变量，注意：这个寄存器与上文的寄存器是不一样的，这里是伪寄存器，而我们展示出来的都是硬件寄存器。</li></ul><p>其它还有一些操作指令，根据名字多半都能够看出来，就不再介绍，直接开始干。</p><h3 id="查看-go-应用代码对应的翻译函数"><a href="#查看-go-应用代码对应的翻译函数" class="headerlink" title="查看 go 应用代码对应的翻译函数"></a>查看 go 应用代码对应的翻译函数</h3><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">test</span><span class="params">()</span> []<span class="title">string</span></span> &#123;</span><br><span class="line">    a := <span class="built_in">make</span>([]<span class="keyword">string</span>, <span class="number">10</span>)</span><br><span class="line">    <span class="keyword">return</span> a</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">--------</span><br><span class="line"></span><br><span class="line"><span class="string">""</span>.test STEXT size=<span class="number">151</span> args=<span class="number">0x18</span> locals=<span class="number">0x40</span></span><br><span class="line">        <span class="number">0x0000</span> <span class="number">00000</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       TEXT    <span class="string">""</span>.test(SB), ABIInternal, $<span class="number">64</span><span class="number">-24</span> <span class="comment">// 栈帧大小，与参数、返回值大小</span></span><br><span class="line">        <span class="number">0x0000</span> <span class="number">00000</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       MOVQ    (TLS), CX</span><br><span class="line">        <span class="number">0x0009</span> <span class="number">00009</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       CMPQ    SP, <span class="number">16</span>(CX)</span><br><span class="line">        <span class="number">0x000d</span> <span class="number">00013</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       JLS     <span class="number">141</span></span><br><span class="line">        <span class="number">0x000f</span> <span class="number">00015</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       SUBQ    $<span class="number">64</span>, SP</span><br><span class="line">        <span class="number">0x0013</span> <span class="number">00019</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       MOVQ    BP, <span class="number">56</span>(SP)</span><br><span class="line">        <span class="number">0x0018</span> <span class="number">00024</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       LEAQ    <span class="number">56</span>(SP), BP</span><br><span class="line">        ... ...</span><br><span class="line">        <span class="number">0x001d</span> <span class="number">00029</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       MOVQ    $<span class="number">0</span>, <span class="string">""</span>.~r0+<span class="number">72</span>(SP)</span><br><span class="line">        <span class="number">0x0026</span> <span class="number">00038</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       XORPS   X0, X0</span><br><span class="line">        <span class="number">0x0029</span> <span class="number">00041</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       MOVUPS  X0, <span class="string">""</span>.~r0+<span class="number">80</span>(SP)</span><br><span class="line">        <span class="number">0x002e</span> <span class="number">00046</span> (test1.<span class="keyword">go</span>:<span class="number">7</span>)       PCDATA  $<span class="number">2</span>, $<span class="number">1</span></span><br><span class="line">        <span class="number">0x002e</span> <span class="number">00046</span> (test1.<span class="keyword">go</span>:<span class="number">7</span>)       LEAQ    <span class="keyword">type</span>.<span class="keyword">string</span>(SB), AX</span><br><span class="line">        <span class="number">0x0035</span> <span class="number">00053</span> (test1.<span class="keyword">go</span>:<span class="number">7</span>)       PCDATA  $<span class="number">2</span>, $<span class="number">0</span></span><br><span class="line">        <span class="number">0x0035</span> <span class="number">00053</span> (test1.<span class="keyword">go</span>:<span class="number">7</span>)       MOVQ    AX, (SP)</span><br><span class="line">        <span class="number">0x0039</span> <span class="number">00057</span> (test1.<span class="keyword">go</span>:<span class="number">7</span>)       MOVQ    $<span class="number">10</span>, <span class="number">8</span>(SP)</span><br><span class="line">        <span class="number">0x0042</span> <span class="number">00066</span> (test1.<span class="keyword">go</span>:<span class="number">7</span>)       MOVQ    $<span class="number">10</span>, <span class="number">16</span>(SP)</span><br><span class="line">        <span class="number">0x004b</span> <span class="number">00075</span> (test1.<span class="keyword">go</span>:<span class="number">7</span>)       CALL    runtime.makeslice(SB) <span class="comment">// 对应的底层runtime function</span></span><br><span class="line">        ... ...</span><br><span class="line">        <span class="number">0x008c</span> <span class="number">00140</span> (test1.<span class="keyword">go</span>:<span class="number">8</span>)       RET</span><br><span class="line">        <span class="number">0x008d</span> <span class="number">00141</span> (test1.<span class="keyword">go</span>:<span class="number">8</span>)       NOP</span><br><span class="line">        <span class="number">0x008d</span> <span class="number">00141</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       PCDATA  $<span class="number">0</span>, $<span class="number">-1</span></span><br><span class="line">        <span class="number">0x008d</span> <span class="number">00141</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       PCDATA  $<span class="number">2</span>, $<span class="number">-1</span></span><br><span class="line">        <span class="number">0x008d</span> <span class="number">00141</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       CALL    runtime.morestack_noctxt(SB)</span><br><span class="line">        <span class="number">0x0092</span> <span class="number">00146</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       JMP     <span class="number">0</span></span><br></pre></td></tr></table></figure><p>根据对应的代码行数与名字，很明显的可以看到应用层写的 <code>make</code> 对应底层是 <code>makeslice</code>。</p><h3 id="逃逸分析"><a href="#逃逸分析" class="headerlink" title="逃逸分析"></a>逃逸分析</h3><p>这里先说一下逃逸分析的概念。这里牵扯到栈、堆分配的问题。如果变量被分配到栈上，会伴随函数调用结束自动回收，并且分配效率很高；其次分配到堆上，则需要 GC 进行标记回收。所谓逃逸就是指变量从栈上逃到了堆上（很多人对这个概念都不清楚就在谈逃逸分析，面试遇到了好几次 😓）。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">test</span><span class="params">()</span> *<span class="title">int</span></span> &#123;</span><br><span class="line">  t := <span class="number">3</span></span><br><span class="line">  <span class="keyword">return</span> &amp;t</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">------</span><br><span class="line"></span><br><span class="line"><span class="string">""</span>.test STEXT size=<span class="number">98</span> args=<span class="number">0x8</span> locals=<span class="number">0x20</span></span><br><span class="line">        <span class="number">0x0000</span> <span class="number">00000</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       TEXT    <span class="string">""</span>.test(SB), ABIInternal, $<span class="number">32</span><span class="number">-8</span></span><br><span class="line">        <span class="number">0x0000</span> <span class="number">00000</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       MOVQ    (TLS), CX</span><br><span class="line">        <span class="number">0x0009</span> <span class="number">00009</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       CMPQ    SP, <span class="number">16</span>(CX)</span><br><span class="line">        <span class="number">0x000d</span> <span class="number">00013</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       JLS     <span class="number">91</span></span><br><span class="line">        <span class="number">0x000f</span> <span class="number">00015</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       SUBQ    $<span class="number">32</span>, SP</span><br><span class="line">        <span class="number">0x0013</span> <span class="number">00019</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       MOVQ    BP, <span class="number">24</span>(SP)</span><br><span class="line">        <span class="number">0x0018</span> <span class="number">00024</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       LEAQ    <span class="number">24</span>(SP), BP</span><br><span class="line">        ... ...</span><br><span class="line">        <span class="number">0x001d</span> <span class="number">00029</span> (test1.<span class="keyword">go</span>:<span class="number">6</span>)       MOVQ    $<span class="number">0</span>, <span class="string">""</span>.~r0+<span class="number">40</span>(SP)</span><br><span class="line">        <span class="number">0x0026</span> <span class="number">00038</span> (test1.<span class="keyword">go</span>:<span class="number">7</span>)       PCDATA  $<span class="number">2</span>, $<span class="number">1</span></span><br><span class="line">        <span class="number">0x0026</span> <span class="number">00038</span> (test1.<span class="keyword">go</span>:<span class="number">7</span>)       LEAQ    <span class="keyword">type</span>.<span class="keyword">int</span>(SB), AX</span><br><span class="line">        <span class="number">0x002d</span> <span class="number">00045</span> (test1.<span class="keyword">go</span>:<span class="number">7</span>)       PCDATA  $<span class="number">2</span>, $<span class="number">0</span></span><br><span class="line">        <span class="number">0x002d</span> <span class="number">00045</span> (test1.<span class="keyword">go</span>:<span class="number">7</span>)       MOVQ    AX, (SP)</span><br><span class="line">        <span class="number">0x0031</span> <span class="number">00049</span> (test1.<span class="keyword">go</span>:<span class="number">7</span>)       CALL    runtime.newobject(SB) <span class="comment">// 堆上分配空间，表示逃逸了</span></span><br><span class="line">        ... ...</span><br></pre></td></tr></table></figure><p>这里如果是对 <code>slice</code> 使用汇编进行逃逸分析，并不会很直观。因为只会看到调用了 <code>runtime.makeslice</code> 函数，该函数内部其实又调用了 <code>runtime.mallocgc</code> 函数，这个函数会分配的内存其实就是堆上的内存（如果栈上足够保存，是不会看到对 <code>runtime.makslice</code> 函数的调用）。</p><p>实际 go 也提供了更方便的命令来进行逃逸分析：<code>go build -gcflags=&quot;-m&quot;</code>，如果真的是做逃逸分析，建议使用该命令，别折腾用汇编。</p><h3 id="传值还是传指针"><a href="#传值还是传指针" class="headerlink" title="传值还是传指针"></a>传值还是传指针</h3><p>对于 golang 中的基本类型：字符串、整型、布尔类型就不多说了，肯定是值传递，那么对于结构体、指针到底是值传递还是指针传递呢？</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Student <span class="keyword">struct</span> &#123;</span><br><span class="line">    name <span class="keyword">string</span></span><br><span class="line">    age  <span class="keyword">int</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    jack := &amp;Student&#123;<span class="string">"jack"</span>, <span class="number">30</span>&#125;</span><br><span class="line">    test(jack)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">test</span><span class="params">(s *Student)</span> *<span class="title">Student</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">-------</span><br><span class="line"></span><br><span class="line"><span class="string">""</span>.test STEXT nosplit size=<span class="number">20</span> args=<span class="number">0x10</span> locals=<span class="number">0x0</span></span><br><span class="line">        <span class="number">0x0000</span> <span class="number">00000</span> (test1.<span class="keyword">go</span>:<span class="number">14</span>)      TEXT    <span class="string">""</span>.test(SB), NOSPLIT|ABIInternal, $<span class="number">0</span><span class="number">-16</span></span><br><span class="line">        ... ...</span><br><span class="line">        <span class="number">0x0000</span> <span class="number">00000</span> (test1.<span class="keyword">go</span>:<span class="number">14</span>)      MOVQ    $<span class="number">0</span>, <span class="string">""</span>.~r1+<span class="number">16</span>(SP) <span class="comment">// 初始返回值为0</span></span><br><span class="line">        <span class="number">0x0009</span> <span class="number">00009</span> (test1.<span class="keyword">go</span>:<span class="number">15</span>)      PCDATA  $<span class="number">2</span>, $<span class="number">1</span></span><br><span class="line">        <span class="number">0x0009</span> <span class="number">00009</span> (test1.<span class="keyword">go</span>:<span class="number">15</span>)      PCDATA  $<span class="number">0</span>, $<span class="number">1</span></span><br><span class="line">        <span class="number">0x0009</span> <span class="number">00009</span> (test1.<span class="keyword">go</span>:<span class="number">15</span>)      MOVQ    <span class="string">""</span>.s+<span class="number">8</span>(SP), AX <span class="comment">// 将引用地址复制到 AX 寄存器</span></span><br><span class="line">        <span class="number">0x000e</span> <span class="number">00014</span> (test1.<span class="keyword">go</span>:<span class="number">15</span>)      PCDATA  $<span class="number">2</span>, $<span class="number">0</span></span><br><span class="line">        <span class="number">0x000e</span> <span class="number">00014</span> (test1.<span class="keyword">go</span>:<span class="number">15</span>)      PCDATA  $<span class="number">0</span>, $<span class="number">2</span></span><br><span class="line">        <span class="number">0x000e</span> <span class="number">00014</span> (test1.<span class="keyword">go</span>:<span class="number">15</span>)      MOVQ    AX, <span class="string">""</span>.~r1+<span class="number">16</span>(SP) <span class="comment">// 将 AX 的引用地址又复制到返回地址</span></span><br><span class="line">        <span class="number">0x0013</span> <span class="number">00019</span> (test1.<span class="keyword">go</span>:<span class="number">15</span>)      RET</span><br></pre></td></tr></table></figure><p>通过这里可以看到在 go 里边，只有值传递，因为它底层还是通过拷贝对应的值。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>今天的文章到此结束，本次主要讲了下面几个点：</p><ol><li>计算机软硬资源之间的相互配合；</li><li><code>Golang</code> 编写的代码，函数与方法是怎么执行的，主要讲了栈上分配与相关调用；</li><li>使用 <code>Plan9</code> 分析了一些常见的问题。</li></ol><p>希望本文对大家在理解、学习 Go 的路上有一些帮助。</p><p><strong>参考资料</strong></p><ul><li>[1]<a href="https://blog.learngoprogramming.com/a-visual-guide-to-golang-memory-allocator-from-ground-up-e132258453ed" target="_blank" rel="noopener">a visual guide to go memory allocator from scratch</a></li><li>[2]<a href="https://golang.org/doc/asm" target="_blank" rel="noopener">a quick guide to go’s assembler</a></li><li>[3]<a href="https://studygolang.com/articles/2917" target="_blank" rel="noopener">a quick guide to go’s assembler 中文版</a></li><li>[4]<a href="https://xargin.com/go-and-plan9-asm/" target="_blank" rel="noopener">go 和 plan9 汇编</a></li><li>[5]<a href="https://xargin.com/plan9-assembly/" target="_blank" rel="noopener">plan9 assembly 完全解析</a></li><li>[6]<a href="https://zh.wikipedia.org/wiki/%E5%AF%84%E5%AD%98%E5%99%A8" target="_blank" rel="noopener">寄存器 wiki</a></li><li>[7]<a href="https://segmentfault.com/a/1190000019753885" target="_blank" rel="noopener">go 函数调用 ━ 栈和寄存器视角</a></li></ul><h2 id="下回预告"><a href="#下回预告" class="headerlink" title="下回预告"></a>下回预告</h2><p>到目前为止，我们已经了解到 <code>Go</code> 代码是怎么生成机器码的；机器码又是如何在计算机中执行的，特别是函数对内存上栈区间的使用。接下来将会介绍 <code>Golang</code> 的内存分配策略是如何的。</p><hr><p><strong>作者</strong></p><p>何磊，小米信息技术部海外商城组</p><p><strong>招聘</strong></p><p>信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p>欢迎投递简历：jin.zhang(a)xiaomi.com(武汉) helei5(a)xiaomi.com(北京)</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;p&gt;通过上一篇&lt;a href=&quot;https://xiaomi-info.github.io/2019/11/13/golang-compiler-principle/&quot;&gt;走进 Golang 之汇编原理&lt;/a&gt;，我们知道了目标代码的生成经历了那些过程。今天我们一起来学习一下生成的
      
    
    </summary>
    
      <category term="Golang" scheme="https://xiaomi-info.github.io/categories/Golang/"/>
    
    
      <category term="Golang" scheme="https://xiaomi-info.github.io/tags/Golang/"/>
    
      <category term="汇编" scheme="https://xiaomi-info.github.io/tags/%E6%B1%87%E7%BC%96/"/>
    
  </entry>
  
  <entry>
    <title>走进Golang之编译器原理</title>
    <link href="https://xiaomi-info.github.io/2019/11/13/golang-compiler-principle/"/>
    <id>https://xiaomi-info.github.io/2019/11/13/golang-compiler-principle/</id>
    <published>2019-11-13T06:41:02.000Z</published>
    <updated>2021-05-24T03:39:30.252Z</updated>
    
    <content type="html"><![CDATA[<h2 id="认识-go-build"><a href="#认识-go-build" class="headerlink" title="认识 go build"></a>认识 go build</h2><p>当我们敲下 <code>go build</code> 的时候，我们的写的源码文件究竟经历了哪些事情？最终变成了可执行文件。</p><p>这个命令会编译 go 代码，今天就来一起看看 go 的编译过程吧！</p><p>首先先来认识以下 go 的代码源文件分类</p><ul><li>命令源码文件：简单说就是含有 main 函数的那个文件，通常一个项目一个该文件，我也没想过需要两个命令源文件的项目</li><li>测试源码文件：就是我们写的单元测试的代码，都是以 <code>_test.go</code> 结尾</li><li>库源码文件：没有上面特征的就是库源码文件，像我们使用的很多第三方包都属于这部分</li></ul><p><code>go build</code> 命令就是用来编译这其中的 <strong>命令源码文件</strong> 以及它依赖的 <strong>库源码文件</strong>。下面表格是一些常用的选项在这里集中说明以下。</p><table><thead><tr><th>可选项</th><th>说明</th></tr></thead><tbody><tr><td>-a</td><td>将命令源码文件与库源码文件全部重新构建，即使是最新的</td></tr><tr><td>-n</td><td>把编译期间涉及的命令全部打印出来，但不会真的执行，非常方便我们学习</td></tr><tr><td>-race</td><td>开启竞态条件的检测，支持的平台有限制</td></tr><tr><td>-x</td><td>打印编译期间用到的命名，它与 -n 的区别是，它不仅打印还会执行</td></tr></tbody></table><p>接下来就用一个 hello world 程序来演示以下上面的命令选项。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    a := <span class="number">1</span> + <span class="number">2</span></span><br><span class="line">    b := <span class="number">10</span></span><br><span class="line"></span><br><span class="line">    c := a * b</span><br><span class="line">    fmt.Println(c)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果对上面的代码执行 <code>go build -n</code> 我们看一下输出信息：</p><img src="/2019/11/13/golang-compiler-principle/go-byq-1.png"><p>来分析下整个执行过程</p><img src="/2019/11/13/golang-compiler-principle/go-byq-2.png"><p>这一部分是编译的核心，通过 <code>compile</code>、 <code>buildid</code>、 <code>link</code> 三个命令会编译出可执行文件 <code>a.out</code>。</p><p>然后通过 <code>mv</code> 命令把 a.out 移动到当前文件夹下面，并改成跟项目文件一样的名字（这里也可以自己指定名字）。</p><p>文章的后面部分，我们主要讲的就是 <code>compile</code>、 <code>buildid、</code> <code>link</code> 这三个命令涉及的编译过程。</p><h2 id="编译器原理"><a href="#编译器原理" class="headerlink" title="编译器原理"></a>编译器原理</h2><p>这是 go 编译器的<a href="https://github.com/golang/go/tree/master/src/cmd/compile" target="_blank" rel="noopener">源码路径</a></p><img src="/2019/11/13/golang-compiler-principle/go-byq-3.png"><p>如上图所见，整个编译器可以分为：编译前端与编译后端；现在我们看看每个阶段编译器都做了些什么事情。先来从前端部分开始。</p><h3 id="词法分析"><a href="#词法分析" class="headerlink" title="词法分析"></a>词法分析</h3><p>词法分析简单来说就是将我们写的源代码翻译成 <code>Token</code>，这是个什么意思呢？</p><p>为了理解 <code>Golang</code> 从源代码翻译到 <code>Token</code> 的过程，我们用一段代码来看一下翻译的一一对应情况。</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> <span class="string">"fmt"</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fmt.Println(<span class="string">"Hello Golang!"</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><img src="/2019/11/13/golang-compiler-principle/go-byq-4.png"><p>图中重要的地方我都进行了注释，不过这里还是有几句话多说一下，我们看着上面的代码想象以下，如果要我们自己来实现这个“翻译工作”，程序要如何识别 <code>Token</code> 呢？</p><p>首先先来给 Go 的 token 类型分个类：变量名、字面量、操作符、分隔符以及关键字。我们需要把一堆源代码按照规则进行拆分，其实就是分词，看着上面的例子代码我们可以大概制定一个规则如下：</p><ol><li>识别空格，如果是空格可以分一个词；</li><li>遇到 <code>(</code> 、<code>)</code>、’&lt;’、’&gt;’ 等这些特殊运算符的时候算一个分词；</li><li>遇到 “ 或者 数字字面量算分词。</li></ol><p>通过上面的简单分析，其实可以看出源代码转 <code>Token</code> 其实没有非常复杂，完全可以自己写代码实现出来。当然也有很多通过正则的方式实现的比较通用的词法分析器，像 <code>Golang</code> 早期就用的是 <code>lex</code>，在后面的版本中才改用了用 go 来自己实现。</p><h3 id="语法分析"><a href="#语法分析" class="headerlink" title="语法分析"></a>语法分析</h3><p>经过词法分析后，我们拿到的就是 <code>Token</code> 序列，它将作为语法分析器的输入。然后经过处理后生成 <code>AST</code> 结构作为输出。</p><p>所谓的语法分析就是将 <code>Token</code> 转化为可识别的程序语法结构，而 <code>AST</code> 就是这个语法的抽象表示。构造这颗树有两种方法。</p><ol><li><p>自上而下<br>这种方式会首先构造根节点，然后就开始扫描 <code>Token</code>，遇到 <code>STRING</code> 或者其它类型就知道这是在进行类型申明，<code>func</code> 就表示是函数申明。就这样一直扫描直到程序结束。</p></li><li><p>自下而上<br>这种是与上一种方式相反的，它先构造子树，然后再组装成一颗完整的树。</p></li></ol><p>go 语言进行语法分析使用的是自下而上的方式来构造 <code>AST</code>，下面我们就来看一下 go 语言通过 <code>Token</code> 构造的这颗树是什么样子。</p><img src="/2019/11/13/golang-compiler-principle/go-byq-5.png"><p>这其中有意思的地方我全部用文字标注出来了。你会发现其实每一个 <code>AST</code> 树的节点都与一个 <code>Token</code> 实际位置相对应。</p><p>这颗树构造后，我们可以看到不同的类型是由对应的结构体来进行表示的。这里如果有语法、词法错误是不会被解析出来的。因为到目前为止说白了都是进行的字符串处理。</p><h3 id="语义分析"><a href="#语义分析" class="headerlink" title="语义分析"></a>语义分析</h3><p>编译器里边都把语法分析后的阶段叫做 <strong>语义分析</strong>，而 go 的这个阶段叫 <strong>类型检查</strong>；但是我看了以下 go 自己的文档，其实做的事情没有太大差别，我们还是按照主流规范来写这个过程。</p><p>那么语义分析（类型检查）究竟要做些什么呢？</p><p><code>AST</code> 生成后，语义分析将使用它作为输入，并且的有一些相关的操作也会直接在这颗树上进行改写。</p><p>首先就是 <code>Golang</code> 文档中提到的会进行类型检查，还有类型推断，查看类型是否匹配，是否进行隐式转化（go 没有隐式转化）。如下面的文字所说：</p><blockquote><p>The AST is then type-checked. The first steps are name resolution and type inference, which determine which object belongs to which identifier, and what type each expression has. Type-checking includes certain extra checks, such as “declared and not used” as well as determining whether or not a function terminates.</p></blockquote><p>大意是：生成 AST 之后是类型检查（也就是我们这里说的语义分析），第一步是进行名称检查和类型推断，签定每个对象所属的标识符，以及每个表达式具有什么类型。类型检查也还有一些其它的检查要做，像“声明未使用”以及确定函数是否中止。</p><blockquote><p>Certain transformations are also done on the AST. Some nodes are refined based on type information, such as string additions being split from the arithmetic addition node type. Some other examples are dead code elimination, function call inlining, and escape analysis.</p></blockquote><p>这一段是说：AST 也会进行转换，有些节点根据类型信息进行精简，比如从算术加法节点类型中拆分出字符串加法。其它一些例子像 dead code 的消除，函数调用内联和逃逸分析。</p><p>上面两段文字来自 <a href="https://github.com/golang/go/tree/master/src/cmd/compile" target="_blank" rel="noopener">golang compile</a>。</p><p>这里多说一句，我们常常在 debug 代码的时候，需要禁止内联，其实就是操作的这个阶段。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"># 编译的时候禁止内联</span><br><span class="line">go build -gcflags &apos;-N -l&apos;</span><br><span class="line"></span><br><span class="line">-N 禁止编译优化</span><br><span class="line">-l 禁止内联,禁止内联也可以一定程度上减小可执行程序大小</span><br></pre></td></tr></table></figure><hr><p>经过语义分析之后，就可以说明我们的代码结构、语法都是没有问题的。所以编译器前端主要就是解析出编译器后端可以处理的正确的 AST 结构。</p><p>接下来我们看看编译器后端又有哪些事情要做。机器只能够理解二进制并运行，所以编译器后端的任务简单来说就是怎么把 AST 翻译成机器码。</p><h3 id="中间码生成"><a href="#中间码生成" class="headerlink" title="中间码生成"></a>中间码生成</h3><p>既然已经拿到 AST，机器运行需要的又是二进制。为什么不直接翻译成二进制呢？其实到目前为止从技术上来说已经完全没有问题了。</p><p>但是，我们有各种各样的操作系统，有不同的 CPU 类型，每一种的位数可能不同；寄存器能够使用的指令也不同，像是复杂指令集与精简指令集等；在进行各个平台的兼容之前，我们还需要替换一些底层函数，比如我们使用 make 来初始化 slice，此时会根据传入的类型替换为：<code>makeslice64</code> 或者 <code>makeslice</code>。当然还有像 painc、channel 等等函数的替换也会在中间码生成过程中进行替换。这一部分的替换操作可以在<a href="https://github.com/golang/go/blob/master/src/cmd/compile/internal/gc/builtin/runtime.go" target="_blank" rel="noopener">这里查看</a>。</p><p>中间码存在的另外一个价值是提升后端编译的重用，比如我们定义好了一套中间码应该是长什么样子，那么后端机器码生成就是相对固定的。每一种语言只需要完成自己的编译器前端工作即可。这也是大家可以看到现在开发一门新语言速度比较快的原因。编译是绝大部分都可以重复使用的。</p><p>而且为了接下来的优化工作，中间代码存在具有非凡的意义。因为有那么多的平台，如果有中间码我们可以把一些共性的优化都放到这里。</p><p>中间码也是有多种格式的，像 <code>Golang</code> 使用的就是 SSA 特性的中间码(IR)，这种形式的中间码，最重要的一个特性就是最在使用变量之前总是定义变量，并且每个变量只分配一次。</p><h3 id="代码优化"><a href="#代码优化" class="headerlink" title="代码优化"></a>代码优化</h3><p>在 go 的编译文档中，我并没找到独立的一步进行代码的优化。不过根据我们上面的分析，可以看到其实代码优化过程遍布编译器的每一个阶段。大家都会力所能及的做些事情。</p><p>通常我们除了用高效代码替换低效的之外，还有如下的一些处理：</p><ul><li>并行性，充分利用现在多核计算机的特性</li><li>流水线，cpu 有时候在处理 a 指令的时候，还能同时处理 b 指令</li><li>指令的选择，为了让 cpu 完成某些操作，需要使用指令，但是不同的指令效率有非常大的差别，这里会进行指令优化</li><li>利用寄存器与高速缓存，我们都知道 cpu 从寄存器取是最快的，从高速缓存取次之。这里会进行充分的利用</li></ul><h3 id="机器码生成"><a href="#机器码生成" class="headerlink" title="机器码生成"></a>机器码生成</h3><p>经过优化后的中间代码，首先会在这个阶段被转化为汇编代码（Plan9），而汇编语言仅仅是机器码的文本表示，机器还不能真的去执行它。所以这个阶段会调用汇编器，汇编器会根据我们在执行编译时设置的架构，调用对应代码来生成目标机器码。</p><p>这里比有意思的是，<code>Golang</code> 总说自己的汇编器是跨平台的。其实他也是写了多分代码来翻译最终的机器码。因为在入口的时候他会根据我们所设置的 <code>GOARCH=xxx</code> 参数来进行初始化处理，然后最终调用对应架构编写的特定方法来生成机器码。这种上层逻辑一致，底层逻辑不一致的处理方式非常通用，非常值得我们学习。我们简单来一下这个处理。</p><p>首先看入口函数 <code>cmd/compile/main.go:main()</code></p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> archInits = <span class="keyword">map</span>[<span class="keyword">string</span>]<span class="function"><span class="keyword">func</span><span class="params">(*gc.Arch)</span></span>&#123;</span><br><span class="line">    <span class="string">"386"</span>:      x86.Init,</span><br><span class="line">    <span class="string">"amd64"</span>:    amd64.Init,</span><br><span class="line">    <span class="string">"amd64p32"</span>: amd64.Init,</span><br><span class="line">    <span class="string">"arm"</span>:      arm.Init,</span><br><span class="line">    <span class="string">"arm64"</span>:    arm64.Init,</span><br><span class="line">    <span class="string">"mips"</span>:     mips.Init,</span><br><span class="line">    <span class="string">"mipsle"</span>:   mips.Init,</span><br><span class="line">    <span class="string">"mips64"</span>:   mips64.Init,</span><br><span class="line">    <span class="string">"mips64le"</span>: mips64.Init,</span><br><span class="line">    <span class="string">"ppc64"</span>:    ppc64.Init,</span><br><span class="line">    <span class="string">"ppc64le"</span>:  ppc64.Init,</span><br><span class="line">    <span class="string">"s390x"</span>:    s390x.Init,</span><br><span class="line">    <span class="string">"wasm"</span>:     wasm.Init,</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="comment">// 从上面的map根据参数选择对应架构的处理</span></span><br><span class="line">    archInit, ok := archInits[objabi.GOARCH]</span><br><span class="line">    <span class="keyword">if</span> !ok &#123;</span><br><span class="line">        ......</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 把对应cpu架构的对应传到内部去</span></span><br><span class="line">    gc.Main(archInit)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后在 <code>cmd/internal/obj/plist.go</code> 中调用对应架构的方法进行处理</p><figure class="highlight golang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Flushplist</span><span class="params">(ctxt *Link, plist *Plist, newprog ProgAlloc, myimportpath <span class="keyword">string</span>)</span></span> &#123;</span><br><span class="line">    ... ...</span><br><span class="line">    <span class="keyword">for</span> _, s := <span class="keyword">range</span> text &#123;</span><br><span class="line">        mkfwd(s)</span><br><span class="line">        linkpatch(ctxt, s, newprog)</span><br><span class="line">        <span class="comment">// 对应架构的方法进行自己的机器码翻译</span></span><br><span class="line">        ctxt.Arch.Preprocess(ctxt, s, newprog)</span><br><span class="line">        ctxt.Arch.Assemble(ctxt, s, newprog)</span><br><span class="line"></span><br><span class="line">        linkpcln(ctxt, s)</span><br><span class="line">        ctxt.populateDWARF(plist.Curfn, s, myimportpath)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>整个过程下来，可以看到编译器后端有很多工作需要做的，你需要对某一个指令集、cpu 的架构了解，才能正确的进行翻译机器码。同时不能仅仅是正确，一个语言的效率是高还是低，也在很大程度上取决于编译器后端的优化。特别是即将进入 AI 时代，越来越多的芯片厂商诞生，我估计以后对这方面人才的需求会变得越来越旺盛。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>总结一下学习编译器这部分古老知识带给我的几个收获：</p><ol><li>知道整个编译由几个阶段构成，每个阶段做什么事情；但是更深入的每个阶段实现的一些细节还不知道，也不打算知道；</li><li>就算是编译器这种复杂，很底层的东西也是可以通过分解，让每一个阶段独立变得简单、可复用，这对我在做应用开发有一些意义；</li><li>分层是为了划分指责，但是某些事情还需要全局的去做，比如优化，其实每一个阶段都会去做；对于我们设计系统也是有一定参考意义的；</li><li>了解到 <code>Golang</code> 对外暴露的很多方法其实是语法糖（如：make、painc etc.），编译器会帮我忙进行翻译，最开始我以为是 go 代码层面在运行时去做的，类似工厂模式，现在回头来看自己真是太天真了；</li><li>对接下来准备学习 Go 的运行机制、以及 Plan9 汇编进行了一些基础准备。</li></ol><p>本文的很多信息都来自下面的资料。</p><ul><li>[1]<a href="https://github.com/golang/go/tree/master/src/cmd/compile" target="_blank" rel="noopener">golang compile</a></li><li>[2]<a href="https://github.com/golang/go/tree/master/src/cmd/compile/internal/ssa" target="_blank" rel="noopener">golang ssa</a></li><li>[3]<a href="https://halfrost.com/go_command/" target="_blank" rel="noopener">golang command</a></li><li>[4]<a href="https://draveness.me/golang/compile/golang-compile-intro.html" target="_blank" rel="noopener">golang compile 介绍</a></li><li>[5]<a href="https://segmentfault.com/a/1190000016523685" target="_blank" rel="noopener">golang 编译流程分析</a></li></ul><h2 id="下回预告"><a href="#下回预告" class="headerlink" title="下回预告"></a>下回预告</h2><p>本文介绍了整个编译流程，下一篇会介绍 Golang 的汇编是如何在内存、CPU、操作系统的联合下运行起来的，以及通过例子介绍如何阅读 Plan9 汇编。</p><hr><p><strong>作者</strong></p><p>何磊，小米信息技术部海外商城组</p><p><strong>招聘</strong></p><p>小米信息部武汉研发中心，信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p><strong>欢迎投递简历：jin.zhang(a)xiaomi.com</strong></p><p>更多技术文章：<a href="https://xiaomi-info.github.io/">小米信息部技术团队</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;认识-go-build&quot;&gt;&lt;a href=&quot;#认识-go-build&quot; class=&quot;headerlink&quot; title=&quot;认识 go build&quot;&gt;&lt;/a&gt;认识 go build&lt;/h2&gt;&lt;p&gt;当我们敲下 &lt;code&gt;go build&lt;/code&gt; 的时候，我们
      
    
    </summary>
    
    
      <category term="Golang" scheme="https://xiaomi-info.github.io/tags/Golang/"/>
    
      <category term="编译器" scheme="https://xiaomi-info.github.io/tags/%E7%BC%96%E8%AF%91%E5%99%A8/"/>
    
  </entry>
  
  <entry>
    <title>设计模式基础之——面向对象的设计过程</title>
    <link href="https://xiaomi-info.github.io/2019/10/11/oo-design/"/>
    <id>https://xiaomi-info.github.io/2019/10/11/oo-design/</id>
    <published>2019-10-11T12:44:39.000Z</published>
    <updated>2021-05-24T03:39:30.269Z</updated>
    
    <content type="html"><![CDATA[<hr><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>我一直认为分享的目的不是炫技。</p><ul><li>一是，自我学习的总结。</li><li>二是，降低他人的学习成本。</li><li>三是，别人对自己学习结果的审核。</li></ul><p>同时，本次分享有下面四个要素：</p><table><thead><tr><th>观点</th><th>本次分享的观点是一个软件工程中的思维方法，不限于编程语言</th></tr></thead><tbody><tr><td><strong>探讨</strong></td><td><strong>我可能理解错的，或者大家没理解的，欢迎大家可以积极留言，尽可能多互动，目的增加理解</strong></td></tr><tr><td><strong>理解</strong></td><td><strong>真的希望大家能理解</strong></td></tr><tr><td><strong>运用</strong></td><td><strong>最重要的，如果你觉着有帮助，一定要去在实际业务中实战</strong></td></tr></tbody></table><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>工作中，几乎大家经常抱怨别人写的代码：</p><ul><li>没法改</li><li>耦合高</li><li>无法扩展</li></ul><blockquote><p>今天就来探讨如何<strong>克服</strong>上面的问题～</p></blockquote><h2 id="场景"><a href="#场景" class="headerlink" title="场景"></a>场景</h2><p>首先问个问题：</p><blockquote><p>平常工作中来了一个业务需求，我们是如何开始写代码的？</p></blockquote><p>我推测大多数人可能：</p><ul><li>1、梳理业务</li><li>2、设计数据库、接口、缓存</li><li>3、评审</li><li>4、于是就开始了 <code>怎么怎么样...如果怎么怎么样...怎么怎么样...</code>愉快的码代码的过程</li></ul><blockquote><p>此处有人觉着有啥问题么？</p></blockquote><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">备注：说出来问题的，本次分享就可以略过了~</span><br></pre></td></tr></table></figure><h3 id="一个简单的业务场景"><a href="#一个简单的业务场景" class="headerlink" title="一个简单的业务场景"></a>一个简单的业务场景</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">比如产品提了个需求：</span><br><span class="line">描述“我一个同事”一天的生活，简单来看看他一天干些啥。</span><br><span class="line"></span><br><span class="line">1.0 饿了吃饭</span><br><span class="line">1.1 到点吃饭</span><br><span class="line"></span><br><span class="line">2.0 渴了喝水</span><br><span class="line">2.1 到点喝水</span><br><span class="line"></span><br><span class="line">3.0 困了睡觉</span><br><span class="line">3.1 到点睡觉</span><br><span class="line">3.2 有可能一个人睡觉，也有可能... 是吧？复杂</span><br></pre></td></tr></table></figure><p>刚开始，一个业务逻辑从头写到尾</p><img src="/2019/10/11/oo-design/20191020234013.png"><p>进阶，一个业务逻辑(拆成多个函数)从头写到尾：<br><img src="/2019/10/11/oo-design/20191020234051.png"><br>再进阶，一个业务逻辑(引入类)从头写到尾：<br><img src="/2019/10/11/oo-design/20191020234118.png"><br>再进阶，一个业务逻辑(拆成多个类方法)从头写到尾，也许、可能、貌似、猜测大多数人停留到了这个阶段。</p><blockquote><p>问题：某一天多了社交的能力，咋办？</p></blockquote><img src="/2019/10/11/oo-design/20191020234526.png"><p>再进阶，一个业务逻辑(拆成多类)从头写到尾：<br><img src="/2019/10/11/oo-design/20191020234848.png"><br>最终，一个业务逻辑(拆成类、抽象类、接口)从头写到尾：<br><img src="/2019/10/11/oo-design/20191020235015.png"></p><blockquote><p>思考 🤔：上面的代码就没啥问题了吗？</p></blockquote><p>上面就是面向对象设计的代码结果。</p><blockquote><p>所以，如何设计出完全面向对象的代码？</p></blockquote><h2 id="代码建模"><a href="#代码建模" class="headerlink" title="代码建模"></a>代码建模</h2><blockquote><p>什么是代码建模？</p></blockquote><p>把业务抽象成事物(类 class、抽象类 abstact class)和行为(接口 interface)的过程。</p><h3 id="实栗-🌰-分析"><a href="#实栗-🌰-分析" class="headerlink" title="实栗 🌰 分析"></a>实栗 🌰 分析</h3><p>又来看一个实际的业务场景：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">最近“我一个同事”开始创业了，刚创立了一家电商公司，B2C，自营书籍《3分钟学会交际》。最近开始写提交订单的代码。</span><br><span class="line"></span><br><span class="line">⚠️注意场景 1.刚创业 2.简单的单体应用 3.此处不探讨架构</span><br></pre></td></tr></table></figure><p>一般来说，我们根据业务需求一顿分析，开始定义接口 API、设计数据库、缓存、技术评审等就开始码代码了。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">接口参数：</span><br><span class="line">uid</span><br><span class="line">address_id</span><br><span class="line">coupon_id</span><br><span class="line">.etc</span><br><span class="line"></span><br><span class="line">业务逻辑：</span><br><span class="line">参数校验-&gt;</span><br><span class="line">地址校验-&gt;</span><br><span class="line">其他校验-&gt;</span><br><span class="line">写订单表-&gt;</span><br><span class="line">写订单商品信息表-&gt;</span><br><span class="line">写日志-&gt;</span><br><span class="line">扣减商品库存-&gt;</span><br><span class="line">清理购物车-&gt;</span><br><span class="line">扣减各种促销优惠活动的库存-&gt;</span><br><span class="line">使用优惠券-&gt;</span><br><span class="line">其他营销逻辑等等-&gt;</span><br><span class="line">发送消息-&gt;</span><br><span class="line">等等...</span><br></pre></td></tr></table></figure><p>就开始写代码了<code>怎么怎么样...如果怎么怎么样...怎么怎么样...</code>一蹴而就、思路清晰、逻辑清楚、很快搞定完代码，很优秀是不是，值得鼓励。</p><p>但是，上面的结果就是大概所有人都见过的连续上千行的代码等等。上面的流程没啥问题吗，那正确的做法是什么了？就是接着要说的<strong>代码建模</strong>。</p><p>我们根据上面的场景，开始建模。</p><h3 id="业务分析少不了"><a href="#业务分析少不了" class="headerlink" title="业务分析少不了"></a>业务分析少不了</h3><p>同样，首先，我们看看<code>提交订单</code>这个业务场景要做的事情:</p><blockquote><p>换个角度看业务其实很简单：根据用户相关信息生成一个订单。</p></blockquote><ol><li>梳理得到业务逻辑</li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">参数校验-&gt;</span><br><span class="line">地址校验-&gt;</span><br><span class="line">其他校验-&gt;</span><br><span class="line">写订单表-&gt;</span><br><span class="line">写订单商品信息表-&gt;</span><br><span class="line">写日志-&gt;</span><br><span class="line">扣减商品库存-&gt;</span><br><span class="line">清理购物车-&gt;</span><br><span class="line">扣减各种促销优惠活动的库存-&gt;</span><br><span class="line">使用优惠券-&gt;</span><br><span class="line">其他营销逻辑等等-&gt;</span><br><span class="line">发送消息-&gt;</span><br><span class="line">等等...</span><br></pre></td></tr></table></figure><ol start="2"><li>梳理业务逻辑依赖信息</li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">用户信息</span><br><span class="line">商品信息</span><br><span class="line">地址信息</span><br><span class="line">优惠券信息</span><br><span class="line">等等...</span><br></pre></td></tr></table></figure><p>再次回归概念</p><blockquote><p>什么是代码建模？把业务抽象成事物(类 class、抽象类 abstact class)和行为(接口 interface)的过程。</p></blockquote><h3 id="获取事物"><a href="#获取事物" class="headerlink" title="获取事物"></a>获取事物</h3><p>比如我们把订单生成的过程可以想象成<code>机器人</code>，一个生成订单的<code>订单生成机器人</code>，或者订单生成机器啥的，这样我们就得到了<code>代码建模</code>过程中的一个事物。</p><p>从而我们就可以把这个事物转化成一个类(或结构体)，或者抽象类。</p><img src="/2019/10/11/oo-design/20191020223812.jpg"><h3 id="获取行为"><a href="#获取行为" class="headerlink" title="获取行为"></a>获取行为</h3><p>这些操作就是上面机器人要做的事情。</p><p>事物有了：<code>订单生成机器人</code><br>行为呢？毫无疑问就是上面各种业务逻辑。把具体的行为抽象成一个订单创建行为接口：</p><img src="/2019/10/11/oo-design/20191020224230.jpg"><h3 id="得到-UML"><a href="#得到-UML" class="headerlink" title="得到 UML"></a>得到 UML</h3><img src="/2019/10/11/oo-design/20191031195921.jpg"><h3 id="设计代码"><a href="#设计代码" class="headerlink" title="设计代码"></a>设计代码</h3><ol><li>定义一个类</li></ol><img src="/2019/10/11/oo-design/20191020235309.png"><ol start="2"><li>定义一个订单创建行为的接口</li></ol><img src="/2019/10/11/oo-design/20191020235643.png"><ol start="3"><li>定义具体的不同订单创建行为类</li></ol><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">参数校验-&gt;</span><br><span class="line">地址校验-&gt;</span><br><span class="line">其他校验-&gt;</span><br><span class="line">写订单表-&gt;</span><br><span class="line">写订单商品信息表-&gt;</span><br><span class="line">写日志-&gt;</span><br><span class="line">扣减商品库存-&gt;</span><br><span class="line">清理购物车-&gt;</span><br><span class="line">扣减各种促销优惠活动的库存-&gt;</span><br><span class="line">使用优惠券-&gt;</span><br><span class="line">其他营销逻辑等等-&gt;</span><br><span class="line">发送消息-&gt;</span><br><span class="line">等等...</span><br></pre></td></tr></table></figure><img src="/2019/10/11/oo-design/20191020235840.png"><ol start="4"><li>创建订单</li></ol><p>这里的代码该怎么写，这样？</p><img src="/2019/10/11/oo-design/20191021000742.png"><p>还可以继续优化吗？<br><img src="/2019/10/11/oo-design/20191021001002.png"><br>使用闭包。<br><img src="/2019/10/11/oo-design/20191021001305.png"></p><h3 id="PHP-版完整代码"><a href="#PHP-版完整代码" class="headerlink" title="PHP 版完整代码"></a>PHP 版完整代码</h3><img src="/2019/10/11/oo-design/20191024143840.png"><blockquote><p>上面的代码有什么好处？</p></blockquote><p>假如“我一个同事”又要新开发一个新的应用，新的应用创建订单的时候又有新的逻辑，比如没有优惠逻辑、新增了增加用户积分的逻辑等等，复用上面的代码，是不是就很简单了。</p><img src="/2019/10/11/oo-design/20191021001739.png"><blockquote><p>所以现在，什么是面向对象？</p></blockquote><h2 id="概念"><a href="#概念" class="headerlink" title="概念"></a>概念</h2><h3 id="面向对象的设计原则"><a href="#面向对象的设计原则" class="headerlink" title="面向对象的设计原则"></a>面向对象的设计原则</h3><ul><li>对接口编程而不是对实现编程</li><li>优先使用对象组合而不是继承</li><li>抽象用于不同的事物，而接口用于事物的行为</li></ul><p>针对上面的概念，我们再回头开我们上面的代码</p><blockquote><p>对接口编程而不是对实现编程</p></blockquote><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">结果：RobotOrderCreate依赖了BehaviorOrderCreateInterface抽象接口</span><br></pre></td></tr></table></figure><blockquote><p>优先使用对象组合而不是继承</p></blockquote><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">结果：完全没有使用继承，多个行为不同场景组合使用</span><br></pre></td></tr></table></figure><blockquote><p>抽象用于不同的事物，而接口用于事物的行为</p></blockquote><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">结果：</span><br><span class="line">1. 抽象了一个创建订单的机器人 RobotOrderCreate</span><br><span class="line">2. 机器人又有不同的创建行为</span><br><span class="line">3. 机器人的创建行为最终依赖于BehaviorOrderCreateInterface接口</span><br></pre></td></tr></table></figure><p>是不是完美契合，所以这就是“面向对象的设计过程”。</p><h2 id="结论"><a href="#结论" class="headerlink" title="结论"></a>结论</h2><p><code>代码建模过程就是“面向对象的设计过程”的具体实现方式.</code></p><h2 id="预习"><a href="#预习" class="headerlink" title="预习"></a>预习</h2><h3 id="设计模式"><a href="#设计模式" class="headerlink" title="设计模式"></a>设计模式</h3><blockquote><p>最后，设计模式又是什么？</p></blockquote><p>同样，我们下结合上面的场景和概念预习下设计模式。</p><h4 id="设计模式的设计原则"><a href="#设计模式的设计原则" class="headerlink" title="设计模式的设计原则"></a>设计模式的设计原则</h4><blockquote><p>开闭原则（Open Close Principle）：对扩展开放，对修改封闭</p></blockquote><p>看看上面的最终的代码是不是完美契合。</p><img src="/2019/10/11/oo-design/20191022131439.png"><blockquote><p>依赖倒转原则：对接口编程，依赖于抽象而不依赖于具体</p></blockquote><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">结果：创建订单的逻辑从依赖具体的业务转变为依赖于抽象接口BehaviorOrderCreateInterface</span><br></pre></td></tr></table></figure><blockquote><p>接口隔离原则：使用多个接口，而不是对一个接口编程，去依赖降低耦合</p></blockquote><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">结果：上面的场景，我们只简单定义了订单创建的接BehaviorOrderCreateInterface。由于订单创建过程可能出现异常回滚，我们就需要再定义一个订单创建回滚的接口</span><br><span class="line">BehaviorOrderCreateRollBackInterface.</span><br></pre></td></tr></table></figure><blockquote><p>迪米特法则，又称最少知道原则：减少内部依赖，尽可能的独立</p></blockquote><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">结果：还是上面那段代码，我们把RobotOrderCreate机器人依赖的行为通过外部注入的方式使用。</span><br></pre></td></tr></table></figure><blockquote><p>合成复用原则：多个独立的实体合成聚合，而不是使用继承</p></blockquote><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">结果：RobotOrderCreate依赖了多个实际的订单创建行为类。</span><br></pre></td></tr></table></figure><blockquote><p>里氏代换：超类（父类）出现的地方，派生类（子类）都可以出现</p></blockquote><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">结果：不好意思，我们完全没用继承。（备注：继承容易造成父类膨胀。）</span><br></pre></td></tr></table></figure><h2 id="下回预告"><a href="#下回预告" class="headerlink" title="下回预告"></a>下回预告</h2><p>上面预习了设计模式的概念，下回我将给大家分享《设计模式基础之——设计模式业务实战》。</p><hr><p><strong>作者</strong></p><p>施展，小米信息技术部海外商城组</p><p><strong>招聘</strong></p><p>小米信息部武汉研发中心，信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP 体系、内网 OA 体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p><strong>欢迎投递简历：jin.zhang(a)xiaomi.com</strong></p><p>更多技术文章：<a href="https://xiaomi-info.github.io/">小米信息部技术团队</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;hr&gt;
&lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;我一直认为分享的目的不是炫技。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一是，自我学习的总结。&lt;/li&gt;
&lt;li&gt;二是，降低他人的学习成本。&lt;/
      
    
    </summary>
    
    
      <category term="设计模式" scheme="https://xiaomi-info.github.io/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
    
  </entry>
  
  <entry>
    <title>当我们在谈论高并发的时候究竟在谈什么?</title>
    <link href="https://xiaomi-info.github.io/2019/06/21/concurrency/"/>
    <id>https://xiaomi-info.github.io/2019/06/21/concurrency/</id>
    <published>2019-06-21T08:56:43.000Z</published>
    <updated>2021-05-24T03:39:30.207Z</updated>
    
    <content type="html"><![CDATA[<h3 id="什么是高并发"><a href="#什么是高并发" class="headerlink" title="什么是高并发?"></a>什么是高并发?</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">高并发是互联网分布式系统架构的性能指标之一,它通常是指单位时间内系统能够同时处理的请求数,</span><br><span class="line">简单点说，就是QPS(Queries per second)。</span><br></pre></td></tr></table></figure><p>那么我们在谈论高并发的时候，究竟在谈些什么东西呢？</p><h3 id="高并发究竟是什么"><a href="#高并发究竟是什么" class="headerlink" title="高并发究竟是什么?"></a>高并发究竟是什么?</h3><p>这里先给出结论:<br><code>高并发</code>的基本表现为单位时间内系统能够同时处理的请求数,<br><code>高并发</code>的核心是对CPU资源的<strong>有效压榨</strong>。</p><p>举个例子，如果我们开发了一个叫做<code>MD5穷举</code>的应用，每个请求都会携带一个md5加密字符串，最终系统穷举出所有的结果，并返回原始字符串。这个时候我们的应用场景或者说应用业务是属于<code>CPU密集型</code>而不是<code>IO密集型</code>。这个时候CPU一直在做有效计算，甚至可以把CPU利用率跑满，这时我们谈论高并发并没有任何意义。(当然，我们可以通过加机器也就是加CPU来提高并发能力,这个是一个正常猿都知道废话方案，谈论加机器没有什么意义，没有任何高并发是加机器解决不了，如果有,那说明你加的机器还不够多!🐶)</p><p><strong>对于大多数互联网应用来说,CPU不是也不应该是系统的瓶颈，系统的大部分时间的状况都是CPU在等I/O (硬盘/内存/网络) 的读/写操作完成。</strong></p><p>这个时候就可能有人会说，我看系统监控的时候，内存和网络都很正常，但是CPU利用率却跑满了这是为什么？<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">这是一个好问题,后文我会给出实际的例子，再次强调上文说的 &apos;有效压榨&apos; 这4个字,这4个字会围绕本文的全部内容！</span><br></pre></td></tr></table></figure></p><h3 id="控制变量法"><a href="#控制变量法" class="headerlink" title="控制变量法"></a>控制变量法</h3><p>万事万物都是互相联系的，当我们在谈论高并发的时候，系统的每个环节应该都是需要与之相匹配的。我们先来回顾一下一个经典C/S的HTTP请求流程。</p><img src="/2019/06/21/concurrency/bVbto9k.jpg"><p>如图中的序号所示:</p><ol><li>我们会经过DNS服务器的解析，请求到达负载均衡集群</li><li>负载均衡服务器会根据配置的规则，想请求分摊到服务层。服务层也是我们的业务核心层，这里可能也会有一些PRC、MQ的一些调用等等</li><li>再经过缓存层</li><li>最后持久化数据</li><li>返回数据给客户端</li></ol><p>要达到高并发，我们需要 负载均衡、服务层、缓存层、持久层 都是高可用、高性能的，甚至在第5步，我们也可以通过 压缩静态文件、HTTP2推送静态文件、CDN来做优化，这里的每一层我们都可以写几本书来谈优化。</p><p>本文主要讨论服务层这一块，即图红线圈出来的那部分。不再考虑讲述数据库、缓存相关的影响。<br>高中的知识告诉我们，这个叫 <code>控制变量法</code>。</p><h3 id="再谈并发"><a href="#再谈并发" class="headerlink" title="再谈并发"></a>再谈并发</h3><ul><li>网络编程模型的演变历史</li></ul><img src="/2019/06/21/concurrency/bVbtqB5.jpg"><p>并发问题一直是服务端编程中的重点和难点问题，为了优系统的并发量，从最初的Fork进程开始，到进程池/线程池,再到epoll事件驱动(Nginx、node.js反人类回调),再到协程。<br>从上中可以很明显的看出，整个演变的过程，就是对CPU有效性能压榨的过程。<br>什么?不明显?</p><ul><li>那我们再谈谈上下文切换</li></ul><p>在谈论上下文切换之前，我们再明确两个名词的概念。<br><strong>并行：两个事件同一时刻完成。</strong><br><strong>并发：两个事件在同一时间段内交替发生,从宏观上看，两个事件都发生了</strong>。</p><p>线程是操作系统调度的最小单位，进程是资源分配的最小单位。由于CPU是串行的,因此对于单核CPU来说,同一时刻一定是只有一个线程在占用CPU资源的。因此，Linux作为一个多任务(进程)系统，会频繁的发生进程/线程切换。</p><p>在每个任务运行前，CPU都需要知道从哪里加载，从哪里运行，这些信息保存在<code>CPU寄存器</code>和操作系统的<code>程序计数器</code>里面，这两样东西就叫做 <code>CPU上下文</code>。<br>进程是由内核来管理和调度的，进程的切换只能发生在内核态，因此 虚拟内存、栈、全局变量等用户空间的资源，以及内核堆栈、寄存器等内核空间的状态,就叫做 <code>进程上下文</code>。<br>前面说过,线程是操作系统调度的最小单位。同时线程会共享父进程的虚拟内存和全局变量等资源，因此 父进程的资源加上线上自己的私有数据就叫做<code>线程的上下文</code>。</p><p>对于线程的上下文切换来说，如果是同一进程的线程，因为有资源共享，所以会比多进程间的切换消耗更少的资源。</p><p>现在就更容易解释了，进程和线程的切换，会产生<code>CPU上下文</code>切换和<code>进程/线程上下文</code>的切换。而这些<code>上下文切换</code>,都是会消耗额外的CPU的资源的。</p><ul><li>进一步谈谈协程的上下文切换<br>那么协程就不需要上下文切换了吗？需要，但是<strong>不会产生</strong> <code>CPU上下文切换</code>和<code>进程/线程上下文</code>的切换,因为这些切换都是在同一个线程中，即用户态中的切换，<strong>你甚至可以简单的理解为</strong>，<code>协程上下文</code>之间的切换，就是移动了一下你程序里面的指针，CPU资源依旧属于当前线程。<br>需要深刻理解的，可以再深入看看Go的<code>GMP模型</code>。<br>最终的效果就是协程<strong>进一步压榨了CPU的有效利用率</strong>。</li></ul><h3 id="回到开始的那个问题"><a href="#回到开始的那个问题" class="headerlink" title="回到开始的那个问题"></a>回到开始的那个问题</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">这个时候就可能有人会说，我看系统监控的时候，内存和网络都很正常，但是CPU利用率却跑满了这是为什么？</span><br></pre></td></tr></table></figure><p>注意本篇文章在谈到CPU利用率的时候，一定会加上<code>有效</code>两字作为定语，CPU利用率跑满，很多时候其实是做了很多低效的计算。<br>以『世界上最好的语言』为例，典型PHP-FPM的CGI模式，每一个HTTP请求:</p><ul><li>都会读取框架的数百个php文件，</li><li>都会重新建立/释放一遍MYSQL/REIDS/MQ连接，</li><li>都会重新动态解释编译执行PHP文件，</li><li>都会在不同的php-fpm进程直接不停的切换切换再切换。</li></ul><p>php的这种<strong>CGI运行模式</strong>，根本上就决定了它在高并发上的<strong>灾难性表现</strong>。</p><p>找到问题，往往比解决问题更难。当我们理解了<code>当我们在谈论高并发究竟在谈什么</code> 之后,我们会发现高并发和高性能并不是编程语言限制了你，限制你的只是你的思想。</p><p>找到问题,解决问题！当我们能有效压榨CPU性能之后,能达到什么样的效果?</p><p>下面我们看看 php+swoole的HTTP服务 与 Java高性能的异步框架netty的HTTP服务之间的性能差异对比。</p><h3 id="性能对比前的准备"><a href="#性能对比前的准备" class="headerlink" title="性能对比前的准备"></a>性能对比前的准备</h3><ul><li>[swoole是什么][1]</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Swoole是一个为PHP用C和C++编写的基于事件的高性能异步&amp;协程并行网络通信引擎</span><br></pre></td></tr></table></figure><ul><li>[Netty是什么][2]</li></ul><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Netty是由JBOSS提供的一个java开源框架。 Netty提供异步的、事件驱动的网络应用程序框架和工具，用以快速开发高性能、高可靠性的网络服务器和客户端程序。</span><br></pre></td></tr></table></figure><ul><li>单机能够达到的最大HTTP连接数是多少？<br>回忆一下计算机网络的相关知识，HTTP协议是应用层协议，在传输层，每个TCP连接建立之前都会进行三次握手。<br>每个TCP连接由 <code>本地ip</code>,<code>本地端口</code>,<code>远端ip</code>,<code>远端端口</code>,四个属性标识。<br>TCP协议报文头如下(图片来自[维基百科][3])：</li></ul><img src="/2019/06/21/concurrency/bVbtosl.jpg"><p>本地端口由16位组成,因此本地端口的最多数量为 2^16 = 65535个。<br>远端端口由16位组成,因此远端端口的最多数量为 2^16 = 65535个。<br>同时，在linux底层的网络编程模型中，每个TCP连接，操作系统都会维护一个File descriptor(fd)文件来与之对应，而fd的数量限制，可以由ulimt -n 命令查看和修改，测试之前我们可以执行命令: ulimit -n 65536修改这个限制为65535。</p><p>因此，在不考虑硬件资源限制的情况下，<br>本地的最大HTTP连接数为： 本地最大端口数65535 <em> 本地ip数1 = 65535 个。<br>远端的最大HTTP连接数为：远端最大端口数65535 </em> 远端(客户端)ip数+∞ = 无限制~~ 。<br>PS: 实际上操作系统会有一些保留端口占用,因此本地的连接数实际也是达不到理论值的。</p><h3 id="性能对比"><a href="#性能对比" class="headerlink" title="性能对比"></a>性能对比</h3><ul><li>测试资源</li></ul><p>各一台docker容器,1G内存+2核CPU,如图所示:</p><img src="/2019/06/21/concurrency/bVbtn3v.jpg"><p>docker-compose编排如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"># java8</span><br><span class="line">version: &quot;2.2&quot;</span><br><span class="line">services:</span><br><span class="line">  java8:</span><br><span class="line">    container_name: &quot;java8&quot;</span><br><span class="line">    hostname: &quot;java8&quot;</span><br><span class="line">    image: &quot;java:8&quot;</span><br><span class="line">    volumes:</span><br><span class="line">      - /home/cg/MyApp:/MyApp</span><br><span class="line">    ports:</span><br><span class="line">      - &quot;5555:8080&quot;</span><br><span class="line">    environment:</span><br><span class="line">      - TZ=Asia/Shanghai</span><br><span class="line">    working_dir: /MyApp</span><br><span class="line">    cpus: 2</span><br><span class="line">    cpuset: 0,1</span><br><span class="line"></span><br><span class="line">    mem_limit: 1024m</span><br><span class="line">    memswap_limit: 1024m</span><br><span class="line">    mem_reservation: 1024m</span><br><span class="line">    tty: true</span><br><span class="line"></span><br><span class="line"># php7-sw</span><br><span class="line">version: &quot;2.2&quot;</span><br><span class="line">services:</span><br><span class="line">  php7-sw:</span><br><span class="line">    container_name: &quot;php7-sw&quot;</span><br><span class="line">    hostname: &quot;php7-sw&quot;</span><br><span class="line">    image: &quot;mileschou/swoole:7.1&quot;</span><br><span class="line">    volumes:</span><br><span class="line">      - /home/cg/MyApp:/MyApp</span><br><span class="line">    ports:</span><br><span class="line">      - &quot;5551:8080&quot;</span><br><span class="line">    environment:</span><br><span class="line">      - TZ=Asia/Shanghai</span><br><span class="line">    working_dir: /MyApp</span><br><span class="line">    cpus: 2</span><br><span class="line">    cpuset: 0,1</span><br><span class="line"></span><br><span class="line">    mem_limit: 1024m</span><br><span class="line">    memswap_limit: 1024m</span><br><span class="line">    mem_reservation: 1024m</span><br><span class="line">    tty: true</span><br></pre></td></tr></table></figure><ul><li>php代码</li></ul><figure class="highlight php"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Swoole</span>\<span class="title">Server</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Swoole</span>\<span class="title">Http</span>\<span class="title">Response</span>;</span><br><span class="line"></span><br><span class="line">$http = <span class="keyword">new</span> swoole_http_server(<span class="string">"0.0.0.0"</span>, <span class="number">8080</span>);</span><br><span class="line">$http-&gt;set([</span><br><span class="line"><span class="string">'worker_num'</span> =&gt; <span class="number">2</span></span><br><span class="line">]);</span><br><span class="line">$http-&gt;on(<span class="string">"request"</span>, <span class="function"><span class="keyword">function</span> <span class="params">($request, Response $response)</span> </span>&#123;</span><br><span class="line"><span class="comment">//go(function () use ($response) &#123;</span></span><br><span class="line">        <span class="comment">// Swoole\Coroutine::sleep(0.01);</span></span><br><span class="line">$response-&gt;end(<span class="string">'Hello World'</span>);</span><br><span class="line"><span class="comment">//&#125;);</span></span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line">$http-&gt;on(<span class="string">"start"</span>, <span class="function"><span class="keyword">function</span> <span class="params">(Server $server)</span> </span>&#123;</span><br><span class="line">go(<span class="function"><span class="keyword">function</span> <span class="params">()</span> <span class="title">use</span> <span class="params">($server)</span> </span>&#123;</span><br><span class="line"><span class="keyword">echo</span> <span class="string">"server listen on 0.0.0.0:8080 \n"</span>;</span><br><span class="line">&#125;);</span><br><span class="line">&#125;);</span><br><span class="line">$http-&gt;start();</span><br></pre></td></tr></table></figure><ul><li>Java关键代码<br>源代码来自, [<a href="https://github.com/netty/netty][4]" target="_blank" rel="noopener">https://github.com/netty/netty][4]</a></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception </span>&#123;</span><br><span class="line">    <span class="comment">// Configure SSL.</span></span><br><span class="line">    <span class="keyword">final</span> SslContext sslCtx;</span><br><span class="line">    <span class="keyword">if</span> (SSL) &#123;</span><br><span class="line">        SelfSignedCertificate ssc = <span class="keyword">new</span> SelfSignedCertificate();</span><br><span class="line">        sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        sslCtx = <span class="keyword">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Configure the server.</span></span><br><span class="line">    EventLoopGroup bossGroup = <span class="keyword">new</span> NioEventLoopGroup(<span class="number">2</span>);</span><br><span class="line">    EventLoopGroup workerGroup = <span class="keyword">new</span> NioEventLoopGroup();</span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        ServerBootstrap b = <span class="keyword">new</span> ServerBootstrap();</span><br><span class="line">        b.option(ChannelOption.SO_BACKLOG, <span class="number">1024</span>);</span><br><span class="line">        b.group(bossGroup, workerGroup)</span><br><span class="line">         .channel(NioServerSocketChannel.class)</span><br><span class="line">         .handler(<span class="keyword">new</span> LoggingHandler(LogLevel.INFO))</span><br><span class="line">         .childHandler(<span class="keyword">new</span> HttpHelloWorldServerInitializer(sslCtx));</span><br><span class="line"></span><br><span class="line">        Channel ch = b.bind(PORT).sync().channel();</span><br><span class="line"></span><br><span class="line">        System.err.println(<span class="string">"Open your web browser and navigate to "</span> +</span><br><span class="line">                (SSL? <span class="string">"https"</span> : <span class="string">"http"</span>) + <span class="string">"://127.0.0.1:"</span> + PORT + <span class="string">'/'</span>);</span><br><span class="line"></span><br><span class="line">        ch.closeFuture().sync();</span><br><span class="line">    &#125; <span class="keyword">finally</span> &#123;</span><br><span class="line">        bossGroup.shutdownGracefully();</span><br><span class="line">        workerGroup.shutdownGracefully();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>因为我只给了两个核心的CPU资源，所以两个服务均只开启连个work进程即可。<br>5551端口表示PHP服务。<br>5555端口表示Java服务。</p><ul><li><strong>压测工具结果对比：ApacheBench (ab) </strong></li></ul><p>ab命令: docker run –rm jordi/ab -k -c 1000 -n 1000000 <a href="http://10.234.3.32:5555/" target="_blank" rel="noopener">http://10.234.3.32:5555/</a><br>在并发1000进行100万次Http请求的基准测试中,</p><p>Java + netty 压测结果:</p><img src="/2019/06/21/concurrency/bVbtoxV.jpg"><img src="/2019/06/21/concurrency/bVbtoyF.jpg"><p>PHP + swoole 压测结果:</p><img src="/2019/06/21/concurrency/bVbtox2.jpg"><img src="/2019/06/21/concurrency/bVbtoyJ.jpg"><table><thead><tr><th style="text-align:left"></th><th style="text-align:left">服务</th><th>QPS</th><th>响应时间ms(max,min)</th><th>内存(MB)</th></tr></thead><tbody><tr><td style="text-align:left"></td><td style="text-align:left">Java + netty</td><td>84042.11</td><td>(11,25)</td><td>600+</td><td></td></tr><tr><td style="text-align:left"></td><td style="text-align:left">php + swoole</td><td>87222.98</td><td>(9,25)</td><td>30+</td><td></td></tr></tbody></table><p>ps: 上图选择的是三次压测下的最佳结果。</p><p>总的来说，性能差异并不大，PHP+swoole的服务甚至比Java+netty的服务还要稍微好一点，特别是在内存占用方面，java用了600MB,php只用了30MB。<br>这能说明什么呢？<br>没有IO阻塞操作,不会发生协程切换。<br>这个仅仅只能说明 多线程+epoll的模式下,有效的压榨CPU性能，你甚至用PHP都能写出高并发和高性能的服务。</p><h3 id="性能对比——见证奇迹的时刻"><a href="#性能对比——见证奇迹的时刻" class="headerlink" title="性能对比——见证奇迹的时刻"></a>性能对比——见证奇迹的时刻</h3><p>上面代码其实并没有展现出协程的优秀性能，因为整个请求没有阻塞操作,但往往我们的应用会伴随着例如 文档读取、DB连接/查询 等各种阻塞操作,下面我们看看加上阻塞操作后,压测结果如何。<br>Java和PHP代码中,我都分别加上 <code>sleep(0.01) //秒</code>的代码，模拟0.01秒的系统调用阻塞。<br>代码就不再重复贴上来了。</p><p>带IO阻塞操作的 Java + netty 压测结果:</p><img src="/2019/06/21/concurrency/bVbtoDW.jpg"><p>大概10分钟才能跑完所有压测。。。</p><p>带IO阻塞操作的 PHP + swoole 压测结果:</p><img src="/2019/06/21/concurrency/bVbtoEb.jpg"><table><thead><tr><th style="text-align:left"></th><th style="text-align:left">服务</th><th>QPS</th><th>响应时间ms(max,min)</th><th>内存(MB)</th></tr></thead><tbody><tr><td style="text-align:left"></td><td style="text-align:left">Java + netty</td><td>1562.69</td><td>(52,160)</td><td>100+</td><td></td></tr><tr><td style="text-align:left"></td><td style="text-align:left">php + swoole</td><td>9745.20</td><td>(9,25)</td><td>30+</td><td></td></tr></tbody></table><p>从结果中可以看出,基于协程的php+ swoole服务比 Java + netty服务的QPS高了6倍。</p><p>当然，这两个测试代码都是官方demo中的源代码，肯定还有很多可以优化的配置，优化之后，结果肯定也会好很多。</p><p>可以再思考下，为什么官方默认线程/进程数量不设置的更多一点呢？<br>进程/线程数量可不是越多越好哦，前面我们已经讨论过了，在进程/线程切换的时候，会产生额外的CPU资源花销，特别是在用户态和内核态之间切换的时候！</p><p><strong>对于这些压测结果来说，我并不是针对Java,我是指 只要明白了高并发的核心是什么,找到这个目标，无论用什么编程语言，只要针对CPU利用率做有效的优化(连接池、守护进程、多线程、协程、select轮询、epoll事件驱动)，你也能搭建出一个高并发和高性能的系统。</strong></p><p>所以,你现在明白了，当我们在谈论高性能的时候，究竟在谈什么了吗？</p><p>思路永远比结果重要！</p><p>参考：<br>[1]: <a href="https://github.com/swoole/swoole-src/blob/master/README-CN.md" target="_blank" rel="noopener">https://github.com/swoole/swoole-src/blob/master/README-CN.md</a><br>[2]: <a href="https://github.com/netty/netty" target="_blank" rel="noopener">https://github.com/netty/netty</a><br>[3]: <a href="https://en.wikipedia.org/wiki/Transmission_Control_Protocol" target="_blank" rel="noopener">https://en.wikipedia.org/wiki/Transmission_Control_Protocol</a><br>[4]: <a href="https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http/helloworld" target="_blank" rel="noopener">https://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/http/helloworld</a></p><p><strong>作者</strong></p><p>陈刚，小米信息技术部服务研发</p><p><strong>招聘</strong></p><p>小米信息部武汉研发中心，信息部是小米公司整体系统规划建设的核心部门，支撑公司国内外的线上线下销售服务体系、供应链体系、ERP体系、内网OA体系、数据决策体系等精细化管控的执行落地工作，服务小米内部所有的业务部门以及 40 家生态链公司。</p><p>同时部门承担大数据基础平台研发和微服务体系建设落，语言涉及 Java、Go，长年虚位以待对大数据处理、大型电商后端系统、微服务落地有深入理解和实践的各路英雄。</p><p><strong>欢迎投递简历：jin.zhang(a)xiaomi.com</strong></p><p>更多技术文章：<a href="https://xiaomi-info.github.io/">小米信息部技术团队</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h3 id=&quot;什么是高并发&quot;&gt;&lt;a href=&quot;#什么是高并发&quot; class=&quot;headerlink&quot; title=&quot;什么是高并发?&quot;&gt;&lt;/a&gt;什么是高并发?&lt;/h3&gt;&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;g
      
    
    </summary>
    
    
  </entry>
  
</feed>
