<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>Eric.Yao</title>
		<description>Errors should never pass silently.</description>
		<link>https://sarkerson.github.io/</link>
		<atom:link href="https://sarkerson.github.io/feed.xml" rel="self" type="application/rss+xml" />
		<pubDate>Mon, 10 Nov 2025 13:18:52 +0000</pubDate>
		<lastBuildDate>Mon, 10 Nov 2025 13:18:52 +0000</lastBuildDate>
		<generator>Jekyll v3.10.0</generator>
    
		<item>
			<title>DDD 战术设计总结</title>
			<description>&lt;h1 id=&quot;目标&quot;&gt;目标&lt;/h1&gt;
&lt;blockquote&gt;
  &lt;p&gt;文本偏战术设计，目标有以下两点：（对战略设计感兴趣的话，可以参考文末的拓展阅读）&lt;/p&gt;

  &lt;ol&gt;
    &lt;li&gt;了解 DDD：你可以了解 DDD 概念，并看懂大部分 DDD 的项目代码；&lt;/li&gt;
    &lt;li&gt;落地 DDD：你可以了解如何用 DDD 思想来建模、落地项目，并了解所需注意的事项与规范；&lt;/li&gt;
  &lt;/ol&gt;

&lt;/blockquote&gt;

&lt;h1 id=&quot;前言&quot;&gt;前言&lt;/h1&gt;
&lt;p&gt;Eric Evans 于 2003 年出版了《领域驱动设计：软件核心复杂性应对之道》，在书中他创造了领域驱动设计方法。是“领域驱动“领域的指明灯。&lt;/p&gt;

&lt;p&gt;Vaughn Vernon 于 2014 年出版了《实现领域驱动设计》分别从战略和战术层面详尽地讨论了如何实现 DDD，其中包含了大量的最佳实践、设计准则和对一些问题的折中性讨论。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DDD 的作用域：&lt;/strong&gt;DDD 可以是单服务内，也可以是多个服务组成（微服务化）的系统范围内。&lt;/p&gt;

&lt;p&gt;微服务化不可回避两个问题：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;如何划分服务？（识别界限上下文）&lt;/li&gt;
  &lt;li&gt;微服务内部如何组织子模块，如何高效应对业务发展？（以领域为核心的分层架构）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;微服务与 DDD，在解决复杂业务问题时，采用了相同的指导思想，即分而治之。分治的手段有优雅也有不优雅，DDD 是一种分治的指导思想。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DDD 的目标&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在做架构设计时，一个好的架构应该需要实现以下几个目标：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;独立于框架：&lt;/strong&gt;架构不应该依赖某个外部的库或框架，不应该被框架的结构所束缚（可以轻松从 kite 切换成 kitex）。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;独立于UI：&lt;/strong&gt;前台展示的样式可能会随时发生变化（今天可能是网页、明天可能变成 console、后天是独立 app），但是底层架构不应该随之而变化。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;独立于底层数据源：&lt;/strong&gt;无论今天你用 MySQL、Oracle 还是 MongoDB、TiDB，甚至使用文件系统，软件架构不应该因为不同的底层数据储存方式而产生巨大改变。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;独立于外部依赖：&lt;/strong&gt;无论外部依赖如何变更、升级，业务的核心逻辑不应该随之而大幅变化。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;可测试：&lt;/strong&gt;无论外部依赖了什么数据库、硬件、UI或者服务，业务的逻辑应该都能够快速被验证正确性。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DDD 正是服务于上述目标的一个方法论。&lt;/p&gt;

&lt;p&gt;由于 DDD 不是一套框架，而是一种架构思想，所以在代码层面缺乏了足够的约束，导致 DDD 在实际应用中上手门槛很高，甚至可以说绝大部分人都对 DDD 的理解有所偏差。&lt;/p&gt;

&lt;p&gt;我对 DDD 的理解也可能是有偏差的，但这个可能偏差的理解，带着我回答了文章开头提到的问题，所以这个有偏差的理解也值得你一起来探讨。下文总结自业界多种实践沉淀下来的方法论，让你对 DDD 的架构、各层级的职责及约束有个认知，降低 DDD 的实践门槛。期望通过下文描述，可以让你以 DDD 思想，参与 DDD 项目的具体开发中。&lt;/p&gt;

&lt;h1 id=&quot;概念&quot;&gt;概念&lt;/h1&gt;
&lt;h2 id=&quot;案例&quot;&gt;案例&lt;/h2&gt;
&lt;p&gt;这里想通过一个案例，让你对 DDD 思想中涉及的概念有个宏观上的认知。&lt;/p&gt;

&lt;p&gt;需求：&lt;/p&gt;

&lt;p&gt;用户可以通过银行网页转账给另一个账号，支持跨币种转账。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110777.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;我们可以看到，一段业务代码里经常包含了参数校验、数据读取存储、业务计算、调用外部服务等多种逻辑。在这个案例里虽然是写在了同一个方法里，在真实代码中经常会被拆分成多个子方法，但实际效果是一样的，而在我们日常的工作中，绝大部分代码都或多或少的接近于此类结构。在 Martin Fowler 的 P of EAA 书中，这种很常见的代码样式被叫做 Transaction Script（事务脚本）。虽然这种类似于脚本的写法在功能上没有什么问题，但是长久来看，他有以下几个很大的问题：&lt;strong&gt;可维护性差、可扩展性差、可测试性差&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;考虑以下场景：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;突然又来了一个同币种转账的需求，上述逻辑是不是又有复制一份？&lt;/li&gt;
  &lt;li&gt;又有另外一个场景，涉及到转账，上述逻辑又得复制一份？&lt;/li&gt;
  &lt;li&gt;有一天，转账逻辑变更了，例如需要做合规检测，每个场景是不是都要修改？&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;下面是经过 DDD 思想重构后的代码：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110832.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;相比之下：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;应用层主要做编排，并且大概率这个编排逻辑是很少变动的；&lt;/li&gt;
  &lt;li&gt;业务逻辑沉淀到了领域服务中，一处改动，多处收益；&lt;/li&gt;
  &lt;li&gt;每个模块都是可测试的；&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;实体entity&quot;&gt;实体（Entity）&lt;/h2&gt;
&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;定义：&lt;/strong&gt;有唯一标识，能表示一个业务的生命周期的对象。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;组成：&lt;/strong&gt;实体由两部分组成：领域属性+领域能力
    &lt;ol&gt;
      &lt;li&gt;领域属性：对实体的描述，是实体的一部分。领域属性又可以分为，简单属性（string/int64等）和复杂属性（结构体，称为值对象）。&lt;/li&gt;
      &lt;li&gt;领域能力：实体自己的职责范围，能做什么，也可以叫作「实体行为」。&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;实体行为&lt;/strong&gt;：实体行为又称为领域能力，就是当前的业务对象能干什么事情，落地到代码上，就是实体的public方法。一般调用者都是application层。
    &lt;ol&gt;
      &lt;li&gt;原则：实体能自己干的事情，尽量自己干，不要交给聚合或领域服务，这样每个领域对象各司其职，把自己的行为做完整。然后和其他领域对象之间的边界职责又很清楚，这样的严格的组织，能容易地帮助业务实现高内聚。区别于过程式编码，需要考虑的东西会多很多，但会对长期业务发展带来好处。&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;实体行为的粒度&lt;/strong&gt;：实体行为的颗粒度，只有写代码的时候才会真正的思考。
    &lt;ol&gt;
      &lt;li&gt;颗粒度大小：原则上我们要求行为是颗粒度是最细的一件事情。提倡一个实体行为只干一件事情，这件事情的颗粒度最好是最细的。这样的好处就是为了方便复用。一般反对在一个实体的行为上去做一件以上事情，当你这么做的，你会发现非常难取名，你的方法上需要有两个动词，这时候，我们就要拆了。
        &lt;ol&gt;
          &lt;li&gt;当我们的颗粒度很细的时候，application 层需要做很多编排工作，这时候，你可以通过领域服务的方式进行封装。&lt;/li&gt;
        &lt;/ol&gt;
      &lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;实体和其他元素的关系&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110840.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;值对象value-object&quot;&gt;值对象（Value Object）&lt;/h2&gt;
&lt;p&gt;值对象通常作为实体的属性而存在，比如封面图、简介、标题、时间等。实体是客观存在的事物，值对象是为了描述事物，抽象出来的概念。是否拥有唯一身份标识，是实体与值对象的本质区别。&lt;/p&gt;

&lt;p&gt;值对象也是可以有行为，可以进行沉淀。例如，图片，对应的主题色计算，是一种沉淀；电话号码，对应的有效性检测是一种沉淀；等等。&lt;/p&gt;

&lt;p&gt;IDL 设计，尽量使用值对象建模思想，而不是分散的属性，难以管理、复用、沉淀&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110847.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;聚合aggregate&quot;&gt;聚合（Aggregate）&lt;/h2&gt;
&lt;p&gt;领域模型内的实体和值对象好比个体，而能让实体和值对象协同工作的组织就是聚合，用来确保这些领域对象在实现共同的业务逻辑时，能保证数据的一致性。&lt;/p&gt;

&lt;p&gt;场景：订单里面，包含商品、优惠券、邮费等属性，优惠券的存在，导致商品单价下降。当一个操作涉及多个实体操作时，并且存在一致性时，则需要把这些实体当作一个整体，这个整体叫做聚合。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110856.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;聚合由业务和逻辑紧密关联的实体和值对象组合而成，是&lt;strong&gt;数据修改和持久化的基本单元&lt;/strong&gt;，每个聚合对应一个仓储，实现数据的持久化。&lt;/p&gt;

&lt;p&gt;内容消费领域，读取、打包是常见的例子。datum 就是一种聚合，通过 loader 来加载 datum，datum 有自己的领域行为，例如校验、过滤等等。&lt;/p&gt;

&lt;h2 id=&quot;仓储repository&quot;&gt;仓储（Repository）&lt;/h2&gt;
&lt;p&gt;仓储负责聚合的「增删改查」操作，一个聚合（实体是一种特殊的聚合）对应一个仓储。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository VS DAO？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DAO 下，业务代码为数据层服务。DAO 操作的是数据库对象，基于此思想编写的代码，自然你的领域逻辑中就会包含数据库的读写逻辑、数据对象到领域对象的转换操作，会使得业务代码与数据库逻辑强耦合。&lt;/p&gt;

&lt;p&gt;Repository 下，数据层为业务代码服务。Repository 的出入参是领域对象，领域层只包含领域对象，不包含仓储层对象及逻辑。因此，一旦要换底层存储，则再编写一个 Repository 接口实现就好了。&lt;/p&gt;

&lt;p&gt;区分好领域模型、数据模型。&lt;/p&gt;

&lt;h2 id=&quot;领域服务domain-service&quot;&gt;领域服务（Domain Service）&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110865.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;当一个自治的聚合无法完成一个完整的业务场景，需要共同协作完成时，可以引入领域服务来封装多个聚合的协作行为。&lt;/p&gt;

&lt;p&gt;另一种场景是，如果实体的相关行为，需要引入仓储，那么也可以封装领域服务来解决。&lt;/p&gt;

&lt;p&gt;封装为领域服务的目的是，如果有第二个场景，需要用到这一个或多个聚合的协作行为时，可以直接复用，即实现领域知识的沉淀。&lt;/p&gt;

&lt;h1 id=&quot;分层&quot;&gt;分层&lt;/h1&gt;
&lt;h2 id=&quot;传统分层架构&quot;&gt;传统分层架构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110877.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;分层架构模式被认为是所有架构的始祖，被广泛地应用于Web、企业级应用和桌面应用。在这种架构中，我们将一个应用程序或者系统分为不同的层次。&lt;/p&gt;

&lt;p&gt;分层架构的一个重要原则是：每层只能与位于其下方的层发生耦合。分层架构也分为几种：在严格分层架构中，某层只能与直接位于其下方的层发生耦合；而松散分层架构则允许任意上方层与任意下方层发生耦合。由于用户界面层和应用服务通常需要与基础设施打交道，&lt;strong&gt;许多系统都是基于松散分层架构&lt;/strong&gt;的。&lt;/p&gt;

&lt;h2 id=&quot;使用依赖倒置的分层架构&quot;&gt;使用依赖倒置的分层架构&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110885.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;然而，在传统的分层架构中，却存在着一些问题，因为领域层或多或少地需要使用基础设施层，即领域层中的有些接口实现依赖于基础设施层。这使得业务规则和数据存储的代码耦合在一起。&lt;/p&gt;

&lt;p&gt;在书籍《实现领域驱动设计》中，Vernon 提出了基于依赖倒置的 DDD 分层架构，来改进传统分层架构。全书提到的 DDD 架构，不额外说明的情况下，都是基于该思想作为具体落地实现（具体来说是用六边形架构）。&lt;/p&gt;

&lt;p&gt;依赖倒置原则（Dependency Inversion Principle，DIP），由 Robert C. Martin 于 1996 年提出，其定义如下：&lt;/p&gt;

&lt;font style=&quot;background-color:rgb(255,245,235);&quot;&gt;高层模块不应该依赖于低层模块，两者都应该依赖于抽象。抽象不应该依赖于细节，细节应该依赖于抽象。&lt;/font&gt;

&lt;p&gt;该原则的提出具备重大意义，后面提到的六边形架构、洋葱架构、整洁架构，都是基于依赖倒置的分层架构的变种，没有实质上改变。&lt;/p&gt;

&lt;h2 id=&quot;用户接口层&quot;&gt;&lt;strong&gt;用户接口层&lt;/strong&gt;&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;职责&lt;/th&gt;
      &lt;th&gt;即常见的 Hanlder。用户接口层负责向用户显示信息和解释用户指令。这里的用户可能是：用户、程序、自动化测试和批处理脚本等等。&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;注意&lt;/td&gt;
      &lt;td&gt;如果用户界面使用了领域模型中的对象，那么此时的领域对象仅限于数据的渲染展现。在采用这种方式时，可以使用展现模型（Presentation Model，14）对用户界面与领域对象进行解耦。&lt;br /&gt;否则，领域对象修改，可能会导致展示层变化；或者展示层逻辑入侵领域层。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;输入输出&lt;/td&gt;
      &lt;td&gt;+ 输入：用户请求&lt;br /&gt;+ 输出：展示层对象&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;核心组件&lt;/td&gt;
      &lt;td&gt;assembler：对应用层返回的 DTO 做适配，返回不同前端所需数据&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;规范&lt;/td&gt;
      &lt;td&gt;请求对象是有业务“语意”的，尽量避免复用，哪怕参数是一样的。&lt;br /&gt;即，每个接口 req/resp 的 IDL 定义不复用。&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;DTO 是什么？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110893.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DTO Assembler：在Application层&lt;/strong&gt;，Entity 到 DTO 的转化器有一个标准的名称叫 DTO Assembler。Martin Fowler 在 P of EAA 一书里对于 DTO 和 Assembler 的描述：Data Transfer Object。DTO Assembler 的核心作用就是将一个或多个相关联的 Entity 转化为一个或多个 DTO。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Converter：在 Infrastructure 层&lt;/strong&gt;，Entity 到 DO 的转化器没有一个标准名称，但是为了区分 Data Mapper，我们叫这种转化器 Data Converter。这里要注意 Data Mapper 通常情况下指的是 DAO。&lt;/p&gt;

&lt;h2 id=&quot;应用层&quot;&gt;&lt;strong&gt;应用层&lt;/strong&gt;&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;职责&lt;/th&gt;
      &lt;th&gt;应用层是很薄的一层，理论上不应该有业务规则或逻辑，主要面向用例和流程相关的操作。但应用层又位于领域层之上，因为领域层包含多个聚合，所以它可以协调多个聚合的服务和领域对象完成&lt;strong&gt;服务编排和组合&lt;/strong&gt;，协作完成业务操作。&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;注意&lt;/td&gt;
      &lt;td&gt;在设计和开发时，不要将本该放在领域层的业务逻辑放到应用层中实现。因为庞大的应用层会使领域模型失焦，时间一长你的微服务就会演化为传统的三层架构，业务逻辑会变得分散、难以维护。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;输入输出&lt;/td&gt;
      &lt;td&gt;+ 输入：用户请求&lt;br /&gt;+ 输出：DTO&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;核心组件&lt;/td&gt;
      &lt;td&gt;+ assembler：对领域层、repo 层返回的 entity、aggr 做组装、适配，这个中间对象叫做 DTO&lt;br /&gt;+ app_service：若编排在多个场景使用，则可以封装为 app_service 供多个 handler 调用&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;规范&lt;/td&gt;
      &lt;td&gt;应用服务只负责业务流程串联，不负责业务逻辑。业务逻辑内聚到 domain 实现。&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;常用的ApplicationService“套路”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我们可以看出来，ApplicationService 的代码通常有类似的结构：AppService 通常不做任何决策，仅仅是把所有决策交给 DomainService 或 Entity，把跟外部交互的交给 Infrastructure 接口，如 Repository 或防腐层。&lt;/p&gt;

&lt;p&gt;一般的“套路”如下：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;准备数据：包括从外部服务或持久化源取出相对应的 Entity、Aggr 以及外部服务返回的 DTO。&lt;/li&gt;
  &lt;li&gt;执行操作：包括新对象的创建、赋值，以及调用领域对象的方法对其进行操作。需要注意的是这个时候通常都是纯内存操作，非持久化。&lt;/li&gt;
  &lt;li&gt;持久化：将操作结果持久化，或操作外部系统产生相应的影响，包括发消息等异步操作。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;领域层&quot;&gt;&lt;strong&gt;领域层&lt;/strong&gt;&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;职责&lt;/th&gt;
      &lt;th&gt;领域层的作用是实现企业核心业务逻辑，通过各种校验手段保证业务的正确性。领域层主要体现领域模型的业务能力，它用来表达业务概念、业务状态和业务规则。&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;注意&lt;/td&gt;
      &lt;td&gt;领域层的对当前系统的依赖只有领域层&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;核心组件&lt;/td&gt;
      &lt;td&gt;+ 聚合根、实体、值对象、领域服务&lt;br /&gt;+ 仓储接口&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;规范&lt;/td&gt;
      &lt;td&gt;+ 对依赖进行抽象接口设计（repo interface），使得业务逻辑和技术实现是相互隔离的。&lt;br /&gt;+ entity 只负责内存操作，不负责数据的存储。存储交给 inf/repo 层实现。&lt;br /&gt;+ 当操作涉及两个或以上的 entity 时，应该使用聚合根，放在 domain/aggr 目录下&lt;br /&gt;+ 当重要的逻辑无法挂到 entity、aggr 上来实现时，或者需要引入 repo，可以考虑构建领域服务，放在 domain/service 目录下。&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110903.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;传统开发方法中，是面向数据开发，即在业务逻辑中适配数据库的数据。在 DDD 思想中，领域层、应用层需要什么数据，约定好接口即可，交给 infra 层来实现。&lt;/p&gt;

&lt;p&gt;在传统架构设计中，由于上层应用对数据库的强耦合，很多公司在架构演进中最担忧的可能就是换数据库了，因为一旦更换数据库，就可能需要重写大部分的代码，这对应用来说是致命的。那采用依赖倒置的设计以后，应用层就可以通过解耦来保持独立的核心业务逻辑。当数据库变更时，我们只需要更换 repository 实现就可以了，这样就将资源变更对应用的影响降到了最低。&lt;/p&gt;

&lt;h2 id=&quot;基础层&quot;&gt;&lt;strong&gt;基础层&lt;/strong&gt;&lt;/h2&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;职责&lt;/th&gt;
      &lt;th&gt;基础层是贯穿所有层的，它的作用就是为其它各层提供通用的技术和基础服务，包括第三方工具、驱动、消息中间件、网关、文件、缓存以及数据库等。比较常见的功能还是提供数据库持久化。&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;注意&lt;/td&gt;
      &lt;td&gt;基础层包含基础服务，它采用依赖倒置设计，封装基础资源服务，实现应用层、领域层与基础层的解耦，降低外部资源变化对应用的影响。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;输入输出&lt;/td&gt;
      &lt;td&gt;+ 输入/输出：通常是原始数据类型、领域对象&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;核心组件&lt;/td&gt;
      &lt;td&gt;+ repo：实现领域层、应用层约定的 interface&lt;br /&gt;+ dependency：service/db/cache 等所有外部依赖&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;规范&lt;/td&gt;
      &lt;td&gt;repo 的入参和出参除了原始数据类型，只能包含领域对象&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110914.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在传统架构中，代码从上到下的变化速度基本上是一致的，改个需求需要从接口、到业务逻辑、到数据库全量变更，而第三方变更可能会导致核心业务代码的重写。但是在 DDD 中不同模块的代码的演进速度是不一样的：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Domain 层属于核心业务逻辑，属于经常被修改的地方。比如：原来不需要扣手续费，现在需要了之类的。通过 Entity 能够解决基于单个对象的逻辑变更，通过 Domain Service 解决多个对象间的业务逻辑变更。顺便你会发现，改了一个 domain 逻辑，所有的应用层都会受益。&lt;/li&gt;
  &lt;li&gt;Application 层属于 Use Case（业务用例）。业务用例一般都是描述比较大方向的需求，接口相对稳定，特别是对外的接口一般不会频繁变更。添加业务用例可以通过新增 Application Service 或者新增接口实现功能的扩展。&lt;/li&gt;
  &lt;li&gt;Infrastructure 层属于最低频变更的。一般这个层的模块只有在外部依赖变更了之后才会跟着升级，而外部依赖的变更频率一般远低于业务逻辑的变更频率。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以在 DDD 架构中，能明显看出越外层的代码越稳定，越内层的代码演进越快，真正体现了领域“驱动”的核心思想。&lt;/p&gt;

&lt;h1 id=&quot;架构选型&quot;&gt;架构选型&lt;/h1&gt;
&lt;h2 id=&quot;六边形架构&quot;&gt;六边形架构&lt;/h2&gt;
&lt;p&gt;https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c&lt;/p&gt;

&lt;p&gt;2005 年六边形架构被提出，比较鲜明的特点是将上下层结构换成同心圆结构，同心圆内层代表了应用的业务逻辑，外层代表应用的用户接口（driving-side）及外部资源（driven-side）。&lt;/p&gt;

&lt;p&gt;如右上图，红圈内的核心业务逻辑（应用程序和领域模型）与外部资源（包括应用的上游比如APP、Web 应用等，以及应用的下游比如数据库、缓存等）完全隔离，两者通过适配器进行交互，很好地实现了系统核心业务与外部依赖资源的解耦。通过适配器负责内层和外层的协议转换，使得系统核心业务能够以一致的方式被上游访问（不同的协议比如HTTPs、消息队列等，可以用不同适配器访问），也能适配不同的下游存储引擎。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110926.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;洋葱架构&quot;&gt;洋葱架构&lt;/h2&gt;
&lt;p&gt;2008 年洋葱架构被提出。洋葱架构可以看作是六边形架构的衍生，两者有相同的思路，都主张将业务核心逻辑与外部依赖资源进行解耦，避免外部依赖代码渗透到业务核心逻辑中。此外，洋葱架构在业务逻辑中加入了一些在 DDD 分层概念，比如用户接口层、应用层、领域层和基础层。&lt;/p&gt;

&lt;p&gt;I propose a new approach to architecture. &lt;u&gt;Honestly, it’s not completely new, but I’m proposing it as a named, architectural pattern. &lt;/u&gt; Patterns are useful because it gives software professionals a common vocabulary with which to communicate. There are a lot of aspects to the Onion Architecture, and if we have a common term to describe this approach, we can communicate more effectively.&lt;/p&gt;

&lt;p&gt;原文：https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102110935.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;整洁架构&quot;&gt;整洁架构&lt;/h2&gt;
&lt;p&gt;整洁架构，是 Robert C. Martin 在 2012 年提出的概念，本质上没有提出新的架构模式，而是整合了六边形架构、洋葱架构等架构模式，&lt;strong&gt;统一了命名及规范，让开发者可以使用统一的语言进行交流。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Though these architectures all vary somewhat in their details, they are very similar. &lt;/u&gt;They all have the same objective, which is the separation of concerns. They all achieve this separation by dividing the software into layers. Each has at least one layer for business rules, and another for interfaces.&lt;/p&gt;

&lt;p&gt;原文：https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html&lt;/p&gt;

&lt;p&gt;解析：https://betterprogramming.pub/the-clean-architecture-beginners-guide-e4b7058c1165&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;undefined&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;小结-1&quot;&gt;小结&lt;/h2&gt;
&lt;p&gt;基于依赖倒置原则的分层架构及其派生出来的各种架构模式，其思想是高度一致的。除了命名不一样、切入点不一样之外，其他的整体架构都是基于一个二维的内外关系。这也说明了基于DDD的架构最终的形态都是类似的。&lt;/p&gt;

&lt;p&gt;概括起来，有以下几个核心点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;抽象不依赖细节，细节应该依赖抽象。&lt;/li&gt;
  &lt;li&gt;内层模块不感知外层模块的存在。&lt;/li&gt;
  &lt;li&gt;业务逻辑应该高度内聚在领域层。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;即使遵循上述架构模式，具体落地的时候，仍然有非常多的细节值得注意。&lt;/p&gt;

&lt;h1 id=&quot;总结&quot;&gt;总结&lt;/h1&gt;
&lt;p&gt;DDD 不是一个什么特殊的架构，而是任何传统代码经过合理的重构之后最终一定会抵达的终点。DDD 的架构能够有效的解决传统架构中的问题：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;高可维护性：当外部依赖变更时，内部代码只用变更跟外部对接的模块，其他业务逻辑不变。&lt;/li&gt;
  &lt;li&gt;高可扩展性：做新功能时，绝大部分的代码都能复用，仅需要增加核心业务逻辑即可。&lt;/li&gt;
  &lt;li&gt;高可测试性：每个拆分出来的模块都符合单一性原则，绝大部分不依赖框架，可以快速的单元测试，做到100%覆盖。&lt;/li&gt;
  &lt;li&gt;代码结构清晰：当团队形成规范后，可以快速的定位到相关代码。&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;
&lt;ol&gt;
  &lt;li&gt;蓝皮书《领域驱动设计》Eric Evans&lt;/li&gt;
  &lt;li&gt;红皮书《实现领域驱动设计》Vaughn Vernon&lt;/li&gt;
  &lt;li&gt;优秀 DDD 博客
    &lt;ol&gt;
      &lt;li&gt;殷浩谈DDD系列（这系列强烈推荐，整体思路很清晰，实操性比较强）
        &lt;ul&gt;
          &lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/kpXklmidsidZEiHNw57QAQ&quot;&gt;&lt;u&gt;殷浩详解DDD系列 第一讲 - Domain Primitive&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/MU1rqpQ1aA1p7OtXqVVwxQ&quot;&gt;&lt;u&gt;殷浩详解DDD系列 第二讲 - 应用架构&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/1bcymUcjCkOdvVygunShmw&quot;&gt;&lt;u&gt;殷浩详解DDD系列 第三讲 - Repository模式&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/w1zqhWGuDPsCayiOgfxk6w&quot;&gt;&lt;u&gt;殷浩详解DDD系列 第四讲 - 领域层设计规范&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
          &lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s/1rdnkROdcNw5ro4ct99SqQ&quot;&gt;&lt;u&gt;殷浩详解DDD系列 第五讲 - 聊聊如何避免写流水账代码&lt;/u&gt;&lt;/a&gt;&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;一些 DDD 资源汇总 https://github.com/evancyz/ddd-learning?tab=readme-ov-file&lt;/li&gt;
&lt;/ol&gt;

</description>
			<pubDate>Sat, 03 Feb 2024 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2024/02/03/DDD-%E6%88%98%E6%9C%AF%E8%AE%BE%E8%AE%A1%E6%80%BB%E7%BB%93/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2024/02/03/DDD-%E6%88%98%E6%9C%AF%E8%AE%BE%E8%AE%A1%E6%80%BB%E7%BB%93/</guid>
        
			<category>database</category>
        
        
		</item>
    
		<item>
			<title>时序数据库——An Overview</title>
			<description>&lt;blockquote&gt;
  &lt;p&gt;TL;DR; 本文介绍常见时序数据库的基本架构，并以 InfluxDB 为例子介绍其存储模型及存储引擎的原理。最后介绍公司的 metrics 常见写入及读取操作。通过本文，你可以对时序数据库的原理有个初步了解，并可以对查询操作得心应手。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;简介&quot;&gt;简介&lt;/h1&gt;
&lt;h2 id=&quot;基本概念&quot;&gt;基本概念&lt;/h2&gt;
&lt;p&gt;时序数据库是处理时序数据最优的数据库类型，而时序数据是随时间变化而被监控，跟踪，降采样，聚合的指标数据和事件。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111984.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;为什么需要时序数据库&quot;&gt;为什么需要时序数据库？&lt;/h2&gt;
&lt;h3 id=&quot;特性&quot;&gt;特性&lt;/h3&gt;
&lt;p&gt;时序数据，区别于其他数据，拥有以下特征：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;数据随着时间增长，根据维度取值，而数据维度几乎不变。&lt;/li&gt;
  &lt;li&gt;持续高写入吞吐量，设备越多，写入数量越大，而且由于定期采样，写入量平稳。&lt;/li&gt;
  &lt;li&gt;持续高读取吞吐量。&lt;/li&gt;
  &lt;li&gt;几乎不会有更新操作（一个设备在某个时间点产生的数据不会变动）以及单独数据点的删除（通常只会删除过期时间范围内所有的数据）&lt;/li&gt;
  &lt;li&gt;查询一般都是查最近产生的数据，很少会去查询过期的数据。&lt;/li&gt;
  &lt;li&gt;设备之间的数据关联性小，同种类设备A和设备B产生的数据互相并不依赖，你并不需要join。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;面临的挑战&quot;&gt;面临的挑战&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;时序数据的写入：如何支持每秒钟上千万上亿数据点的写入。&lt;/li&gt;
  &lt;li&gt;时序数据的读取：如何支持在秒级对上亿数据的分组聚合运算。&lt;/li&gt;
  &lt;li&gt;成本敏感：由海量数据存储带来的是成本问题。如何更低成本的存储这些数据，将成为时序数据库需要解决的重中之重。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;应用场景&quot;&gt;应用场景&lt;/h3&gt;
&lt;p&gt;Metrics不仅仅可以用于软件开发中的监控指标大盘，还有以下应用场景：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;监控软件系统： 虚拟机、容器、服务、应用&lt;/li&gt;
  &lt;li&gt;监控物理系统： 设备、机器、接入的设备、环境、我们的房屋、我们的身体&lt;/li&gt;
  &lt;li&gt;资产跟踪应用： 汽车、卡车、物理容器、运货托盘（Pallets）&lt;/li&gt;
  &lt;li&gt;金融交易系统： 传统证券、新兴的加密数字货币&lt;/li&gt;
  &lt;li&gt;事件应用程序： 跟踪用户、客户的交互数据&lt;/li&gt;
  &lt;li&gt;商业智能工具： 跟踪关键指标和业务的总体健康情况&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;解决方案&quot;&gt;解决方案&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111020.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;https://db-engines.com/en/ranking/time+series+dbms&lt;/p&gt;

&lt;h3 id=&quot;组件概览&quot;&gt;组件概览&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111031.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;采集器&quot;&gt;采集器&lt;/h4&gt;
&lt;p&gt;采集器是用于采集时序数据的组件。它们通常是通过一些特定的协议或API获取数据，并将其发送到时序数据库中。&lt;/p&gt;

&lt;p&gt;通常会在服务器端部署 agent 来进行数据采集，然后传送给时序库。这里常见有推拉两种模式。推模式下，采集器会周期性地向时序数据库发送数据，实时性更高，且适合短作业，但是对写吞吐有较高要求；拉模式下，时序数据库会定期向采集器发起请求获取最新的数据，稳定性更高，但有延迟，通常只用于长作业。&lt;/p&gt;

&lt;h4 id=&quot;时序库&quot;&gt;时序库&lt;/h4&gt;
&lt;p&gt;监控系统的架构中，最核心的就是时序库。与传统的关系型数据库不同，时序数据库通常采用分布式架构和列式存储以支持高吞吐量和低延迟的数据写入和查询。常见的时序数据库包括InfluxDB、OpenTSDB、Prometheus等。它们通常提供了一些特殊的查询语言和API以便进行时序数据的查询和操作。&lt;/p&gt;

&lt;h4 id=&quot;告警引擎&quot;&gt;告警引擎&lt;/h4&gt;
&lt;p&gt;告警引擎的核心职责就是处理告警规则，生成告警事件。通常来讲，用户会配置数百甚至数千条告警规则，一些超大型的公司可能要配置数万条告警规则。每个规则里含有数据过滤条件、阈值、执行频率等，有一些配置丰富的监控系统，还支持配置规则生效时段、持续时长、留观时长等。&lt;/p&gt;

&lt;p&gt;当然，随着时代的发展，也有系统支持统计算法和机器学习的方式做告警预判。AiOps 概念中最容易落地，或者说落地之后最容易有效果的，就是告警引擎。&lt;/p&gt;

&lt;p&gt;告警引擎通常有两种架构，一种是数据触发式，一种是周期轮询式。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;数据触发式，是指服务端接收到监控数据之后，除了存储到时序库，还会转发一份数据给告警引擎，告警引擎每收到一条监控数据，就要判断是否关联了告警规则，做告警判断。因为监控数据量比较大，告警规则的量也可能比较大，所以告警引擎是会做分片部署的，即部署多个实例。这样的架构，即时性很好，但是想要做指标关联计算就很麻烦，因为不同的指标哈希后可能会落到不同的告警引擎实例。&lt;/li&gt;
  &lt;li&gt;周期轮询式，架构简单，通常是一个规则一个协程，按照用户配置的执行频率，周期性查询判断即可，因为是主动查询的，做指标关联计算就会很容易。像 Prometheus、Nightingale、Grafana 等，都是这样的架构。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;数据展示&quot;&gt;数据展示&lt;/h4&gt;
&lt;p&gt;监控数据的可视化也是一个非常通用且重要的需求，业界做得最成功的当数 Grafana。Grafana 采用插件式架构，可以支持不同类型的数据源，图表非常丰富，基本可以看做是开源领域的事实标准。很多公司的商业化产品中，甚至直接内嵌了 Grafana。当然，Grafana 新版本已经修改了开源协议，使用 AGPLv3，这就意味着如果某公司的产品基于 Grafana 做了二次开发，就必须公开代码。&lt;/p&gt;

&lt;h3 id=&quot;业界方案&quot;&gt;业界方案&lt;/h3&gt;
&lt;h4 id=&quot;influxdata-tick-stack&quot;&gt;InfluxData TICK Stack&lt;/h4&gt;
&lt;p&gt;https://zhoujinl.github.io/2018/02/27/tick/&lt;/p&gt;

&lt;p&gt;TICK 是由 InfluxData 开发的一套运维工具栈，由 Telegraf, InfluxDB, Chronograf, Kapacitor 四个工具的首字母组成。这一套组件将收集数据和入库、数据库存储、展示、告警四者囊括。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111040.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://w2.influxdata.com/time-series-platform/telegraf/&quot;&gt;Telegraf&lt;/a&gt;: 用Golang开发的代理程序，可用于收集和提交metric。Telegraf工作原理大概是这样：定时去执行&lt;strong&gt;输入插件&lt;/strong&gt;收集数据，数据经过&lt;strong&gt;处理插件&lt;/strong&gt;和&lt;strong&gt;聚合插件&lt;/strong&gt;，通过&lt;strong&gt;输出插件&lt;/strong&gt;输出到数据存储。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.influxdata.com/&quot;&gt;InfluxDB&lt;/a&gt;：一款专门处理高写入和查询负载的时序数据库。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://w2.influxdata.com/time-series-platform/chronograf/&quot;&gt;Chronograf&lt;/a&gt;: Chronograf 是InfluxData的开源可视化引擎，可让通过数据的实时可视化快速构建仪表板，并支持与 Kapacitor 联动。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://w2.influxdata.com/time-series-platform/kapacitor/&quot;&gt;Kapacitor&lt;/a&gt;: 指标和事件处理和告警引擎。使用它将时间序列数据处理成可操作的告警，并将这些告警发送到许多流行的产品，如 PagerDuty，Slack 等。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;open-falcon&quot;&gt;Open-Falcon&lt;/h4&gt;
&lt;p&gt;https://cloud.tencent.com/developer/article/1845407&lt;/p&gt;

&lt;p&gt;小米开源的云监控系统，提供了采集、存储、查询、告警、展示等一整套解决方案，支持主机、应用、网络等多种监控数据类型。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111052.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;prometheus-stack&quot;&gt;Prometheus Stack&lt;/h4&gt;
&lt;p&gt;https://prometheus.kpingfan.com/01-introduction/01.prometheus%E6%9E%B6%E6%9E%84/&lt;/p&gt;

&lt;p&gt;由 Prometheus、Alertmanager 和 Grafana 组成的一整套解决方案，用于采集、存储、查询和可视化时序数据，并提供了告警和自动化操作的功能。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111064.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Prometheus Server：Prometheus组件中的核心部分，负责对监控数据的获取，存储及查询。&lt;/li&gt;
  &lt;li&gt;Exporter：负责将监控数据通过HTTP服务的形式暴露给Prometheus Server，Prometheus Server通过访问该Exporter提供的Endpoint端点，即可获取到需要采集的监控数据。&lt;/li&gt;
  &lt;li&gt;PushGateway：通过PushGateway将监控数据主动Push到Gateway当中。而Prometheus Server则可以采用同样Pull的方式从PushGateway中获取到监控数据。解决短作业无法提供通信服务的问题。&lt;/li&gt;
  &lt;li&gt;AlertManager：在Prometheus Server中支持基于PromQL创建告警规则，如果满足PromQL定义的规则，则会产生一条告警，而告警的后续处理流程则由AlertManager进行管理。AlertManager集成了邮件，Slack等通知方式，也提供Webhook自定义告警处理。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;1324-metrics&quot;&gt;1.3.2.4. Metrics&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://bytedance.feishu.cn/wiki/wikcn8Y3Bp5Sqw5pEuO18kZn7dh&quot;&gt;Metrics系统是什么？&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111074.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;写入&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;写入侧组件是用户需要感知的，由如下2类：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;SDK&lt;/strong&gt;: Metrics提供了多种开发语言的SDK&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;agent&lt;/strong&gt;: Metrics在每台物理机/云主机都有部署名为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metricserver2&lt;/code&gt;的agent，该agent负责收集SDK打过来的指标，并且每30s进行一次&lt;strong&gt;序列内&lt;/strong&gt;、&lt;strong&gt;时间纬度&lt;/strong&gt;上的聚合，然后发送给Metrics后端。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;存储&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;按照时序数据的&lt;strong&gt;冷热&lt;/strong&gt;特点，Metrics将数据按照时间纬度，存放在不同的存储系统：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;近28小时数据：存放在热存&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tsdc&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;28小时以外数据：存放在冷存&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mstore&lt;/code&gt;(on HDFS)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;此外，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mstore&lt;/code&gt;会archive历史数据，具体规则：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;近30天内：不archive&lt;/li&gt;
  &lt;li&gt;近60天～近30天：按照5分钟一个点archive&lt;/li&gt;
  &lt;li&gt;60天以以外：按照1h一个点archive&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;存储模型及存储引擎&quot;&gt;存储模型及存储引擎&lt;/h1&gt;
&lt;p&gt;这里以 influxdb 为例子，介绍其存储模型及存储引擎，其他时序数据库思路类似&lt;/p&gt;

&lt;h2 id=&quot;存储模型&quot;&gt;存储模型&lt;/h2&gt;
&lt;p&gt;InfluxDB 使用的是典型的 KV 存储模型。Measurement+Tags 确定一个 timeseries。&lt;/p&gt;

&lt;p&gt;下面是一条向InfluxDB中写入一条数据的命令行，来看下这条数据由哪几个部分组成：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;INSERT machine_metric,cluster=Cluster-A,hostname=host-a cpu=10 1501554197019201823
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上面是一条向InfluxDB中写入一条数据的命令行，来看下这条数据由哪几个部分组成：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Measurement：Measurement代表数据所属监控指标的名称。例如上述例子是对机器指标的监控，所以其measurement命名为machine_metric。&lt;/li&gt;
  &lt;li&gt;Tags：用于描述measurement的不同的维度，允许存在一个或多个Tag，每个Tag也是由TagKey和TagValue构成。&lt;/li&gt;
  &lt;li&gt;Field：一行measurement数据可以对应多个value，每个value根据Field来区分。&lt;/li&gt;
  &lt;li&gt;Timestamp: 时序数据的必备属性，代表该条数据所属的时间点，可以看到InfluxDB的时间精度能够精确到纳秒。&lt;/li&gt;
  &lt;li&gt;TimeSeries：Measurement+Tags的组合，在InfluxDB中被称为TimeSeries。TimeSeries就是时间线，根据时间能够定位到某个时间点，所以TimeSeries+Field+Timestamp能够定位到某个Value。这个概念比较重要，在后续的章节中都会提到。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;最终在逻辑上每个Measurement内的数据会组织成一张大的数据表，如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111094.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在查询时，InfluxDB支持在Measurement内任意维度的条件查询，你可以指定任意某个Tag或者Filed的条件做查询。接着上面的数据案例，你可以构造以下查询条件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-plain&quot;&gt;SELECT * FROM &quot;machine_metric&quot; WHERE time &amp;gt; now() - 1h;  
SELECT * FROM &quot;machine_metric&quot; WHERE &quot;cluster&quot; = &quot;Cluster-A&quot; AND time &amp;gt; now() - 1h;
SELECT * FROM &quot;machine_metric&quot; WHERE &quot;cluster&quot; = &quot;Cluster-A&quot; AND cpu &amp;gt; 5 AND time &amp;gt; now() - 1h;
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;从数据模型以及查询的条件上看，Tag和Field没有任何区别。从语义上来看，Tag用于描述Measurement，而Field用于描述Value。从内部实现来上看，Tag会被全索引，而Filed不会，所以根据Tag来进行条件查询会比根据Filed来查询效率高很多。&lt;/p&gt;

&lt;h2 id=&quot;存储引擎&quot;&gt;存储引擎&lt;/h2&gt;
&lt;p&gt;https://docs.influxdata.com/influxdb/v1.8/concepts/storage_engine&lt;/p&gt;

&lt;p&gt;https://zhuanlan.zhihu.com/p/32710333&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111105.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;概念&quot;&gt;概念&lt;/h3&gt;
&lt;p&gt;InfluxDB在经历了几个小版本的BoltDB后，最终决定自研TSM，TSM的设计目标一是解决LevelDB的文件句柄过多问题，二是解决BoltDB的写入性能问题。TSM全称是Time-Structured Merge Tree，思想类似LSM，不过是基于时序数据的特性做了一些特殊的优化。来看下TSM的一些重要组件：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;In-Memory Index - The in-memory index is a shared index across shards that provides the quick access to &lt;a href=&quot;https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#measurement&quot;&gt;measurements&lt;/a&gt;, &lt;a href=&quot;https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#tag&quot;&gt;tags&lt;/a&gt;, and &lt;a href=&quot;https://docs.influxdata.com/influxdb/v1.8/concepts/glossary/#series&quot;&gt;series&lt;/a&gt;. The index is used by the engine, but is not specific to the storage engine itself.&lt;/li&gt;
  &lt;li&gt;Write Ahead Log(WAL) : 数据会先写入WAL持久化，后进入memory-index和cache。Cache内数据会异步刷入TSM File，在Cache内数据未持久化到TSM File之前若遇到进程crash，则会通过WAL内的数据来恢复cache内的数据，这个行为与LSM是完全类似的。&lt;/li&gt;
  &lt;li&gt;Cache: TSM的Cache与LSM的MemoryTable类似，其内部的数据为WAL中未持久化到TSM File的数据。若进程发生failover，则cache中的数据会根据WAL中的数据进行重建。&lt;/li&gt;
  &lt;li&gt;TSM Files: TSM File与LSM的SSTable类似，TSM File由四个部分组成，分别为：header, blocks, index和footer。后文会详细介绍。&lt;/li&gt;
  &lt;li&gt;Compaction: compaction是一个将write-optimized的数据存储格式优化为read-optimized的数据存储格式的一个过程&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;tsm-文件&quot;&gt;TSM 文件&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111113.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;TSM文件最核心的由Series Data Section以及Series Index Section两个部分组成，其中前者表示存储时序数据，而后者存储文件级别B+树索引，用于在文件中快速查询时间序列数据块。&lt;/p&gt;

&lt;h4 id=&quot;series-data-block&quot;&gt;Series Data Block&lt;/h4&gt;
&lt;p&gt;Map中一个Key对应一系列时序数据，因此能想到的最简单的flush策略是将这一系列时序数据在内存中构建成一个Block并持久化到文件。然而，有可能一个Key对应的时序数据非常之多，导致一个Block非常之大，超过Block大小阈值，因此在实际实现中有可能会将同一个Key对应的时序数据构建成多个连续的Block。但是，在任何时候，同一个Block中只会存储同一种Key的数据。&lt;/p&gt;

&lt;p&gt;另一个需要关注的点在于，Map会按照Key顺序排列并执行flush，这是构建索引的需求。Series Data Block文件结构如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111122.jpeg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;series-index-block&quot;&gt;Series Index Block&lt;/h4&gt;
&lt;p&gt;每个key 对应一个Index Block。&lt;/p&gt;

&lt;p&gt;很多时候用户需要根据Key查询某段时间（比如最近一小时）的时序数据，如果没有索引，就会需要将整个TSM文件加载到内存中才能一个Data Block一个Data Block查找，这样一方面非常占用内存，另一方面查询效率非常之低。为了在不占用太多内存的前提下提高查询效率，TSM文件引入了索引。TSM文件索引数据由一系列索引Block组成，每个索引Block的结构如下图所示：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://obsidian-1254275759.cos.ap-shanghai.myqcloud.com/202511102111132.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Series Index Block由Index Block Meta以及一系列Index Entry构成：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Index Block Meta最核心的字段是Key，表示这个索引Block内所有IndexEntry所索引的时序数据块都是该Key对应的时序数据。&lt;/li&gt;
  &lt;li&gt;Index Entry表示一个索引字段，指向对应的Series Data Block。指向的Data Block由Offset唯一确定，Offset表示该Data Block在文件中的偏移量，Size表示指向的Data Block大小。Min Time和Max Time表示指向的Data Block中时序数据集合的最小时间以及最大时间，用户在根据时间范围查找时可以根据这两个字段进行过滤。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;读写&quot;&gt;读写&lt;/h3&gt;
&lt;p&gt;https://zhuanlan.zhihu.com/p/97247465&lt;/p&gt;

&lt;h4 id=&quot;写入&quot;&gt;写入&lt;/h4&gt;
&lt;p&gt;Writes are appended to the current WAL segment and are also added to the Cache. Each WAL segment has a maximum size. Writes roll over to a new file once the current file fills up. The cache is also size bounded; snapshots are taken and WAL compactions are initiated when the cache becomes too full. If the inbound write rate exceeds the WAL compaction rate for a sustained period, the cache may become too full, in which case new writes will fail until the snapshot process catches up.&lt;/p&gt;

&lt;p&gt;When WAL segments fill up and are closed, the Compactor snapshots the Cache and writes the data to a new TSM file. When the TSM file is successfully written and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fsync&lt;/code&gt;’d, it is loaded and referenced by the FileStore.&lt;/p&gt;

&lt;p&gt;Updates (writing a newer value for a point that already exists) occur as normal writes. Since cached values overwrite existing values, newer writes take precedence. &lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt;If a write would overwrite a point in a prior &lt;/font&gt;&lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt;TSM&lt;/font&gt;&lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt; file, the points are merged at query runtime and the newer write takes precedence.&lt;/font&gt;&lt;/p&gt;

&lt;p&gt;Deletes occur by writing a delete entry to the WAL for the measurement or series and then updating the Cache and FileStore. The Cache evicts all relevant entries. &lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt;The FileStore writes a &lt;/font&gt;&lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt;tombstone&lt;/font&gt;&lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt; file for each &lt;/font&gt;&lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt;TSM&lt;/font&gt;&lt;font style=&quot;background-color:rgba(222,224,227,0.8);&quot;&gt; file that contains relevant data. &lt;/font&gt;These tombstone files are used at startup time to ignore blocks as well as during compactions to remove deleted entries.&lt;/p&gt;

&lt;p&gt;Queries against partially deleted series are handled at query time until a compaction removes the data fully from the TSM files.&lt;/p&gt;

&lt;h4 id=&quot;读取&quot;&gt;读取&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;undefined&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;上图中中间部分为索引层，TSM在启动之后就会将TSM文件的索引部分加载到内存，数据部分因为太大并不会直接加载到内存。用户查询可以分为三步：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;首先根据Key找到对应的SeriesIndex Block，因为Key是有序的，所以可以使用二分查找来具体实现。&lt;/li&gt;
  &lt;li&gt;找到SeriesIndex Block之后再根据查找的时间范围，使用[MinTime, MaxTime]索引定位到可能的Series Data Block列表。&lt;/li&gt;
  &lt;li&gt;将满足条件的Series Data Block加载到内存中解压进一步使用二分查找算法根据timestamp查找即可找到。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;压缩&quot;&gt;压缩&lt;/h3&gt;
&lt;h4 id=&quot;level-compaction&quot;&gt;Level Compaction&lt;/h4&gt;
&lt;p&gt;InfluxDB将TSM文件分为4个层级(Level 1-4)，compaction只会发生在同层级文件内，同层级的文件compaction后会晋升到下一层级。从这个规则看，根据时序数据的产生特性，level越高数据生成时间越久，访问热度越低。由Cache数据初次生成的TSM文件称为Snapshot，多个Snapshot文件compaction后产生Level1的TSM文件，Level1的文件compaction后生成level2的文件，依次类推。&lt;/p&gt;

&lt;p&gt;低Level和高Level的compaction会采用不同的算法，低level文件的compaction采用低CPU消耗的做法，例如不会做解压缩和block合并，而高level文件的compaction则会做block解压缩以及block合并，以进一步提高压缩率。&lt;/p&gt;

&lt;h4 id=&quot;index-optimization-compaction&quot;&gt;Index Optimization Compaction&lt;/h4&gt;
&lt;p&gt;当Level4的文件积攒到一定个数后，index会变得很大，查询效率会变的比较低。影响查询效率低的因素主要在于同一个TimeSeries数据会被多个TSM文件所包含，所以查询不可避免的需要跨多个文件进行数据整合。所以IndexOptimizationCompaction的主要作用就是将同一TimeSeries下的数据合并到同一个TSM文件中，尽量减少不同TSM文件间的TimeSeries重合度。&lt;/p&gt;

&lt;h4 id=&quot;full-compaction&quot;&gt;Full Compaction&lt;/h4&gt;
&lt;p&gt;InfluxDB在判断某个Shard长时间内不会再有数据写入之后，会对数据做一次FullCompaction。FullCompaction是LevelCompaction和IndexOptimization的整合，在做完一次FullCompaction之后，这个Shard不会再做任何的compaction，除非有新的数据写入或者删除发生。这个策略是对冷数据的一个规整，主要目的在于提高压缩率。&lt;/p&gt;

&lt;h3 id=&quot;查询优化&quot;&gt;查询优化&lt;/h3&gt;
&lt;h3 id=&quot;演变历程&quot;&gt;演变历程&lt;/h3&gt;
&lt;p&gt;InfluxDB 的存储引擎经历了从 LSM Tree =&amp;gt; B+Tree =&amp;gt; TSM Tree 的过程。&lt;/p&gt;

&lt;p&gt;https://docs.influxdata.com/influxdb/v1.8/concepts/storage_engine/&lt;/p&gt;

&lt;h1 id=&quot;metrics&quot;&gt;Metrics&lt;/h1&gt;
&lt;h2 id=&quot;写入-1&quot;&gt;写入&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://bytedance.feishu.cn/wiki/wikcnxbsf7fzZRQabZfxjXcVfAf#FjsXAM&quot;&gt;Metrics看这一篇就够了&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;metrics_agent 每30s进行一次&lt;strong&gt;序列内&lt;/strong&gt;、&lt;strong&gt;时间纬度&lt;/strong&gt;上的聚合，然后发送给Metrics后端。&lt;/p&gt;

&lt;h3 id=&quot;counter&quot;&gt;counter&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;会将在指定tagkv集合积攒的值与打上来的值相加作为新值, 关注的是变化量和变化速率&lt;/li&gt;
  &lt;li&gt;会一直累加直到达到 double 类型的最大值，即2^1024，也就是1.79E+308&lt;/li&gt;
  &lt;li&gt;适用于求rate{counter}(rate表示求导，counter表示去掉导数的负值)之后计算任意操作的速率(qps/tps/ops) 使用。&lt;/li&gt;
  &lt;li&gt;举个例子，每分钟新增打点 30 个，rate{counter}对 60 秒求导，30/60 = 0.5,查询时勾上 rate，counter 的情况下的值就是 0.5&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;store&quot;&gt;Store&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;打上来什么值就是什么值，实际显示的值是30秒的采样周期中最后打上来的值。&lt;/li&gt;
  &lt;li&gt;会按照 tag 分组后统计（tag 相同的 30 秒内取最后一次的值，tag 不同的两个都上传）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;适用于只关注每个周期最新状态的监控采集，如 CPU/内存使用率，&lt;/strong&gt;&lt;strong&gt;线程&lt;/strong&gt;&lt;strong&gt;数，连接数，消费积压量&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;timer&quot;&gt;Timer&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;将30秒的采样周期内（同一个ms2/宿主机）打上来的值缓存起来，然后在采样周期结束时统计本采样周期内打上来的值。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;个人理解，timer 本质上与 counter/store 类似，只不过顺便提供了窗口计算功能。猜测 &lt;strong&gt;metrics&lt;/strong&gt; 在 agent 端对 timer 数据做了预处理，预计算了 metrics 在窗口内的统计数据。例如：counter（30秒打了有多少个值，大致为__qps__*30）、avg、pctx 等。&lt;/em&gt;&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;t0&lt;/th&gt;
      &lt;th&gt;t0+10s&lt;/th&gt;
      &lt;th&gt;t0+20s&lt;/th&gt;
      &lt;th&gt;t0+30s&lt;/th&gt;
      &lt;th&gt;t0+40s&lt;/th&gt;
      &lt;th&gt;t0+50s&lt;/th&gt;
      &lt;th&gt;…&lt;/th&gt;
      &lt;th&gt;t0+ns&lt;/th&gt;
      &lt;th&gt; &lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Raw data&lt;/td&gt;
      &lt;td&gt;27&lt;/td&gt;
      &lt;td&gt;28&lt;/td&gt;
      &lt;td&gt;29&lt;/td&gt;
      &lt;td&gt;32&lt;/td&gt;
      &lt;td&gt;31&lt;/td&gt;
      &lt;td&gt;30&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;27.3&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;.min&lt;/td&gt;
      &lt;td&gt;27&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;30&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;.max&lt;/td&gt;
      &lt;td&gt;29&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;32&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;.avg&lt;/td&gt;
      &lt;td&gt;28&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;31&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;.counter&lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;rate_counter&quot;&gt;rate_counter&lt;/h3&gt;
&lt;p&gt;类似&lt;a href=&quot;https://doc.bytedance.net/docs/3593/4657/36781/&quot;&gt;&lt;u&gt;打点类型-counter&lt;/u&gt;&lt;/a&gt;，但是是将30秒的采样周期内打上来的值累加/30秒，算出变化率，比如QPS。最终将这个变化率emit出去。查询时如果再次求rate，就是变化率的变化率，比如QPS的变化率。不关注增量，只关注变化率的可以使用这个打点，相对于 counter 查询时无需做 rate计算，开销较低，支持流式聚合，支持预聚合等优化&lt;/p&gt;

&lt;h3 id=&quot;meter&quot;&gt;meter&lt;/h3&gt;
&lt;p&gt;为Counter和Rate_Counter的结合，一次meter类型的打点可以生成两种类型打点。&lt;/p&gt;

&lt;h2 id=&quot;查询&quot;&gt;查询&lt;/h2&gt;
&lt;p&gt;https://tech.bytedance.net/articles/6867450721697185799&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;undefined&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;了解基本原理之后，查询逻辑基本可以通吃&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;聚合算子：对指定 tag 的 timeseries 进行 groupby&lt;/li&gt;
  &lt;li&gt;降采样：在执行聚合算子之后，对新数据进行执行降采样操作&lt;/li&gt;
  &lt;li&gt;运算符：在上述两种 reduce 操作执行之后，执行运算符，得到结果列表&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;附录&quot;&gt;附录&lt;/h1&gt;
&lt;h2 id=&quot;引用&quot;&gt;引用&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;
    &lt;table&gt;
      &lt;tbody&gt;
        &lt;tr&gt;
          &lt;td&gt;[In-memory indexing and the Time-Structured Merge Tree (TSM)&lt;/td&gt;
          &lt;td&gt;InfluxDB OSS 1.8 Documentation](https://docs.influxdata.com/influxdb/v1.8/concepts/storage_engine/)&lt;/td&gt;
        &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://bytedance.feishu.cn/wiki/wikcnxbsf7fzZRQabZfxjXcVfAf&quot;&gt;Metrics看这一篇就够了&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;
    &lt;table&gt;
      &lt;tbody&gt;
        &lt;tr&gt;
          &lt;td&gt;[InfluxDB storage subsystem: the TSM files&lt;/td&gt;
          &lt;td&gt;Just my thouhgts](https://migue.github.io/post/influx-storage-tsm-component/)&lt;/td&gt;
        &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;table&gt;
      &lt;tbody&gt;
        &lt;tr&gt;
          &lt;td&gt;[InfluxDB storage subsystem: an introduction&lt;/td&gt;
          &lt;td&gt;Just my thouhgts](https://migue.github.io/post/quick-tour-influx-storage/)&lt;/td&gt;
        &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;拓展阅读&quot;&gt;拓展阅读&lt;/h2&gt;
&lt;ul&gt;
  &lt;li&gt;https://tech.bytedance.net/articles/6867450721697185799&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://medium.com/swlh/log-structured-merge-trees-9c8e2bea89e8&quot;&gt;Log Structured Merge Trees&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/google/leveldb&quot;&gt;LevelDB: a fast key-value storage&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Fri, 16 Jun 2023 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2023/06/16/%E6%97%B6%E5%BA%8F%E6%95%B0%E6%8D%AE%E5%BA%93/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2023/06/16/%E6%97%B6%E5%BA%8F%E6%95%B0%E6%8D%AE%E5%BA%93/</guid>
        
			<category>database</category>
        
        
		</item>
    
		<item>
			<title>信息流系统设计</title>
			<description>&lt;h1 id=&quot;信息流系统设计&quot;&gt;信息流系统设计&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;TL;DR; 本文以关注流为例子，介绍了几种信息流的实现方案，并对比其优缺点，介绍了各自适合的应用场景。并归纳了信息流领域常见的幻读、不可重复读问题，参考数据库隔离级别实现的思路，提出对应的两个解决方案。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;信息流（或称 Feed 流）这种功能在我们手机 APP 中几乎随处可见，最典型的就是微信朋友圈、Twitter 等。&lt;/p&gt;

&lt;p&gt;但是对于推荐流跟关注流这两种 Feed 流，它们背后用到的技术架构差别会比较大。不同于基于模型排序的推荐流，关注流通常基于时间线来排序，用户对数据的完整性和排序都比较敏感。&lt;/p&gt;

&lt;h1 id=&quot;push-mode&quot;&gt;push mode&lt;/h1&gt;

&lt;p&gt;push mode 对每个粉丝维护一个「关注拉链」的存储；每当关注的用户有新动态（发文、评论等）时，则将动态离线写入其粉丝的「关注拉链」存储中。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;用户发文时&lt;/strong&gt;，遍历所有的粉丝，将文章写入所有粉丝的「关注拉链」存储中。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;用户消费信息流时&lt;/strong&gt;，直接从「关注拉链」中读取即可。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;好处&lt;/strong&gt;: 读帖简单；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;坏处&lt;/strong&gt;: 发帖复杂，存在写扩散问题；大 v 粉丝量多时存在性能；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;: 用户活跃，经常刷帖；无大 v，用户粉丝量少；&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-4f129b28512ead524fd9111e46d455d0_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;pull-mode&quot;&gt;pull mode&lt;/h1&gt;

&lt;p&gt;pull mode 即实时召回，每个用户不会维护一个拉链进行存储，而是实时拉取「关注用户」的动态，进行实时排序。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;用户发文时&lt;/strong&gt;，写入自己的「发文拉链」中；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;用户消费信息流时&lt;/strong&gt;，遍历关注的所有作者，从作者发文拉链中实时拉取数据，进行合并、排序；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;好处&lt;/strong&gt;: 节约存储，避免写扩散；发帖简单；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;坏处&lt;/strong&gt;: 读帖复杂；关注人多时存在性能问题；&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;适用场景&lt;/strong&gt;: 用户不活跃，很少读帖；有大 v 场景，粉丝量很多；关注的人少；&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-2968e65dee780275ed26ec567ef61358_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;pull--push-混合&quot;&gt;pull &amp;amp; push 混合&lt;/h1&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt; &lt;/th&gt;
      &lt;th&gt;优点&lt;/th&gt;
      &lt;th&gt;缺点&lt;/th&gt;
      &lt;th&gt;适用场景&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Pull mode&lt;/td&gt;
      &lt;td&gt;节约存储，避免写扩散发帖简单&lt;/td&gt;
      &lt;td&gt;读帖复杂关注人多时存在性能问题&lt;/td&gt;
      &lt;td&gt;用户不活跃，很少读帖有大 v 场景，粉丝量很多关注的人少&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Push mode&lt;/td&gt;
      &lt;td&gt;读帖简单&lt;/td&gt;
      &lt;td&gt;发帖复杂，存在写扩散问题大 v 粉丝量多时存在性能&lt;/td&gt;
      &lt;td&gt;用户活跃，经常刷帖无大 v，用户粉丝量少&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;针对上述，对用户群体进行划分，混合使用 pull &amp;amp; push，具体操作如下：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;将用户群体分为活跃 G1、不活跃 G2 两个组；&lt;/li&gt;
  &lt;li&gt;当大明星 X1 发帖时，将帖子写入 X 的发件箱的同时，顺便写入其粉丝中属于 G1 那部分的收件箱；&lt;/li&gt;
  &lt;li&gt;当路人甲 X2 发帖时，采用 push mode，遍历他的所有粉丝并将帖子写入粉丝收件箱；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;现在考虑消费场景：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;G1 用户登录刷Feed流时：直接从自己的收件箱读取帖子即可，保证了活跃用户的体验。&lt;/li&gt;
  &lt;li&gt;G2 用户突然登录刷Feed流时：
    &lt;ol&gt;
      &lt;li&gt;读他的收件箱；&lt;/li&gt;
      &lt;li&gt;遍历他所关注的大 V 用户的发件箱提取帖子；&lt;/li&gt;
      &lt;li&gt;merge 上述结果；&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;注：因为有 pull mode 的场景存在，因此即使是混合模式，每个阅读者所能关注的人数也要设置上限。&lt;/p&gt;

&lt;h1 id=&quot;关注流的方案&quot;&gt;关注流的方案&lt;/h1&gt;

&lt;p&gt;根据量级测算，笔者所在业务关注流使用了 pull-mode，存储选型使用了内部的 IndexService。这是一个基于 LMDB 的纯内存数据库，并且会建立基于 score 的索引，由于每条发文拉链不会很长，扇出百级别、近千级别仍然可以有不错的性能。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;特性 / 存储选型&lt;/th&gt;
      &lt;th&gt;IndexService&lt;/th&gt;
      &lt;th&gt;ByteGraph&lt;/th&gt;
      &lt;th&gt;Redis&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;存储模型&lt;/td&gt;
      &lt;td&gt;key:value|score|extra（可以基于 extra 进行业务过滤）&lt;/td&gt;
      &lt;td&gt;Vertex –Edge–&amp;gt; Vertex&lt;/td&gt;
      &lt;td&gt;key:value|score&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;性能&lt;/td&gt;
      &lt;td&gt;批量取倒排延时 IndexService &amp;lt; redis &amp;lt; Rocksdb&lt;/td&gt;
      &lt;td&gt;底层依赖 Rocksdb，latency 比 IndexService 高&lt;/td&gt;
      &lt;td&gt;性能优于 abase，因为 abase 基于 rocksdb&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;召回&lt;/td&gt;
      &lt;td&gt;支持多种 merge支持排序&lt;/td&gt;
      &lt;td&gt;业务自己 merge单条召回支持排序，多路召回需要业务自己排序&lt;/td&gt;
      &lt;td&gt;业务自己 merge单条召回支持排序，多路召回需要业务自己排序&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;读取&lt;/td&gt;
      &lt;td&gt;支持 impression_container 消重对于多路召回，不需要业务维护游标，使用内存现算&lt;/td&gt;
      &lt;td&gt;业务自己消重业务需要维护多路游标，否则可能导致漏数据&lt;/td&gt;
      &lt;td&gt;业务自己消重业务需要维护多路游标，否则可能导致漏数据&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;拓展性&lt;/td&gt;
      &lt;td&gt;拓展好，extra_info 可以携带拓展信息&lt;/td&gt;
      &lt;td&gt;拓展好，edge 可以携带拓展信息&lt;/td&gt;
      &lt;td&gt;拓展差，需要额外维护 meta 信息&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;注意，即使后续切换到 pull &amp;amp; push 混合模式，仍然可以使用该技术栈来解决，并且这时候，召回的扇出会变得非常小（用户收件箱+关注的几个大V的发件箱）。虽然这时候用 ByteGraph 性能也不赖，但是业务方需要维护多个游标（每路召回对应一个），实现起来仍然比较复杂。&lt;/p&gt;

&lt;h1 id=&quot;不可重复读幻读&quot;&gt;不可重复读、幻读&lt;/h1&gt;

&lt;p&gt;上述关注流场景是基于时间线排序，时间线的一个特点就是，数据写了，score 就不会改变（score 就是发文的 timestamp）。&lt;/p&gt;

&lt;p&gt;但是某些业务场景可能不是按照严格时间线，而是按照点赞数等互动行为的热度来排序，这时候 score 会经常变动。基于时间线通常使用 (start_time, count) 来拉数据，而基于热度通常是使用 offset、count，即游标的起始值以及读取的数量。&lt;/p&gt;

&lt;p&gt;那么对于此场景，使用上述任何一种方案，都可能导致缺数据、或者多数据的问题。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;本质上，这个问题类似数据库的不可重复读、幻读&lt;/strong&gt;。下面进行简单分析：&lt;/p&gt;

&lt;p&gt;如下图所示，第一刷返回 5 条之后，考虑以下几种情况：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;如果下面有其他 item 有 score 的变更，导致插在前 5 条之中；（不可重复读）&lt;/li&gt;
  &lt;li&gt;如果恰好有新增的 item 插入到前 5 条之中；（幻读）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;那么会导致第二刷返回的数据包含了第一刷的部分内容，并且漏掉了本该在第二刷返回的部分内容。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-9c5f9529f8bf850673e0325004182c0a_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这大概是数据库领域的经典问题了。&lt;/p&gt;

&lt;p&gt;数据库（这里指 InnoDB）是使用 MVCC 机制来解决上述问题的，即维护一个基于事务版本实现的视图，在可重复读隔离级别下，整个事务执行过程中看到的都是事务开始时的快照。&lt;/p&gt;

&lt;p&gt;而对于 ES 而言，也有类似问题。给定一个分页查询（ES 里称为 from、size，跟上述提到的 offset、count 一样一样的），ES 通常需要在每个分片中搜索得到 [0, size+from) 区间内的所有数据，然后在 coordinator 进行 merge 计算得到结果并返回。在进行分页查询期间，数据有任何变动的话，则可能会影响数据的完整性，出现上述提到的信息流一摸一样的问题。&lt;/p&gt;

&lt;p&gt;ES 的解决方案与 InnoDB 的思想类似，分页起始时，返回一个快照，由于 ES 没有事务的概念，因此该快照存在一个过期时间，由客户端来指定。这里将分页开始到快照结束称为一个会话，ES 保证在该会话期间，分页查询访问的是同一个快照。详细可以阅读 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after&quot;&gt;ES earch-after&lt;/a&gt;。&lt;/p&gt;

&lt;h2 id=&quot;解决方案&quot;&gt;解决方案&lt;/h2&gt;

&lt;p&gt;本文提供两个解决方案供参考。&lt;/p&gt;

&lt;h3 id=&quot;方案一&quot;&gt;方案一&lt;/h3&gt;

&lt;p&gt;预请求+一致性哈希+会话缓存&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;每次会话开始时，服务端生成一个 token，并预请求 X 条数据进行本地缓存。缓存 key 是 token，值是 X 条数据 ID；&lt;/li&gt;
  &lt;li&gt;接下来该用户的所有的 load more 行为，经过网关一致性哈希路由到同一台后端实例，读取内存中的缓存，进行分页操作；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这样的话，至少可以保证前 X 条数据从会话开始之后是稳定的。&lt;/p&gt;

&lt;p&gt;当然，也可以使用分布式缓存来替换上述的一致性哈希+单机缓存方案，可以根据性能需求灵活选择。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;局限性&lt;/strong&gt;: 集群实例数变化时路由会失效；首刷需要获取 X 条数据，X 过大时会影响首刷性能；当用户量很大时，会占用很大内存（或其他存储），影响性能；&lt;/p&gt;

&lt;p&gt;另外，通常来说，数据重复不可接受，而数据不完整用户很难感知。X 条数据之后，有可能会有重复问题，所以一般会走客户端消重来配合食用，数据不完整则无解。&lt;/p&gt;

&lt;h3 id=&quot;方案二&quot;&gt;方案二&lt;/h3&gt;

&lt;p&gt;从头拉数据+消重；参考选型 index_service+impression&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;每次会话开始时，服务端生成一个 token，token=会话起始时间戳；&lt;/li&gt;
  &lt;li&gt;没有 offset 的概念，服务端底层会从头拉取 index_service 的数据，只是携带 impression 上下文进行消重；&lt;/li&gt;
  &lt;li&gt;每一刷服务端返回 count 个数据，并上报 impression 消重；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;index_service 底层依赖 LMDB，可以应对大规模的数据集，因此对于拉取用户所需的那么几刷数据没有性能问题（因为大部分场景下所需召回倒排不会很多，例如文章评论，就只需要一个倒排）。&lt;/p&gt;

&lt;p&gt;当然，消重也可以走其他方案，比如客户端透传、服务端自己记录来实现。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-251c83c655bf26e1e96c3b89079bdb6f_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;业界方案&quot;&gt;业界方案&lt;/h1&gt;

&lt;h2 id=&quot;朋友圈&quot;&gt;朋友圈&lt;/h2&gt;

&lt;p&gt;https://cloud.tencent.com/developer/article/1168946&lt;/p&gt;

&lt;p&gt;注：猜测使用混合模式，并且注意每个用户能看到的朋友圈，其实是有数量上限的，因此收件箱必然是有截断的，而发件箱则是全量；&lt;/p&gt;

&lt;h2 id=&quot;twitter&quot;&gt;Twitter&lt;/h2&gt;

&lt;p&gt;https://blog.mi.hdm-stuttgart.de/index.php/2021/03/10/how-to-scale-real-time-tweet-delivery-architecture-at-twitter/&lt;/p&gt;

&lt;h1 id=&quot;小结&quot;&gt;小结&lt;/h1&gt;

&lt;p&gt;本文以关注流为例子，介绍了几种信息流的实现方案，并对比其优缺点，介绍了各自适合的应用场景。并归纳了信息流领域常见的幻读、不可重复读问题，参考数据库隔离级别实现的思路，提出对应的两个解决方案。&lt;/p&gt;
</description>
			<pubDate>Tue, 09 Aug 2022 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2022/08/09/%E4%BF%A1%E6%81%AF%E6%B5%81%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2022/08/09/%E4%BF%A1%E6%81%AF%E6%B5%81%E7%B3%BB%E7%BB%9F%E8%AE%BE%E8%AE%A1/</guid>
        
			<category>architecture</category>
        
        
		</item>
    
		<item>
			<title>浅谈 Golang 编译原理及其应用</title>
			<description>&lt;h1 id=&quot;浅谈-golang-编译原理及其应用&quot;&gt;浅谈 Golang 编译原理及其应用&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;TL;DR；本文简要介绍 Golang 编译的各个阶段干了什么，即从源文件到最终的机器码中间经历的过程。并从汇编代码、AST 入手介绍了相关的应用场景及实现原理。本文第一节深度参考了 &lt;a href=&quot;https://draveness.me/golang/docs/part1-prerequisite/ch02-compile/golang-lexer-and-parser/&quot;&gt;解析器眼中的 Go 语言&lt;/a&gt;，也非常推荐有时间的读者阅读原文。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;编译原理&quot;&gt;编译原理&lt;/h1&gt;

&lt;p&gt;编译器的前端一般承担着词法分析、语法分析、类型检查和中间代码生成几部分工作.&lt;/p&gt;

&lt;p&gt;编译器后端主要负责目标代码的生成和优化，也就是将中间代码翻译成目标机器能够运行的二进制机器码。&lt;/p&gt;

&lt;h2 id=&quot;词法分析&quot;&gt;词法分析&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;过程：源代码文件 =&amp;gt; 词法分析（lexer） =&amp;gt; Token 序列&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;词法分析：词法分析是将字符序列转换为 Token 序列的过程。&lt;/strong&gt;源代码在计算机看来其实就是一个由字符组成的、无法被理解的字符串，所有的字符在计算器看来并没有什么区别，为了理解这些字符我们需要做的第一件事情就是将字符串分组，即转换为 Token 序列，这能够降低理解字符串的成本，简化源代码的分析过程，这个转换的过程就是词法分析。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;func (s *scanner) next() {
    // ...
    s.stop()
    startLine, startCol := s.pos()for s.ch == &apos; &apos; || s.ch == &apos;\t&apos; || s.ch == &apos;\n&apos; &amp;amp;&amp;amp; !nlsemi || s.ch == &apos;\r&apos; {
        s.nextch()
    }

    s.line, s.col = s.pos()
    s.blank = s.line &amp;gt; startLine || startCol == colbase
    s.start()
    if isLetter(s.ch) || s.ch &amp;gt;= utf8.RuneSelf &amp;amp;&amp;amp; s.atIdentChar(true) {
        s.nextch()
        s.ident()return
    }
    switch s.ch {
    case -1:
        s.tok = _EOF
    case &apos;0&apos;, &apos;1&apos;, &apos;2&apos;, &apos;3&apos;, &apos;4&apos;, &apos;5&apos;, &apos;6&apos;, &apos;7&apos;, &apos;8&apos;, &apos;9&apos;:
        s.number(false)
    // ...
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;上述节选的代码是遍历源文件不断获取最新的字符，将字符通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd/compile/internal/syntax.source.nextch&lt;/code&gt; 方法追加到 scanner 持有的缓冲区中，并在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd/compile/internal/syntax.scanner.next&lt;/code&gt; 中对最新的字符进行词法分析的过程。&lt;/p&gt;

&lt;h2 id=&quot;语法分析&quot;&gt;语法分析&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;过程：Token 序列 =&amp;gt; 语法分析 =&amp;gt; AST&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;语法分析：&lt;/strong&gt;&lt;strong&gt;语法分析&lt;/strong&gt;&lt;strong&gt;是根据某种特定的文法，对 Token 序列构成的输入文本进行分析并确定其语法结构的过程。&lt;/strong&gt;语法分析由文法、分析方法构成。文法描述了语法的组成，分析方法则是解析文法的过程。&lt;/p&gt;

&lt;h3 id=&quot;文法&quot;&gt;文法&lt;/h3&gt;

&lt;p&gt;上下文无关文法是用来形式化、精确描述某种编程语言的工具，我们能够通过文法定义一种语言的语法，它主要包含一系列用于转换字符串的生产规则（Production rule）。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://go.dev/ref/spec#Lexical_elements&quot;&gt;The Go Programming Language Specification（Go 语言说明书）&lt;/a&gt;使用 &lt;a href=&quot;https://zh.m.wikipedia.org/zh/扩展巴科斯范式&quot;&gt;EBNF 范式&lt;/a&gt;对 Golang 语法进行描述。&lt;/p&gt;

&lt;p&gt;下面是使用 EBNF 范式对 EBNF 本身进行描述：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Plain&quot;&gt;Production  = production_name &quot;=&quot; [ Expression ] &quot;.&quot; .
Expression  = Alternative { &quot;|&quot; Alternative } .
Alternative = Term { Term } .
Term        = production_name | token [ &quot;…&quot; token ] | Group | Option | Repetition .
Group       = &quot;(&quot; Expression &quot;)&quot; .
Option      = &quot;[&quot; Expression &quot;]&quot; .
Repetition  = &quot;{&quot; Expression &quot;}&quot; .
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;生产规则是由 Term 与下述操作符组成：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Plain&quot;&gt;|   alternation
()  grouping
[]  option (0 or 1 times)
{}  repetition (0 to n times)
&quot;&quot;  string
.   terminator symbol
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/cmd/compile/internal/syntax/parser.go&lt;/code&gt; 文件中描述了 Go 语言文法的生产规则：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;SourceFile = PackageClause &quot;;&quot; { ImportDecl &quot;;&quot; } { TopLevelDecl &quot;;&quot; } .
PackageClause  = &quot;package&quot; PackageName .
PackageName    = identifier .

ImportDecl       = &quot;import&quot; ( ImportSpec | &quot;(&quot; { ImportSpec &quot;;&quot; } &quot;)&quot; ) .
ImportSpec       = [ &quot;.&quot; | PackageName ] ImportPath .
ImportPath       = string_lit .

TopLevelDecl  = Declaration | FunctionDecl | MethodDecl .
Declaration   = ConstDecl | TypeDecl | VarDecl .
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;从上述 SourceFile 相关的生产规则我们可以看出，每一个文件都包含一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package&lt;/code&gt; 的定义以及可选的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;import&lt;/code&gt; 声明和其他的顶层声明（TopLevelDecl），每一个 SourceFile 在编译器中都对应一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd/compile/internal/syntax.File&lt;/code&gt; 结构体，可以从该定义中轻松找到两者的联系：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;type File struct {
    Pragma   Pragma
    PkgName  *Name
    DeclList []Decl
    Lines    uint
    node
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;分析方法&quot;&gt;分析方法&lt;/h3&gt;

&lt;p&gt;语法分析的分析方法一般分为自顶向下和自底向上两种，这两种方式会使用不同的方式对输入的 Token 序列进行推导：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Top-down_parsing&quot;&gt;自顶向下分析&lt;/a&gt;：解析器会从开始符号分析，通过新加入的字符判断应该使用什么生产规则展开当前的输入流；
    &lt;ul&gt;
      &lt;li&gt;LL 使用自顶向下分析方法&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Bottom-up_parsing&quot;&gt;自底向上分析&lt;/a&gt;：解析器会从输入流开始，维护一个栈用于存储未被归约的符号，当栈中符号满足规约条件，则会规约成对应的生产规则；
    &lt;ul&gt;
      &lt;li&gt;LR(0)、SLR、LR(1) 和 LALR(1) 都是使用了自底向上的处理方式；&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Lookahead&quot;&gt;Lookahead&lt;/a&gt;：在不同生产规则发生冲突时，解析器需要通过预读一些 Token 判断当前应该用什么生产规则对输入流进行展开或者归约，例如在 LALR(1) 文法中，需要预读一个 Token 保证出现冲突的生产规则能够被正确处理。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go 语言的解析器使用了 LALR(1) 的文法来解析词法分析过程中输出的 Token 序列，得到 AST。&lt;/p&gt;

&lt;h3 id=&quot;ast&quot;&gt;AST&lt;/h3&gt;

&lt;p&gt;语法分析器最终会使用不同的结构体来构建抽象语法树中的节点，File 是根结点。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Plain&quot;&gt;type File struct {
    Pragma   Pragma
    PkgName  *Name
    DeclList []Decl
    Lines    uint
    node
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;src/cmd/compile/internal/syntax/nodes.go&lt;/code&gt; 文件中也定义了其他节点的结构体，比如函数声明的结构：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;type (
    Decl interface {
        Node
        aDecl()
    }

    FuncDecl struct {
        Attr   map[string]bool
        Recv   *Field
        Name   *Name
        Type   *FuncType
        Body   *BlockStmt
        Pragma Pragma
        decl
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;这里草草了解对 AST 的说明，在「应用」一节会举一个有关 AST 的例子，这里顺便推荐一个&lt;a href=&quot;https://yuroyoro.github.io/goast-viewer/index.html&quot;&gt;在线解析 AST&lt;/a&gt; 的工具。&lt;/p&gt;

&lt;p&gt;另外，非常推荐一本详细讲解 AST 的书籍，有兴趣的读者可以查阅 https://chai2010.cn/go-ast-book/index.html。&lt;/p&gt;

&lt;h2 id=&quot;类型检查&quot;&gt;类型检查&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;过程： AST =&amp;gt; 类型检查 =&amp;gt; 关键字改写的 AST&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;得到 AST 之后，对象类型、对象值已经出来了，这时候执行类型检查是很方便的事情；如果有任何类型不匹配，则会在该阶段抛出异常，&lt;strong&gt;这个过程叫做静态类型检查&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;与静态类型检查互补的是动态类型检查，例如我们在代码中会将 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;interface{}&lt;/code&gt; 转换成具体类型，如果无法发生转换就会发生程序崩溃，那么这里实际上&lt;strong&gt;涉及到动态类型检查&lt;/strong&gt;。动态检查会依赖编译期间得到的类型信息。&lt;/p&gt;

&lt;p&gt;另外，执行类型检查的同时，会对内建函数进行一些替换操作，例如 make =&amp;gt;  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime.makeslice&lt;/code&gt; 或者 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime.makechan&lt;/code&gt; 。&lt;/p&gt;

&lt;h2 id=&quot;中间代码生成&quot;&gt;中间代码生成&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;过程：AST =&amp;gt; 并发编译所有函数 =&amp;gt; SSA 等代码优化 =&amp;gt; 中间代码&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;生成中间代码之前，编译器还需要替换抽象语法树中节点的一些元素，即编程语言给开发者的语法糖。该操作将一些关键字和内建函数转换成函数调用，例如： 将 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;panic&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;recover&lt;/code&gt; 两个内建函数转换成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime.gopanic&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime.gorecover&lt;/code&gt; 两个真正运行时函数，而关键字 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;new&lt;/code&gt; 也会被转换成调用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtime.newobject&lt;/code&gt; 函数。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-99e0b46092ed08986a1f632165001ae1_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;经过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;walk&lt;/code&gt; 系列函数的处理之后，抽象语法树就不会改变了，Go 语言的编译器会将 AST 转换为具备 SSA 特性的中间代码。&lt;/p&gt;

&lt;p&gt;我们能在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GOSSAFUNC=func_name go build main.go&lt;/code&gt; 命令生成的文件中，看到指定函数 func_name，每一轮处理后的中间代码。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-ae1962e96cfb4f231a5b569116be9de6_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;中间代码是一种更接近机器语言的表示形式，对中间代码的优化和分析相比直接分析高级编程语言更容易。&lt;/p&gt;

&lt;h2 id=&quot;机器码生成&quot;&gt;机器码生成&lt;/h2&gt;

&lt;p&gt;SSA 输出结果跟最后生成的汇编代码已经非常相似了，随后调用的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd/compile/internal/gc.Progs.Flush&lt;/code&gt; 会使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd/internal/obj&lt;/code&gt; 包中的汇编器将 SSA 转换成汇编代码。&lt;/p&gt;

&lt;p&gt;我们可以使用命令生成汇编代码，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GOOS=linux GOARCH=amd64 go tool compile -S main.go&lt;/code&gt;。这种方式生产的并非标准的汇编代码，而是上节提到的中间代码。不过进行分析的话也可以使用。&lt;/p&gt;

&lt;p&gt;如果想获得更准确，并且更加标准化的汇编代码，可以使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;go tool objdump -s &amp;lt;interesting_function_&amp;gt; main&lt;/code&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;usage: go tool objdump [-S] [-gnu] [-s symregexp] binary [start end]
  -S    print Go code alongside assembly
  -gnu  print GNU assembly next to Go assembly (where supported)
  -s string
        only dump symbols matching this regexp
&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;自举&quot;&gt;自举&lt;/h2&gt;

&lt;p&gt;自举的定义是，使用 Golang 编写的程序来构建 Golang 编写的程序。实际上，要构建 x ≥ 5 的 Go 1.x，必须在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$GOROOT_BOOTSTRAP&lt;/code&gt; 中已经安装 Go 1.4（或更高版本）。而 Go 1.4 本身是依赖 C 语言的。所以这个自举过程其实也并不神秘。&lt;/p&gt;

&lt;h1 id=&quot;应用&quot;&gt;应用&lt;/h1&gt;

&lt;h2 id=&quot;机器码&quot;&gt;机器码&lt;/h2&gt;

&lt;h3 id=&quot;monkey-patch&quot;&gt;Monkey Patch&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;p&gt;原理：获取 from 的函数地址 =&amp;gt; 将跳转 to 的汇编指令替换 from 的函数体&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Monkey Patch 是一个实现函数替换的工具，通常用在本地测试 Mock 数据的场景。这里简单介绍函数替换的实现原理。&lt;/p&gt;

&lt;p&gt;举个例子，对于下述程序，我们的目标是实现某个函数&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replace(a, b)&lt;/code&gt;，使得调用函数 a 的时候，实际上运行函数 b。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;package main

func a() int { return 1 }

func b() int { return 2 }

func main() {
  // replace(a, b)
  print(a())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;先看其汇编代码：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-c0a607db20abb989db44f2529e843bcf_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;再强调一下，我们的目标是调用函数 a 的时候，实际上调用 b。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;可以看到 0x2000-0x2009 是函数 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.a&lt;/code&gt; 的函数体。&lt;/p&gt;

&lt;p&gt;那么，要达到目标，需要把下述汇编代码替换到 0x2000-0x2009 的内存位置。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Assembly&quot;&gt;mov rdx, main.b.f
jmp [rdx]
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;因此，将汇编对应的机器码（字节表示，可以使用&lt;a href=&quot;http://shell-storm.org/online/Online-Assembler-and-Disassembler/?inst=mov+rdx%2C+0x1&amp;amp;arch=x86-64#assembly&quot;&gt;在线汇编器&lt;/a&gt;进行转换）强制拷贝到函数&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;main.a&lt;/code&gt; 的内存位置即可。&lt;/p&gt;

&lt;p&gt;详细可以查阅原文，这里只是简单介绍一下实现的过程。&lt;/p&gt;

&lt;p&gt;https://berryjam.github.io/2018/12/golang%E6%9B%BF%E6%8D%A2%E8%BF%90%E8%A1%8C%E6%97%B6%E5%87%BD%E6%95%B0%E4%BD%93%E5%8F%8A%E5%85%B6%E5%8E%9F%E7%90%86/&lt;/p&gt;

&lt;h3 id=&quot;性能分析&quot;&gt;性能分析&lt;/h3&gt;

&lt;p&gt;我们借助 pprof 工具，对性能瓶颈进行分析。找到到更具体的问题代码块，可以再通过汇编分析等方法，定位到影响性能的代码。&lt;/p&gt;

&lt;p&gt;下图是嵌套指针导致的指令依赖，而无法充分利用指令并行的一个例子。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-0410a963c4eb93585db3c156e77aeb26_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;通过汇编指令分析性能瓶颈，帮助机器更好地优化，例如：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;充分利用指令并行&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;避免不可预测的分支&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;提高指令缓存命中率&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;详细可以参阅 CSAPP 的第五章，之前记了相关的一段笔记：&lt;a href=&quot;https://sarkerson.github.io/2021/08/22/优化程序性能-CSAPP-笔记&quot;&gt;第五章 优化程序性能&lt;/a&gt;&lt;/p&gt;

&lt;h2 id=&quot;ast-1&quot;&gt;AST&lt;/h2&gt;

&lt;h3 id=&quot;魔改代码&quot;&gt;魔改代码&lt;/h3&gt;

&lt;p&gt;有时候，你需要批量修改代码。例如，你需要给很多个 Handler 添加一个公有的 BaseCheck 逻辑。&lt;/p&gt;

&lt;p&gt;这时候，可选的做法是，加一个 BaseHandler 实现改 BaseCheck 方法，让其他 Handler 嵌套 BaseHandler 从而继承该公有方法。然后在所有的 Handler 的 check 方法中，调用继承过来的 BaseCheck 方法，即新增下述高亮部分的代码。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;package handler

import &quot;package_path/biz/base_handler&quot;

type HandlerA struct {
    *base_handler.BaseHandler
}

func (h *HandlerA) check() error {
    // other checks by handler A
    // other checks by handler A
    if err := h.BaseCheck(); err != nil { // inherit from BaseHandler
        return err
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;问题来了，当 handler 非常多（大型项目可能有几十上百个）时，手动给每个 handler 文件添加上述逻辑显然不是很爽。&lt;/p&gt;

&lt;p&gt;回顾一下上面提到的 AST，我们可以利用 AST 解析整个 go 文件，得到所有的节点。那么当我知道需要在哪些节点上新增代码，便可以写代码来生成这部分代码，我们把这个工具暂称 generator。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;这里面涉及两个步骤：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;识别目标节点&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;插入新节点（要新增的代码块）&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我们可以先手动对任何一个 handler 加这部分代码，然后观察其 AST，模仿该 AST 来编写 generator。上文提到一个&lt;a href=&quot;https://yuroyoro.github.io/goast-viewer/index.html&quot;&gt;在线解析 AST&lt;/a&gt; 的工具，我们把已经写好的 handler 贴进去，找到我们感兴趣的那部分逻辑。&lt;/p&gt;

&lt;p&gt;例如你要给每个 Handler 的声明添加 *base_handler.BaseHandler 这个 field，我们来看对应的代码以及 AST：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;type Handler struct {
        *base_handler.BaseHandler
        reqCtx *request.RequestContext
}
     0  *ast.File {
     3  .  Name: *ast.Ident {
     5  .  .  Name: &quot;xx_handler&quot;
     7  .  }
     8  .  Decls: []ast.Decl (len = 5) {
     9  .  .  0: *ast.GenDecl {}
    95  .  .  1: *ast.GenDecl {
    96  .  .  .  Doc: nil
    98  .  .  .  Tok: type
   100  .  .  .  Specs: []ast.Spec (len = 1) {
   101  .  .  .  .  0: *ast.TypeSpec {
   102  .  .  .  .  .  Doc: nil
   103  .  .  .  .  .  Name: *ast.Ident {
   105  .  .  .  .  .  .  Name: &quot;Handler&quot;
   106  .  .  .  .  .  .  Obj: *ast.Object {
   107  .  .  .  .  .  .  .  Kind: type
   108  .  .  .  .  .  .  .  Name: &quot;Handler&quot;
   112  .  .  .  .  .  .  }
   113  .  .  .  .  .  }
   115  .  .  .  .  .  Type: *ast.StructType {
   117  .  .  .  .  .  .  Fields: *ast.FieldList {
   119  .  .  .  .  .  .  .  List: []*ast.Field (len = 2) {
   120  .  .  .  .  .  .  .  .  0: *ast.Field {
   121  .  .  .  .  .  .  .  .  .  Doc: nil
   122  .  .  .  .  .  .  .  .  .  Names: nil
   123  .  .  .  .  .  .  .  .  .  Type: *ast.StarExpr {
   124  .  .  .  .  .  .  .  .  .  .  Star: foo:15:2
   125  .  .  .  .  .  .  .  .  .  .  X: *ast.SelectorExpr {
   126  .  .  .  .  .  .  .  .  .  .  .  X: *ast.Ident {
   127  .  .  .  .  .  .  .  .  .  .  .  .  NamePos: foo:15:3
   128  .  .  .  .  .  .  .  .  .  .  .  .  Name: &quot;base_handler&quot;
   129  .  .  .  .  .  .  .  .  .  .  .  .  Obj: nil
   130  .  .  .  .  .  .  .  .  .  .  .  }
   131  .  .  .  .  .  .  .  .  .  .  .  Sel: *ast.Ident {
   132  .  .  .  .  .  .  .  .  .  .  .  .  NamePos: foo:15:16
   133  .  .  .  .  .  .  .  .  .  .  .  .  Name: &quot;BaseHandler&quot;
   134  .  .  .  .  .  .  .  .  .  .  .  .  Obj: nil
   135  .  .  .  .  .  .  .  .  .  .  .  }
   136  .  .  .  .  .  .  .  .  .  .  }
   137  .  .  .  .  .  .  .  .  .  }
   138  .  .  .  .  .  .  .  .  .  Tag: nil
   139  .  .  .  .  .  .  .  .  .  Comment: nil
   140  .  .  .  .  .  .  .  .  }
   141  .  .  .  .  .  .  .  .  1: *ast.Field {
   142  .  .  .  .  .  .  .  .  // ...
   183  .  .  }
&lt;/code&gt;&lt;/pre&gt;

&lt;h4 id=&quot;更改目标节点&quot;&gt;更改目标节点&lt;/h4&gt;

&lt;p&gt;观察上述 AST，可以发现 *ast.File 这个源文件节点下面，声明部分 Decls 就有 Handler 的声明语句。这个声明中有一个 Fields 节点，就是 Handler 的字段定义。&lt;/p&gt;

&lt;p&gt;根据该 AST 可以很方便写出目标节点的识别代码，本文使用了 “golang.org/x/tools/go/ast/astutil” 这个库，相比于 “go/ast” 库，astutil 支持获取节点的父节点等功能。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;func main() {
    fset := token.NewFileSet()
    file, err := parser.ParseFile(fset, &quot;input.go&quot;, nil, parser.ParseComments)
    if err != nil {
        log.Fatal(err)
        return
    }

    astutil.Apply(file, nil, func(c *astutil.Cursor) bool {
        n := c.Node()
        
        switch x := n.(type) {
        
        case *ast.File:
        
        for _, decl := range x.Decls {
            if genDecl, ok := decl.(*ast.GenDecl); ok {
                for _, spec := range genDecl.Specs {
                    switch dx := spec.(type) {
                    case *ast.TypeSpec:
                        // 下面判断是 type struct 声明，并且名称以 Handler 结尾
                        if dx.Name != nil &amp;amp;&amp;amp; strings.HasSuffix(dx.Name.Name, &quot;Handler&quot;) {
                            if stype, ok := dx.Type.(*ast.StructType); ok {
                                // checkIfAddBaseHandlerDone 判断是否已经插入过该 field
                                if !checkIfAddBaseHandlerDone(stype.Fields) {
                                    // 在 fields.List 中插入 baseHandler 的 field
                                    stype.Fields.List = append([]*ast.Field{newAddBaseHandlerField()}, stype.Fields.List...)
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    printer.Fprint(os.Stdout, fset, file)
}

func newAddBaseHandlerField() *ast.Field {
    return &amp;amp;ast.Field{
        Names: nil,
        Type: &amp;amp;ast.StarExpr{
            X: &amp;amp;ast.SelectorExpr{
                X: &amp;amp;ast.Ident{
                    Name: &quot;base_handler&quot;,
                },
                Sel: &amp;amp;ast.Ident{
                    Name: &quot;BaseHandler&quot;,
                },
            },
        },
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;expr&quot;&gt;expr&lt;/h3&gt;

&lt;p&gt;expr 自己实现了一套语法，因此也有自己的一整套编译过程。只不过得到了 AST 之后，转换为命令+参数的形式用栈来模拟程序的执行。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;首先是 source code =&amp;gt; token，使用状态机自底向上进行分析，&lt;a href=&quot;https://github.com/antonmedv/expr/blob/master/parser/lexer/lexer.go#L11&quot;&gt;expr/parser/lexer/lexer.go&lt;/a&gt;，下面是状态机的部分代码，当匹配到期望的规则时，则使用对应的生产规则跳转到下一个状态，因此是自顶向下分析，输出预设的 token 列表。&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;func root(l *lexer) stateFn {
        switch r := l.next(); {
        case r == eof:
                l.emitEOF()
                return nil
        case IsSpace(r):
                l.ignore()
                return root
        case r == &apos;\&apos;&apos; || r == &apos;&quot;&apos;:
                l.scanString(r)
                str, err := unescape(l.word())
                if err != nil {
                        l.error(&quot;%v&quot;, err)
                }
                l.emitValue(String, str)
        case &apos;0&apos; &amp;lt;= r &amp;amp;&amp;amp; r &amp;lt;= &apos;9&apos;:
                l.backup()
                return number
        default:
                return l.error(&quot;unrecognized character: %#U&quot;, r)
        }
        return root
}
&lt;/code&gt;&lt;/pre&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;其次，token =&amp;gt; AST，当 token 确定之后，可以很方便构建一棵树。个人觉得这部分代码写得有点挫，详细可以看：&lt;a href=&quot;https://github.com/antonmedv/expr/blob/master/parser/parser.go#L145&quot;&gt;expr/parser/parser.go&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;下面是 AST =&amp;gt; 程序命令的例子，三目的条件运算符的解析过程：&lt;a href=&quot;https://github.com/antonmedv/expr/blob/master/compiler/compiler.go#L623&quot;&gt;expr/compile/compiler.go&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;func (c *compiler) ConditionalNode(node *ast.ConditionalNode) {
        c.compile(node.Cond)
        otherwise := c.emit(OpJumpIfFalse, c.placeholder()...)

        c.emit(OpPop)
        c.compile(node.Exp1)
        end := c.emit(OpJump, c.placeholder()...)

        c.patchJump(otherwise)
        c.emit(OpPop)
        c.compile(node.Exp2)

        c.patchJump(end)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;ol&gt;
  &lt;li&gt;下面是执行函数的相关代码，&lt;a href=&quot;https://github.com/antonmedv/expr/blob/master/vm/vm.go#L281&quot;&gt;expr/vm/vm.go&lt;/a&gt;，模拟了计算机取值、执行的过程。会将存在 bytecode 中的指令取出，并从 constant 中取出指令对应的参数进行执行。由于指令都是预设的，因此 constant 只需要根据预设进行出栈入栈即可。&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;op := vm.bytecode[vm.pp]
switch op {
case OpCall:
        call := vm.constant().(Call)
        in := make([]reflect.Value, call.Size)
        for i := call.Size - 1; i &amp;gt;= 0; i-- {
                param := vm.pop()
                if param == nil &amp;amp;&amp;amp; reflect.TypeOf(param) == nil {
                        // In case of nil value and nil type use this hack,
                        // otherwise reflect.Call will panic on zero value.
                        in[i] = reflect.ValueOf(&amp;amp;param).Elem()
                } else {
                        in[i] = reflect.ValueOf(param)
                }
        }
        out := FetchFn(env, call.Name).Call(in)
        if len(out) == 2 &amp;amp;&amp;amp; out[1].Type() == errorType &amp;amp;&amp;amp; !out[1].IsNil() {
                return nil, out[1].Interface().(error)
        }
        vm.push(out[0].Interface())
}
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&quot;gocover&quot;&gt;gocover&lt;/h3&gt;

&lt;p&gt;go test coverage 采用的方法是在生成的 AST 上直接进行编辑，注入统计逻辑，然后根据编辑后的 AST 反向生成代码。&lt;/p&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://xargin.com/ast/&quot;&gt;xargin.com&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;a href=&quot;https://draveness.me/golang/docs/part1-prerequisite/ch02-compile/golang-compile-intro/&quot;&gt;Go 语言编译过程概述&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Mon, 01 Aug 2022 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2022/08/01/%E6%B5%85%E8%B0%88-Golang-%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2022/08/01/%E6%B5%85%E8%B0%88-Golang-%E7%BC%96%E8%AF%91%E5%8E%9F%E7%90%86%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8/</guid>
        
			<category>golang</category>
        
			<category>os</category>
        
        
		</item>
    
		<item>
			<title>二分搜索，你还在用三个模版？一个就够了！</title>
			<description>&lt;h1 id=&quot;二分搜索你还在用三个模版一个就够了&quot;&gt;二分搜索，你还在用三个模版？一个就够了！&lt;/h1&gt;

&lt;p&gt;One Template for All Binary Search Problems&lt;/p&gt;

&lt;h2 id=&quot;背景&quot;&gt;背景&lt;/h2&gt;

&lt;p&gt;首先，面对二分搜索，我们往往会碰到以下疑问：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; 初始值？&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;循环条件是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left &amp;lt; right&lt;/code&gt; 还是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left &amp;lt;= right&lt;/code&gt; ？&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;如何更新&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; ？&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left = mid&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left = mid + 1&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right = mid&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right = mid — 1&lt;/code&gt; ?&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;结束时要选&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 还是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; ？&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;另外，二分搜索有多个场景，&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;标准的二分查找&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;二分查找左边界&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;二分查找右边界&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;二分查找极值点&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对于上述多个场景，网上也有相关的模板，但是很多都是使用多个模版来针对性解决不同场景的应用问题。&lt;/p&gt;

&lt;p&gt;但是，其实只要&lt;strong&gt;一个模板&lt;/strong&gt;就够了！可能有很多大佬在用，本文只作总结，并&lt;strong&gt;提炼出统一的方法来应对上述不同场景&lt;/strong&gt;。&lt;/p&gt;

&lt;h2 id=&quot;模版&quot;&gt;模版&lt;/h2&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;binary_search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;array&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;pass&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;search_space_&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;几个注意点：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; 是解空间的闭区间&lt;/li&gt;
  &lt;li&gt;循环结束时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**left**&lt;/code&gt; &lt;strong&gt;是满足&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**condition**&lt;/code&gt; &lt;strong&gt;的最小值&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt;的取值是有讲究的，下面会提到&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;应用&quot;&gt;应用&lt;/h2&gt;

&lt;p&gt;下面使用这一个模板来解决几个常见场景的问题。&lt;/p&gt;

&lt;h3 id=&quot;寻找插入点&quot;&gt;寻找插入点&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/search-insert-position/&quot;&gt;https://leetcode.cn/problems/search-insert-position&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;searchInsert&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;  

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;几个注意点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right = len(nums)&lt;/code&gt;：上面提到，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; 是解空间的闭区间；当 target 比所有值大时，必定是落在索引&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;len(nums)&lt;/code&gt;上面。&lt;/li&gt;
  &lt;li&gt;返回值&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt;是满足&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nums[mid] ≥ target&lt;/code&gt;的最小值，即要么等于要么大于&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;寻找左边界&quot;&gt;寻找左边界&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/&quot;&gt;力扣&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# ====== left bound ===========
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;几个注意点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;返回值&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt;是满足&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nums[mid] ≥ target&lt;/code&gt;的最小值：即要么找到等于 target 的最小值，返回该左边界；要么找到大于 target 的最小值，返回 -1&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;寻找右边界&quot;&gt;寻找右边界&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/&quot;&gt;力扣&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# ====== right bound ===========
&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;几个注意点：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right = len(nums)&lt;/code&gt;：上面提到，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; 是解空间的闭区间；当 target 比所有值大时，必定是落在索引&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;len(nums)&lt;/code&gt;上面。&lt;/li&gt;
  &lt;li&gt;返回值&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt;是满足&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nums[mid] &amp;gt; target&lt;/code&gt;的最小值，因此解是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left-1&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;if left == 0&lt;/code&gt; 的判断：因为返回值是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left-1&lt;/code&gt; ，因此多了这么一步判断&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;寻找极值点&quot;&gt;寻找极值点&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/find-peak-element/&quot;&gt;力扣&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;findPeakElement&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;注意点：返回值&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt;是满足&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nums[mid] &amp;gt; nums[mid+1]&lt;/code&gt;的最小值&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;综合应用题&quot;&gt;综合应用题&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/search-in-rotated-sorted-array-ii/&quot;&gt;力扣&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;寻找分割点：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 是满足条件的最小值，因此我们找的是右半段的左边界，并且由此计算左半段的右边界，重新赋值给&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt;；&lt;/li&gt;
  &lt;li&gt;得到两段为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[start, left]&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[left + 1, end]&lt;/code&gt; ；&lt;/li&gt;
  &lt;li&gt;判断 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;target&lt;/code&gt; 所在的段，并进行普通二分搜索&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-python highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;search&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;List&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;  
    &lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;n&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# 1. 寻找分割点（小于 nums[start] 的第一个值）
&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# 1.1. 如果存在分割点，则将 left 作为左半段的右边界
&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;start&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;  
    &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# 2. 普通二分搜索
&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;//&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;right&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
            &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;mid&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;

    &lt;span class=&quot;c1&quot;&gt;# 2.2. 小插曲，通过测试用例排除  
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 出现的原因是上面进行了 left, right = left + 1, end 的赋值  
&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;# 导致 left &amp;gt; right，因此需要作此判断
&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;left&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;  
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;True&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;nums&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;left&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;target&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;结论&quot;&gt;结论&lt;/h2&gt;

&lt;p&gt;记住下面两句话，重新刷一下二分搜索，你会发现如鱼得水。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;left&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;right&lt;/code&gt; 是解空间的闭区间&lt;/li&gt;
  &lt;li&gt;循环结束时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**left**&lt;/code&gt; &lt;strong&gt;是满足&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;**condition**&lt;/code&gt; &lt;strong&gt;的最小值&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Fri, 20 May 2022 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2022/05/20/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2022/05/20/%E4%BA%8C%E5%88%86%E6%90%9C%E7%B4%A2/</guid>
        
			<category>algorithm</category>
        
        
		</item>
    
		<item>
			<title>GraphQL A Backend Engineer&apos;s Perspective</title>
			<description>&lt;h1 id=&quot;graphqla-backend-engineers-perspective&quot;&gt;GraphQL：A Backend Engineer’s Perspective&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;TLDR；本文是一个对 BFF 思想的学习与调研，也对业界经常用来实现 BFF 的 GraphQL 进行一些介绍。本文重点在于对业界方案进行的调研与小结，并在文末总结一种可以实践的 BFF 思路。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;概述&quot;&gt;概述&lt;/h1&gt;

&lt;p&gt;有些人觉得 GraphQL 与微服务思想相悖，其观点在于&lt;a href=&quot;https://draveness.me/graphql-microservice/&quot;&gt;中心化与去中心化&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;但本人认为，GraphQL 思想与微服务（确切来说是领域服务）是不谋而合的，服务端可以更加面向领域对象编程，而非面向场景编程，因为借鉴该思想，服务端可以省去了维护面向场景的聚合逻辑，可以更加关注领域服务的开发。&lt;/p&gt;

&lt;h1 id=&quot;graphql-vs-restful&quot;&gt;GraphQL vs RESTful&lt;/h1&gt;

&lt;p&gt;这里简单介绍一下 GraphQL 的思路。在 Web 服务器中，RESTful 应该是最常见的规范。在这里通过简单的对比 RESTful 与 GraphQL 的区别，来介绍什么是 GraphQL，以及为什么需要它。&lt;/p&gt;

&lt;p&gt;下面举了一个简单的例子，一个页面需要获取多个数据来进行渲染：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;左图是经典的 RestFul 的做法，请求多个接口进行拼凑；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;右图是 GraphQL，只有一个 path，通过类似 SQL 思想的 query 来获取数据；&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;因此 GraphQL 的名字非常生动形象，抽象了从多种异构存储或者服务中获取数据的过程。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-5f8355edc68df2b74d23a494c810a733_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;下面简单介绍 GraphQL 对于 RestFul 的一些优劣。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;GraphQL 优劣讨论&lt;/th&gt;
      &lt;th&gt;优点&lt;/th&gt;
      &lt;th&gt;缺点&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;宏观&lt;/td&gt;
      &lt;td&gt;服务端倾向于提供单一职责服务（微服务+领域驱动设计）&lt;/td&gt;
      &lt;td&gt;URI 对应资源路径，传统的 RestFul 对每个场景使用单独的 URI 获取对应资源&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;开发体验-前端&lt;/td&gt;
      &lt;td&gt;只需要请求一个接口，并且请求参数是直观的，按需取数后端自动处理路由逻辑，前端不需要执行多个请求有开源工具自动生成接口文档，不需要担心接口文档没有维护、更新&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;开发体验-服务端&lt;/td&gt;
      &lt;td&gt;更加专注领域服务的构建，而不是面向场景编程，提高领域层逻辑的复用不需要关注接口应该拆分还是聚合的问题，职责交给网关不需要配置很多路由&lt;/td&gt;
      &lt;td&gt;原生 GraphQL 对每种展示字段需要新开辟一个字段解决（或者将逻辑下沉到终端，但是又缺少了灵活性）对于复杂业务，数据图可能非常大&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;性能&lt;/td&gt;
      &lt;td&gt;避免返回不必要字段耗费带宽避免 n+1 问题（fetch list + n * get(item)）&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;稳定性&lt;/td&gt;
      &lt;td&gt;内容聚合大大减少了 API 请求的次数，比如数据字段可选大大减少无用流量的传输schema 文件中的强类型，极大的降低程序 crash 的风险。&lt;/td&gt;
      &lt;td&gt;没法基于场景进行以下监控或者操作，只能基于字段超时控制：不同场景下，相同字段超时可能存在区别流量监控：基于场景的流量监控是刚需熔断机制：基于字段的熔断可能导致误伤&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;鉴权&lt;/td&gt;
      &lt;td&gt;GraphQL 内置的 Directives 模块可以进行权限控制，包括字段级别，对象级别，接口级别，颗粒度非常细；RESTful 正常情况下只能做到路由级别的权限控制，接口里面的字段难以进行分级控制；&lt;/td&gt;
      &lt;td&gt; &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;容灾&lt;/td&gt;
      &lt;td&gt;CDN：同样可以实现基于 query 的缓存&lt;/td&gt;
      &lt;td&gt;限流：基于场景的限流是刚需&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h1 id=&quot;现有问题&quot;&gt;现有问题&lt;/h1&gt;

&lt;p&gt;下面列举了一些现有开发中常见的问题，这些问题可以直接使用 GraphQL，或者借鉴 GraphQL 的思路来解决。&lt;/p&gt;

&lt;h2 id=&quot;前后端协同&quot;&gt;前后端协同&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;微服务演进过程中，后端服务/接口骤增，客户端/前端开发需求时，单个页面进行多次请求后端服务，开发效率低下且数据加载时延较高，页面偏卡顿；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;前端期望接口自由请求数据以进行快速迭代，而不需要等待服务端将新接口上线（对于那些已经有打包逻辑的领域对象）；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;服务端的数据模型在版本迭代中，会越来越膨胀，因为它相当于多个版本数据的一个并集，不同版本使用数据模型中的不同字段；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;服务端往往在适配客户端/前端不同需求时，需要对数据进行加工，导致服务端掺杂UI逻辑，边界不清晰，不能更好的专注于领域逻辑；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;对于不同版本的客户端，服务端需要做非常多的兼容逻辑，导致维护成本较大；&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;微服务职责划分&quot;&gt;微服务职责划分&lt;/h2&gt;

&lt;p&gt;由于微服务架构的广泛应用，数量非常容易膨胀。这些微服务要么职能过于扁平，比如专门提供各种计数；要么职能过多，彼此之间存在交叠，比如多个内容服务提供了用户数据、作者数据、评论数据。&lt;/p&gt;

&lt;p&gt;另外，传统的面向场景编程，如果设计不当，很容易陷入领域对象与场景对象划分的纠结之中。比如说，文章详情页这个场景，返回给前端的场景对象，是否要直接使用文章的领域对象，通常来说两者应该隔离开，使用两个不同的对象进行处理。但是，对于没有经验的工程师来说，通常会陷入二者的界线划分的难题之中，&lt;strong&gt;一旦划分失误，那么领域层将避免不了耦合一些场景层的展示逻辑。&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;打包服务&quot;&gt;打包服务&lt;/h2&gt;

&lt;p&gt;打包服务通常作为一个聚合服务存在，有点类似网关，通过请求指定所需的字段，从各种异构数据源获取数据，并打包为协议定义的对象。&lt;/p&gt;

&lt;p&gt;传统的打包服务流程大致如下：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;通过 loader manager 定义该 pack 请求的下游依赖，构成 loader 的 DAG 图；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;执行所有 loader 进行数据获取，将数据放到透传上下文 datum 中；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;所有 loader 执行完毕（或者超时），执行 packer 的打包逻辑，将 datum 映射为 packed doc；&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-91433f706b3cb750452d7b074a334247_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;但是，这样的做法存在一些问题：&lt;/p&gt;

&lt;h3 id=&quot;选择性打包&quot;&gt;选择性打包&lt;/h3&gt;

&lt;p&gt;典型的基于 pack fields 的选择性打包，通常需要在代码层对每个 field 进行依赖的编排逻辑，会导致代码非常冗长。而如果对 field 进行分组，类似抖音的 pack level，则粒度又过大。&lt;/p&gt;

&lt;h3 id=&quot;字段依赖&quot;&gt;字段依赖&lt;/h3&gt;

&lt;p&gt;上述打包流程中，我们需要手动维护 loader 之间的依赖图。理想的做法是，通过指定每个字段对应的领域服务及对象依赖，自动解决依赖编排逻辑。&lt;/p&gt;

&lt;h3 id=&quot;超时机制&quot;&gt;超时机制&lt;/h3&gt;

&lt;p&gt;典型的基于 loader_manager 的超时机制，粒度太大，因为各个子 loader 的超时可能不同，取决于子 loader 里面耗时最长的那个。但这个超时不好确定，一旦定不合理，比如说为了某一个短板 loader 而调高耗时，很容易导致上游拿不到整个数据。&lt;/p&gt;

&lt;h3 id=&quot;维护成本&quot;&gt;维护成本&lt;/h3&gt;

&lt;p&gt;上面可以看到，loader 与 packer 的逻辑是解耦开的。这意味着，如果要修改某个 packed doc 返回值字段，你需要去定位该字段来自 datum 的哪个值，datum 这个值又是在哪个 loader 赋值的。而基于 GraphQL 的思想，则可以将字段的赋值逻辑收敛在 resolver 中，一定程度上可以降低维护成本。&lt;/p&gt;

&lt;h2 id=&quot;字段权限控制&quot;&gt;字段权限控制&lt;/h2&gt;

&lt;p&gt;目前只有接口级别的权限控制，希望细化到字段 =&amp;gt; psm 的维度的细粒度鉴权。&lt;/p&gt;

&lt;h2 id=&quot;服务治理&quot;&gt;服务治理&lt;/h2&gt;

&lt;p&gt;通常业务会倾向于复用接口来提供服务，比如信息流会使用同一个 path 提供数据，根据入参进行业务逻辑编排。&lt;/p&gt;

&lt;p&gt;但是这样也引入了一个问题，很难对具体的类型进行不同的超时处理。并且，针对具体的请求也很难统一进行一些埋点监控。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;区分当前服务处理错误，与上游服务调用该服务处理超时。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;当然埋点监控可以通过在入口层统一使用中间件来解决。但是对于超时处理、熔断，则难以复用 mesh 的服务治理能力。&lt;/p&gt;

&lt;h1 id=&quot;业界方案&quot;&gt;业界方案&lt;/h1&gt;

&lt;h2 id=&quot;美团&quot;&gt;美团&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;https://tech.meituan.com/2021/05/06/bff-graphql.html&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;美团的思路比较优雅，但是私以为文章描述不是很清晰，这里尽所能对各个模块进行介绍。&lt;/p&gt;

&lt;h3 id=&quot;取数展示分离&quot;&gt;取数展示分离&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;p&gt;All problems in computer science can be solved by another level of indirection.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-8c24b8aab9c83c5d17303b7eee2125d5_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;主要目的在于，避免在数据图中，混入展示层的逻辑。&lt;/p&gt;

&lt;p&gt;传统的 GraphQL 方案中，每个 field 对应相应的 resolver，通常也需要对应单独的取数 / 打包，这样必然导致数据图非常大，而且其中包括很多冗余字段（比如 title、category、title_with_category 同时存在）。&lt;/p&gt;

&lt;p&gt;通过取数和展示的分离，元数据的关联和运行时的组合调用，可以保持逻辑单元的简单，同时又满足复用诉求，这也很好地解决了传统方案中存在的展示服务的颗粒度问题。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;相当于，&lt;/strong&gt;&lt;strong&gt;GraphQL&lt;/strong&gt; &lt;strong&gt;充当了字段计算的职责，数据图负责更&lt;/strong&gt;&lt;strong&gt;原子化&lt;/strong&gt;&lt;strong&gt;的&lt;/strong&gt;&lt;strong&gt;数据获取&lt;/strong&gt;&lt;strong&gt;，这某种意义上也使得领域服务职责更加清晰及稳定。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;查询模型归一&quot;&gt;查询模型归一&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-5c49a9cc7b1a364e0d1097a750ed05fb_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;每个查询模型相当于一个场景。&lt;/p&gt;

&lt;p&gt;私以为，查询模型相当于 schema（命名、类型、映射），并且每个字段维护一个映射（查询模型 =&amp;gt; 展示单元），相当于一段动态代码，标识一个该场景的字段的计算逻辑。&lt;/p&gt;

&lt;p&gt;查询模型可能会膨胀，比如描述某个场景下一个商品的模型，可能包含很多字段，通过标准字段 + 扩展属性的方式建立查询模型，能够较好地解决字段扩散的问题，类似于头条的内容云 optional_data。&lt;/p&gt;

&lt;p&gt;其中，查询模型是多变的（不同版本、不同终端），展示单元变化较小，数据图变化非常小。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;相当于，通过查询模型，解决了场景快速迭代与领域模型相对稳定的矛盾。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;元数据驱动&quot;&gt;元数据驱动&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-6112f131a42a99a4e165fc5108d0399a_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;整体架构由三个核心部分组成：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;业务能力：标准的业务逻辑单元，包括取数单元、展示单元和查询模型，这些都是关键的可复用资产。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;元数据：描述业务功能（如：展示单元、取数单元）以及业务功能之间的关联关系，比如展示单元依赖的数据，展示单元映射的展示字段等。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;执行引擎：负责消费元数据，并基于元数据对业务逻辑进行调度和执行。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所谓元数据驱动，无非描述一个查询模型的取数链路，查询模型 =&amp;gt; 展示单元 =&amp;gt; 取数单元。&lt;/p&gt;

&lt;p&gt;不过，为每个模块记录一个元数据是有意义的，&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;方便可视化，给定一个查询模型，可以很方便查看其 schema 及查询链路&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;减少维护接口协议、接口文档的烦恼&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;低代码化，可以将其平台化，通过创建元数据搭建整条链路的实现&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;airbnb&quot;&gt;AirBnb&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;Eng https://medium.com/airbnb-engineering/reconciling-graphql-and-thrift-at-airbnb-a97e8d290712 中文版 https://juejin.cn/post/6844903698544459784&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;总结一下，AirBnb 在&lt;/strong&gt; &lt;strong&gt;GraphQL&lt;/strong&gt; &lt;strong&gt;架构中主要有两点可以参考：&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;graphql-网关&quot;&gt;GraphQL 网关&lt;/h3&gt;

&lt;p&gt;AirBnb 直接使用 GraphQL 作为网关，承担以下职责：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;聚合 Schema&lt;/strong&gt;：将所有展现服务层的 GraphQL Schema 聚合在一起形成一个单一的 Schema。网关在初始化的时候获取和解析所有展现服务层的 GraphQL Schema，并将他们合并在一起，同时通过轮询来监听 Schema 的变化。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;路由&lt;/strong&gt;：将 GraphQL query 转发到相应的展现服务层去执行。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Query 注册&lt;/strong&gt;：每个生产环境使用的 Query 都会注册生成一个 UUID。一是提高安全性，只有被注册过 query 才能在生产环境中执行；二是客户端不用每次都发送冗长的完整的 GraphQL query，只需使用 query 注册时生成的 UUID 即可。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;thriftgraphql-转换器&quot;&gt;Thrift/GraphQL 转换器&lt;/h3&gt;

&lt;p&gt;Thrift/GraphQL 转换器应用在展示层。在 AirBnb 的架构中，展示层处于网关层的直接下游的位置。&lt;/p&gt;

&lt;p&gt;所有的 GraphQL 查询逻辑和 schema 定义全部都是通过展现服务层定义的 Thrfit 自动构建出来的。如果想让自己负责的展现服务层支持 GraphQL，只需把转换器模块包含进来即可。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-5e780cb34a80f4841fe73d3d64af8ca1_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;头条直播&quot;&gt;头条直播&lt;/h2&gt;

&lt;p&gt;直播的 packer 很有 GraphQL 特色，通过定义每个字段的 resolver 来确定字段的打包逻辑，一个 resolver 可能包含一个或多个 loader。然后根据 IDL 生成的 graph 来确定请求 field 的依赖，只加载对应的数据。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-60c0585808f8db088a91f12d34b1b911_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;优点：
    &lt;ol&gt;
      &lt;li&gt;load、pack 逻辑内聚在字段内，易于理解和维护&lt;/li&gt;
      &lt;li&gt;底层基于 dataloader 实现了 batching/caching 能力，减少调用次数，对下游友好&lt;/li&gt;
      &lt;li&gt;field 维度的数据粒度，只请求必要数据，不做冗余加载&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;缺点：
    &lt;ol&gt;
      &lt;li&gt;使用了 dataloader 的基于时间窗口 batching 能力
        &lt;ul&gt;
          &lt;li&gt;如果一个批量请求的 load 被聚合到不同批次，会导致接口延时增大&lt;/li&gt;
          &lt;li&gt;打破原有 logid 的链路，会导致问题追溯变复杂&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;每个字段都需要维护一个 resolver，组织较清晰，但是代码量相对较大&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;可改进点&quot;&gt;可改进点&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;问题：&lt;/strong&gt;对于同一个请求，不同字段可能依赖同一个下游数据，如果每个字段的 resolve 逻辑都单独调用一次，则会导致很多重复请求，造成读放大。因此直播使用了开源的 dataloader 来实现，会导致 batching 不同会话的请求。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;解决：&lt;/strong&gt;同一个会话共用一份缓存。将数据加载缓存在 context 中，如果已经有数据，则不必重复请求，并且 context 会随着会话终结而释放进行回收。&lt;/p&gt;

&lt;h1 id=&quot;展望与总结&quot;&gt;展望与总结&lt;/h1&gt;

&lt;p&gt;结合业界多个业务在 GraphQL 方面的实践，这里做一个小结。私以为，借鉴美团元数据管理的思路，是一种很好的解决思路。原因有二，一方面能够有效将变动最频繁的逻辑进行配置化管理，另一方面能够沉淀并充分复用领域层的业务逻辑。&lt;/p&gt;

&lt;p&gt;另外，如果能顺便借鉴 AirBnb 把 query 进行注册并且在平台配置对应 gql，那么一方面可以防止端上请求不规范，另一方面可以实现动态修改 gql 而不需要客户端发版，可以在 proxy 层做一些骚操作。&lt;/p&gt;

&lt;p&gt;实际上，上述两者完全可以相结合，下面描述一种 BFF 网关思路：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-8d6d7a1977ef0ee1da2a15f74a02a423_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;注：上图场景层/展示层/实体层都是提供一个平台进行管控。&lt;/p&gt;

&lt;h2 id=&quot;场景层&quot;&gt;场景层&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;场景层可以对业务线的场景进行注册、管理&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;每个场景对应一个客户端/前端页面，具有唯一标识，并且有版本的概念&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;每个场景绑定多个展示层实体 entity，可以通过 GraphQL 实现，进行字段裁剪或者 mapping，但不进行额外计算，如果需要额外计算，则可以新增字段实现，这有利于展示逻辑的沉淀&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;可以看到，场景层类似 AirBnb 的思路，通过 scene_id+version 确定一个 gql&lt;/p&gt;

&lt;h2 id=&quot;展示层&quot;&gt;展示层&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;展示层用于注册、管理展示实体&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;每个展示实体有多个字段，每个字段对应一个或多个领域实体字段；并可以选择直接赋值，或通过映射函数做计算，映射函数可以是 built-in 或者 customized&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;这里可以使用 GraphQL 实现，也可以使用其他方式&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;可以查看每个展示实体、以及具体字段的上层依赖，以此来提醒你某个展示层配置的影响范围&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;领域实体层&quot;&gt;领域实体层&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;领域实体层用于注册、管理领域实体，你可以在此看到系统中所有领域实体的信息、描述&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;每个领域实体有多个字段，从一个指定的领域服务中获取
    &lt;ol&gt;
      &lt;li&gt;不建议从多个领域服务获取，这样做说明领域划分可能存在问题，并且可能造成读放大&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;可以查看每个领域实体、以及具体字段的上层展示、场景依赖，以此来提醒你某个 MR 修改的字段的影响范围&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;领域层&quot;&gt;领域层&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;领域层即各个执行领域逻辑的微服务&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;领域服务的打包，可以借鉴直播的思路，将 load/pack 逻辑内聚，并通过上文提到的基于 context 的会话缓存来避免重复请求&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;一些想法&quot;&gt;一些想法&lt;/h2&gt;

&lt;h3 id=&quot;公共依赖&quot;&gt;公共依赖&lt;/h3&gt;

&lt;p&gt;某些场景下，一个场景可能会关联的多个领域服务，这些领域服务可能有公共依赖，那么，场景层应该借鉴 AGW Loader 的思路，每个场景层可以配置多个 Loader，并提供可插拔的 built-in / customized 的公共依赖加载能力，通过 context 的透传能力透传到下游，或者在上述各个层级的入参配置进行引用从而达到透传的目的。&lt;/p&gt;

&lt;h3 id=&quot;架构中的定位&quot;&gt;架构中的定位&lt;/h3&gt;

&lt;p&gt;对于列表场景，入口层可以是应用服务作为上游，此时 BFF 网关作为一个聚合打包服务，可以应对复杂打包场景，例如抖音 Pack 在架构中的位置。&lt;/p&gt;

&lt;p&gt;而对于 item 场景，入口层可以是 TLB 作为上游，此时 BFF 网关作为一个网关，例如 AGW/Janus 在架构中的位置。&lt;/p&gt;

&lt;h3 id=&quot;现阶段的一些建议&quot;&gt;现阶段的一些建议&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;区分场景层实体与领域层实体。举个例子：&lt;/li&gt;
&lt;/ol&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;type IdeaDetail struct {    // 对文章的点亮（类似笔记）
    IdeaId      int64
    IdeaAuthor  *Author
    IdeaContent string
    // ...
    Post *Post       // 文章
    Group *Group     // 文章所在小组
}
type IdeaDetail struct {    // 对文章的点亮（类似笔记）
    Idea *Idea
    Post *Post       // 文章
    Group *Group     // 文章所在小组
}

type IdeaMeta struct {
    IdeaId      int64
    PostId      int64
    GroupId     int64
    AuthorId    int64
}

type Idea struct {
    IdeaId      int64
    IdeaAuthor  *Author
    IdeaContent string
    // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;左边的实体 IdeaDetail 其实是场景层实体，领域层不应该直接使用该实体作为打包对象，而是通过外键的方式，抽离一个中间的 IdeaMeta，场景层直接关联具体的领域层对象或者展示层对象（如果能细化到展示层当然更好），这样的话，每个领域服务只需要打包所在上下文的数据（Idea 领域打包 Idea 的，Post 领域打包 Post 的…），不需要关心其他上下文数据。&lt;/p&gt;

&lt;p&gt;而抽离出的 IdeaMeta，便是上文提到的场景层公共依赖 Loader 思路。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;区分展示层逻辑与领域层逻辑。举个例子：文章内容是领域层逻辑，基于文章内容计算的简介是展示层逻辑；图片 URI 算领域层逻辑，基于 URI 打包的 URL 是展示层逻辑。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;划分好领域上下文，这是一个大话题，可以参考领域驱动设计相关书籍。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;另外注意，上述三层能力中，每一层都可以通过提供 RPC/HTTP 形式的 OpenAPI 对外提供能力。&lt;/p&gt;

&lt;p&gt;以上如果有疏漏，也欢迎批评指正。&lt;/p&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;https://www.howtographql.com/basics/1-graphql-is-the-better-rest/&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://tech.meituan.com/2021/05/06/bff-graphql.html&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://medium.com/airbnb-engineering/reconciling-graphql-and-thrift-at-airbnb-a97e8d290712&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Thu, 07 Apr 2022 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2022/04/07/GraphQL-A-Backend-Engineer-Perspective/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2022/04/07/GraphQL-A-Backend-Engineer-Perspective/</guid>
        
			<category>architecture</category>
        
			<category>graphql</category>
        
        
		</item>
    
		<item>
			<title>从 Redlock 到共识算法</title>
			<description>&lt;h1 id=&quot;从-redlock-到共识算法&quot;&gt;从 Redlock 到共识算法&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;TL;DR; 本文从介绍 Redlock 开始，引出 DDIA 作者 Martin 对 Redlock 的批判、Relock 作者 antirez 的反驳，从中总结出实现一个分布式锁的核心难题。该难题可以归结为分布式一致性问题，并总结了解决分布式一致性问题的模型与算法。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;redlock&quot;&gt;Redlock&lt;/h1&gt;

&lt;p&gt;2016 年 2 月，为了规范各家对基于Redis的分布式锁的实现，Redis的作者提出了一个更安全的实现，叫做 &lt;a href=&quot;https://redis.io/topics/distlock&quot;&gt;Redlock&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;背景：解决基于单 Redis 节点的单点故障问题；以及哨兵模式下基于异步的主从复制（replication）可能带来的数据不一致问题。&lt;/p&gt;

&lt;p&gt;因此 antirez 提出了新的分布式锁的算法 Redlock，它基于 N 个完全独立的 Redis 节点（通常情况下N可以设置成5）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-10088f8aaccd5780fbbf1dd320007409_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;获取锁&quot;&gt;获取锁&lt;/h2&gt;

&lt;p&gt;运行Redlock算法的客户端依次执行下面各个步骤，来完成&lt;strong&gt;获取锁&lt;/strong&gt;的操作：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;获取当前时间（毫秒数）。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;按顺序依次向N个Redis节点执行获取锁的操作。&lt;/strong&gt;这个获取操作跟前面基于单Redis节点的&lt;strong&gt;获取锁&lt;/strong&gt;的过程相同，包含随机字符串&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my_random_value&lt;/code&gt;，也包含过期时间(比如&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PX 30000&lt;/code&gt;，即锁的有效时间)。为了保证在某个Redis节点不可用的时候算法能够继续运行，这个获取锁的操作还有一个超时时间(time out)，它要远小于锁的有效时间（几十毫秒量级）。&lt;strong&gt;客户端在向某个Redis节点获取锁失败以后，应该立即尝试下一个Redis节点。&lt;/strong&gt;这里的失败，应该包含任何类型的失败，比如该Redis节点不可用，或者该Redis节点上的锁已经被其它客户端持有（注：Redlock原文中这里只提到了Redis节点不可用的情况，但也应该包含其它的失败情况）。&lt;/li&gt;
  &lt;li&gt;计算整个获取锁的过程总共消耗了多长时间，计算方法是用&lt;strong&gt;当前时间减去第1步记录的时间&lt;/strong&gt;。如果客户端从&lt;strong&gt;大多数Redis节点（&amp;gt;= N/2+1）成功获取到了锁，并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time)&lt;/strong&gt;，那么这时客户端才认为最终获取锁成功；否则，认为最终获取锁失败。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;如果最终获取锁成功了，那么这个锁的有效时间应该重新计算&lt;/strong&gt;，它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。&lt;/li&gt;
  &lt;li&gt;如果最终获取锁失败了（可能由于获取到锁的Redis节点个数少于N/2+1，或者整个获取锁的过程消耗的时间超过了锁的最初有效时间），那么客户端应该立即&lt;strong&gt;向所有Redis节点发起释放锁的操作&lt;/strong&gt;（即前面介绍的Redis Lua脚本）。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;释放锁&quot;&gt;释放锁&lt;/h2&gt;

&lt;p&gt;上面描述的只是&lt;strong&gt;获取锁&lt;/strong&gt;的过程，而&lt;strong&gt;释放锁&lt;/strong&gt;的过程比较简单：客户端向所有Redis节点发起&lt;strong&gt;释放锁&lt;/strong&gt;的操作，不管这些节点当时在获取锁的时候成功与否。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;为什么？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;设想这样一种情况，客户端发给某个Redis节点的&lt;strong&gt;获取锁&lt;/strong&gt;的请求成功到达了该Redis节点，这个节点也成功执行了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SET&lt;/code&gt;操作，但是它返回给客户端的响应包却丢失了。这在客户端看来，获取锁的请求由于超时而失败了，但在Redis这边看来，加锁已经成功了。因此，释放锁的时候，客户端也应该对当时获取锁失败的那些Redis节点同样发起请求。实际上，这种情况在异步通信模型中是有可能发生的：客户端向服务器通信是正常的，但反方向却是有问题的。&lt;/p&gt;

&lt;h2 id=&quot;failover&quot;&gt;Failover&lt;/h2&gt;

&lt;p&gt;由于N个Redis节点中的大多数能正常工作就能保证Redlock正常工作，因此理论上它的可用性更高。我们前面讨论的单Redis节点的分布式锁在failover的时候锁失效的问题，在Redlock中不存在了，&lt;strong&gt;但如果有节点发生崩溃重启，还是会对锁的安全性有影响的&lt;/strong&gt;。具体的影响程度跟 Redis 对数据的持久化程度有关。&lt;/p&gt;

&lt;p&gt;假设一共有5个Redis节点：A, B, C, D, E。设想发生了如下的事件序列：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;客户端1成功锁住了A, B, C，&lt;strong&gt;获取锁&lt;/strong&gt;成功（但D和E没有锁住）。&lt;/li&gt;
  &lt;li&gt;节点C崩溃重启了，但客户端1在C上加的锁没有持久化下来，丢失了。&lt;/li&gt;
  &lt;li&gt;节点C重启后，客户端2锁住了C, D, E，&lt;strong&gt;获取锁&lt;/strong&gt;成功。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这样，客户端1和客户端2同时获得了锁（针对同一资源）。&lt;/p&gt;

&lt;p&gt;在默认情况下，Redis 的 AOF 持久化方式是每秒写一次磁盘（即执行fsync），因此最坏情况下可能丢失1秒的数据。为了尽可能不丢数据，Redis允许设置成每次修改数据都进行fsync，但这会降低性能。当然，即使执行了fsync也仍然有可能丢失数据（这取决于系统而不是Redis的实现）。所以，上面分析的由于节点重启引发的锁失效问题，总是有可能出现的。为了应对这一问题，antirez又提出了&lt;strong&gt;延迟重启&lt;/strong&gt;(delayed restarts)的概念。也就是说，一个节点崩溃后，先不立即重启它，而是等待一段时间再重启，这段时间应该大于锁的有效时间(lock validity time)。这样的话，这个节点在重启前所参与的锁都会过期，它在重启后就不会对现有的锁造成影响。&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;redlock-的各种讨论&quot;&gt;Redlock 的各种讨论&lt;/h1&gt;

&lt;p&gt;要知道，亲手实现过Redis Cluster这样一个复杂系统的antirez，足以算得上分布式领域的一名专家了。但对于由分布式锁引发的一系列问题的分析中，不同的专家却能得出迥异的结论，从中我们可以窥见分布式系统相关的问题具有何等的复杂性。&lt;/p&gt;

&lt;p&gt;实际上，在分布式系统的设计中经常发生的事情是：许多想法初看起来毫无破绽，而一旦详加考量，却发现不是那么天衣无缝。&lt;/p&gt;

&lt;h2 id=&quot;martin-的批判&quot;&gt;Martin 的批判&lt;/h2&gt;

&lt;h3 id=&quot;缺乏-fencing-机制&quot;&gt;缺乏 Fencing 机制&lt;/h3&gt;

&lt;p&gt;首先，在没有提供一种 fencing 机制的条件下，锁不具备安全性。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-3c3d8dd3c1095003444838f60fde5a44_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;假设使锁服务本身是没有问题的，而仅仅是客户端有长时间的 pause 或网络延迟，仍然会造成两个客户端同时访问共享资源的冲突情况发生。&lt;/p&gt;

&lt;p&gt;那怎么解决这个问题呢？Martin给出了一种方法，称为 fencing token。fencing token 是一个单调递增的数字，当客户端成功获取锁的时候它随同锁一起返回给客户端。而客户端访问共享资源的时候带着这个 fencing token，这样提供共享资源的服务就能根据它进行检查，拒绝掉延迟到来的访问请求（避免了冲突）。如下图：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-d46f4b7656cd24eab82dfde9703b24e0_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在上图中，客户端1先获取到的锁，因此有一个较小的 fencing token，等于33，而客户端2后获取到的锁，有一个较大的 fencing token，等于34。客户端1从GC pause中恢复过来之后，依然是向存储服务发送访问请求，但是带了 fencing token = 33。存储服务发现它之前已经处理过34的请求，所以会拒绝掉这次33的请求。这样就避免了冲突。&lt;/p&gt;

&lt;h3 id=&quot;过多的计时假设&quot;&gt;过多的计时假设&lt;/h3&gt;

&lt;p&gt;另外，由于Redlock本质上是建立在一个同步模型之上，而且&lt;strong&gt;对系统的记时假设(timing assumption)有很强的要求，因此本身的安全性是不够的。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Martin在文中构造了一些事件序列，能够让Redlock失效（两个客户端同时持有锁）。为了说明Redlock对系统记时(timing)的过分依赖，他首先给出了下面的一个例子（还是假设有5个Redis节点A, B, C, D, E）：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;客户端1从Redis节点A, B, C成功获取了锁（多数节点）。由于网络问题，与D和E通信失败。&lt;/li&gt;
  &lt;li&gt;节点C上的时钟发生了向前跳跃，导致它上面维护的锁快速过期。&lt;/li&gt;
  &lt;li&gt;客户端2从Redis节点C, D, E成功获取了同一个资源的锁（多数节点）。&lt;/li&gt;
  &lt;li&gt;客户端1和客户端2现在都认为自己持有了锁。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;上面这种情况之所以有可能发生，本质上是因为Redlock的安全性(safety property)对系统的时钟有比较强的依赖，一旦系统的时钟变得不准确，算法的安全性也就保证不了了。Martin在这里其实是要指出分布式算法研究中的一些基础性问题，或者说一些常识问题，&lt;strong&gt;即好的分布式算法应该基于异步模型(asynchronous model)，算法的安全性不应该依赖于任何记时假设(timing assumption)。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在异步模型中，进程可能pause任意长的时间，消息可能在网络中延迟任意长的时间，甚至丢失，系统时钟也可能以任意方式出错。一个好的分布式算法，这些因素不应该影响它的安全性(safety property)，只可能影响到它的活性(liveness property)，也就是说，即使在非常极端的情况下（比如系统时钟严重错误），算法顶多是不能在有限的时间内给出结果而已，而不应该给出错误的结果。这样的算法在现实中是存在的，像比较著名的 Paxos，或 Raft。但显然按这个标准的话，Redlock 的安全性级别是达不到的。&lt;/p&gt;

&lt;h2 id=&quot;antirez-的反驳&quot;&gt;antirez 的反驳&lt;/h2&gt;

&lt;h3 id=&quot;fencing-token-无需单调&quot;&gt;Fencing Token 无需单调&lt;/h3&gt;

&lt;p&gt;antirez 对于 Martin 的这种论证方式提出了质疑：&lt;strong&gt;并发下的顺序没有意义&lt;/strong&gt;。即使退一步讲，Redlock虽然提供不了 Martin 所讲的递增的 fencing token，但利用Redlock产生的随机数可以达到同样的效果。这个随机字符串虽然不是递增的，但却是唯一的，可以称之为 unique token。&lt;/p&gt;

&lt;h3 id=&quot;时钟无需过分精确&quot;&gt;时钟无需过分精确&lt;/h3&gt;

&lt;p&gt;Martin 认为 Redlock 会失效的情况主要有三种&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;时钟漂移&lt;/li&gt;
  &lt;li&gt;长时间的 GC pause&lt;/li&gt;
  &lt;li&gt;长时间的 网络延迟&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;时钟漂移&quot;&gt;时钟漂移&lt;/h4&gt;

&lt;p&gt;Martin 在提到时钟跳跃的时候，举了两个可能造成时钟跳跃的具体例子：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;系统管理员手动修改了时钟。&lt;/li&gt;
  &lt;li&gt;从 NTP 服务收到了一个大的时钟更新事件。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;antirez反驳说：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;手动修改时钟这种人为原因，不要那么做就是了。否则的话，如果有人手动修改Raft协议的持久化日志，那么就算是Raft协议它也没法正常工作了。&lt;/li&gt;
  &lt;li&gt;使用一个不会进行“跳跃”式调整系统时钟的 ntpd 程序（可能是通过恰当的配置），对于时钟的修改通过多次微小的调整来完成。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;而Redlock对时钟的要求，并不需要完全精确，它只需要时钟差不多精确就可以了。比如，要记时5秒，但可能实际记了4.5秒，然后又记了5.5秒，有一定的误差。不过只要误差不超过一定范围，这对Redlock不会产生影响。&lt;strong&gt;antirez认为，像这样对时钟精度并不是很高的要求，在实际环境中是完全合理的。&lt;/strong&gt;&lt;/p&gt;

&lt;h4 id=&quot;gc-pause&quot;&gt;GC Pause&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;获取当前时间。&lt;/li&gt;
  &lt;li&gt;完成&lt;strong&gt;获取锁&lt;/strong&gt;的整个过程（与N个Redis节点交互）。&lt;/li&gt;
  &lt;li&gt;再次获取当前时间。&lt;/li&gt;
  &lt;li&gt;把两个时间相减，计算&lt;strong&gt;获取锁&lt;/strong&gt;的过程是否消耗了太长时间，导致锁已经过期了。如果没过期，&lt;/li&gt;
  &lt;li&gt;客户端持有锁去访问共享资源。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在Martin举的例子中，GC pause或网络延迟，实际发生在上述第1步和第3步之间。而不管在第1步和第3步之间由于什么原因（进程停顿或网络延迟等）导致了大的延迟出现，在第4步都能被检查出来，不会让客户端拿到一个它认为有效而实际却已经过期的锁。当然，这个检查依赖系统时钟没有大的跳跃。这也就是为什么 antirez 在前面要对时钟条件进行辩护的原因。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;第四步之后，仍然可能存在延迟呢？&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;antirez 申明称，这个问题对于&lt;em&gt;所有的分布式锁的实现&lt;/em&gt;是普遍存在的。（这 Redlock 确实解决不了，因为需要递增 fencing 机制解决）&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-db46ffa55c238046bade1166f61280e9_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;redlock-的问题与小结&quot;&gt;Redlock 的问题与小结&lt;/h1&gt;

&lt;p&gt;Martin 认为 Redlock 实在不是一个好的选择，对于需求性能的分布式锁应用它太重了且成本高；对于需求正确性的应用来说它不够安全。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;因为它对高危的时钟或者说其他上述列举的情况进行了不可靠的假设&lt;/strong&gt;，如果你的应用只需要高性能的分布式锁不要求多高的正确性，那么单节点 Redis 够了；&lt;strong&gt;如果你的应用想要保住正确性，那么不建议 Redlock，建议使用一个合适的一致性协调系统，例如 Zookeeper，且保证存在 fencing token&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;仅有在你假设了一个同步系统模型的基础上，Redlock 才能正常工作，也就是系统能满足以下属性：&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;网络延时边界，即假设数据包一定能在某个最大延时之内到达&lt;/li&gt;
  &lt;li&gt;进程停顿边界，即进程停顿一定在某个最大时间之内&lt;/li&gt;
  &lt;li&gt;时钟错误边界，即不会从一个坏的 NTP 服务器处取得时间&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在Martin 的这篇文章中，还有一个很有见地的观点，就是对锁的用途的区分。他把锁的用途分为两种：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;为了效率(efficiency)，协调各个客户端避免做重复的工作。即使锁偶尔失效了，只是可能把某些操作多做一遍而已，不会产生其它的不良后果。比如重复发送了一封同样的email。&lt;/li&gt;
  &lt;li&gt;为了正确性(correctness)。在任何情况下都不允许锁失效的情况发生，因为一旦发生，就可能意味着数据不一致(inconsistency)，数据丢失，文件损坏，或者其它严重的问题。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;最后，Martin得出了如下的结论：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;如果是为了效率(efficiency)而使用分布式锁，允许锁的偶尔失效，那么使用单Redis节点的锁方案就足够了，简单而且效率高。Redlock则是个过重的实现(heavyweight)。&lt;/li&gt;
  &lt;li&gt;如果是为了正确性(correctness)在很严肃的场合使用分布式锁，那么不要使用Redlock。它不是建立在异步模型上的一个足够强的算法，它对于系统模型的假设中包含很多危险的成分(对于timing)。而且，它没有一个机制能够提供fencing token。那应该使用什么技术呢？Martin认为，应该考虑类似Zookeeper的方案，或者支持事务的数据库。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;宁愿正确地挂掉，也不错误地运行。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;分布式模型&quot;&gt;分布式模型&lt;/h1&gt;

&lt;h2 id=&quot;现实中的挑战&quot;&gt;现实中的挑战&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;前面我们介绍了 Redlock 算法及各大咖的讨论，引出 Redlock 的问题，这些问题也是现实中实现分布式系统会经常遇到的挑战，这里简单对这些挑战做下小结。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;不可靠的网络&quot;&gt;不可靠的网络&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;如果发送请求并没有得到响应，则无法区分（a）请求是否丢失，（b）远程节点是否关闭，或（c）响应是否丢失。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-12afd3ab53d6af5eed48b5f302d89233_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;不可靠的时钟&quot;&gt;不可靠的时钟&lt;/h3&gt;

&lt;p&gt;计算机中的石英钟不够精确：它会&lt;strong&gt;漂移（drifts）&lt;/strong&gt;（运行速度快于或慢于预期）。时钟漂移取决于机器的温度。 Google 假设其服务器时钟漂移为200 ppm（百万分之一），相当于每30秒与服务器重新同步一次的时钟漂移为6毫秒，或者每天重新同步的时钟漂移为17秒。即使一切工作正常，此漂移也会限制程序可以达到的最佳准确度。&lt;/p&gt;

&lt;p&gt;一个多主复制的场景，&lt;strong&gt;客户端B的写入比客户端A的写入要晚，但是B的写入具有较早的时间戳。因此解决冲突的时候把 B 的请求丢了（如果使用 Last Write Wins 的话）。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-f1228759043c31533310f96a9302a680_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;逻辑时钟（logic clock）&lt;/strong&gt;是基于递增计数器而不是振荡石英晶体，对于排序事件来说是更安全的选择。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google TrueTime API&lt;/strong&gt; ，Google 在 spanner 中使用的全局时间戳，它明确地报告了本地时钟的置信区间。当你询问当前时间时，你会得到两个值：[最早，最晚]，这是最早可能的时间戳和最晚可能的时间戳。&lt;strong&gt;在不确定性估计的基础上，时钟知道当前的实际时间落在该区间内&lt;/strong&gt;。可以根据这个区间做一些骚操作，比如两个事务之间等待置信区间长度，保证两个事务的置信区间不重叠，由此保证事务的顺序。&lt;/p&gt;

&lt;h3 id=&quot;进程暂停&quot;&gt;进程暂停&lt;/h3&gt;

&lt;ul&gt;
  &lt;li&gt;许多编程语言运行时（如Java虚拟机）都有一个垃圾收集器（GC），偶尔需要停止所有正在运行的线程。这些“&lt;strong&gt;停止世界（stop-the-world）&lt;/strong&gt;”GC暂停有时会持续几分钟【64】！甚至像HotSpot JVM的CMS这样的所谓的“并行”垃圾收集器也不能完全与应用程序代码并行运行，它需要不时地停止世界【65】。尽管通常可以通过改变分配模式或调整GC设置来减少暂停【66】，但是如果我们想要提供健壮的保证，就必须假设最坏的情况发生。&lt;/li&gt;
  &lt;li&gt;在虚拟化环境中，可以&lt;strong&gt;挂起（suspend）&lt;/strong&gt;虚拟机（暂停执行所有进程并将内存内容保存到磁盘）并恢复（恢复内存内容并继续执行）。这个暂停可以在进程执行的任何时候发生，并且可以持续任意长的时间。这个功能有时用于虚拟机从一个主机到另一个主机的实时迁移，而不需要重新启动，在这种情况下，暂停的长度取决于进程写入内存的速率【67】。&lt;/li&gt;
  &lt;li&gt;在最终用户的设备（如笔记本电脑）上，执行也可能被暂停并随意恢复，例如当用户关闭笔记本电脑的盖子时。&lt;/li&gt;
  &lt;li&gt;当操作系统上下文切换到另一个线程时，或者当管理程序切换到另一个虚拟机时（在虚拟机中运行时），当前正在运行的线程可以在代码中的任意点处暂停。在虚拟机的情况下，在其他虚拟机中花费的CPU时间被称为&lt;strong&gt;窃取时间（steal time）&lt;/strong&gt;。如果机器处于沉重的负载下（即，如果等待运行的线程很长），暂停的线程再次运行可能需要一些时间。&lt;/li&gt;
  &lt;li&gt;如果应用程序执行同步磁盘访问，则线程可能暂停，等待缓慢的磁盘I/O操作完成【68】。在许多语言中，即使代码没有包含文件访问，磁盘访问也可能出乎意料地发生——例如，Java类加载器在第一次使用时惰性加载类文件，这可能在程序执行过程中随时发生。 I/O暂停和GC暂停甚至可能合谋组合它们的延迟【69】。如果磁盘实际上是一个网络文件系统或网络块设备（如亚马逊的EBS），I/O延迟进一步受到网络延迟变化的影响【29】。&lt;/li&gt;
  &lt;li&gt;如果操作系统配置为允许交换到磁盘（分页），则简单的内存访问可能导致&lt;strong&gt;页面错误（page fault）&lt;/strong&gt;，要求将磁盘中的页面装入内存。当这个缓慢的I/O操作发生时，线程暂停。如果内存压力很高，则可能需要将不同的页面换出到磁盘。在极端情况下，操作系统可能花费大部分时间将页面交换到内存中，而实际上完成的工作很少（这被称为&lt;strong&gt;抖动（thrashing）&lt;/strong&gt;）。为了避免这个问题，通常在服务器机器上禁用页面调度（如果你宁愿干掉一个进程来释放内存，也不愿意冒抖动风险）。&lt;/li&gt;
  &lt;li&gt;可以通过发送SIGSTOP信号来暂停Unix进程，例如通过在shell中按下Ctrl-Z。 这个信号立即阻止进程继续执行更多的CPU周期，直到SIGCONT恢复为止，此时它将继续运行。 即使你的环境通常不使用SIGSTOP，也可能由运维工程师意外发送。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所有这些事件都可以随时&lt;strong&gt;抢占（preempt）&lt;/strong&gt;正在运行的线程，并在稍后的时间恢复运行，而线程甚至不会注意到这一点。这个问题类似于在单个机器上使多线程代码线程安全：你不能对时机做任何假设，因为随时可能发生上下文切换，或者出现并行运行。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;分布式系统中的节点，必须假定其执行可能在任意时刻暂停相当长的时间&lt;/strong&gt;，即使是在一个函数的中间。在暂停期间，世界的其它部分在继续运转，甚至可能因为该节点没有响应，而宣告暂停节点的死亡。最终暂停的节点可能会继续运行，在再次检查自己的时钟之前，甚至可能不会意识到自己进入了睡眠。&lt;/p&gt;

&lt;h3 id=&quot;拜占庭故障&quot;&gt;拜占庭故障&lt;/h3&gt;

&lt;p&gt;拜占庭将军问题是 Leslie Lamport 在 &lt;a href=&quot;https://web.archive.org/web/20170205142845/http://lamport.azurewebsites.net/pubs/byz.pdf&quot;&gt;The Byzantine Generals Problem&lt;/a&gt; 论文中提出的分布式领域的容错问题，它是分布式领域中最复杂、最严格的容错模型。&lt;/p&gt;

&lt;p&gt;在该模型下，&lt;strong&gt;系统不会对集群中的节点做任何的限制，它们可以向其他节点发送随机数据、错误数据，也可以选择不响应其他节点的请求，这些无法预测的行为使得容错这一问题变得更加复杂&lt;/strong&gt;。&lt;/p&gt;

&lt;h2 id=&quot;计时模型&quot;&gt;计时模型&lt;/h2&gt;

&lt;h3 id=&quot;同步模型&quot;&gt;同步模型&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;同步模型（synchronous model）&lt;/strong&gt;假设网络延迟，进程暂停和和时钟误差都是有界限的。这并不意味着完全同步的时钟或零网络延迟；这只意味着你知道网络延迟，暂停和时钟漂移将永远不会超过某个固定的上限。&lt;strong&gt;同步模型并不是大多数实际系统的现实模型，因为（如本章所讨论的）无限延迟和暂停确实会发生。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;半同步模型&quot;&gt;半同步模型&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;部分同步（partial synchronous）&lt;/strong&gt;意味着一个&lt;strong&gt;系统在大多数情况下像一个同步系统一样运行，但有时候会超出网络延迟，进程暂停和时钟漂移的界限&lt;/strong&gt;。&lt;strong&gt;这是很多系统的现实模型&lt;/strong&gt;：大多数情况下，网络和进程表现良好，否则我们永远无法完成任何事情，但是我们必须承认，在任何时刻假设都存在偶然被破坏的事实。发生这种情况时，网络延迟，暂停和时钟错误可能会变得相当大。&lt;/p&gt;

&lt;h3 id=&quot;异步模型&quot;&gt;异步模型&lt;/h3&gt;

&lt;p&gt;在这个模型中，&lt;strong&gt;一个算法不允许对时机做任何假设（所以它不能使用超时）&lt;/strong&gt;—— 事实上它甚至没有时钟。一些算法被设计为可用于异步模型，但非常受限。&lt;/p&gt;

&lt;h2 id=&quot;节点故障模型&quot;&gt;节点故障模型&lt;/h2&gt;

&lt;h3 id=&quot;崩溃-终止模型&quot;&gt;崩溃-终止模型&lt;/h3&gt;

&lt;p&gt;在&lt;strong&gt;崩溃停止（crash-stop）&lt;/strong&gt;模型中，算法可能会假设一个节点只能以一种方式失效，即通过崩溃。这意味着&lt;strong&gt;节点可能在任意时刻突然停止响应，此后该节点永远消失&lt;/strong&gt;——它永远不会回来。&lt;/p&gt;

&lt;h3 id=&quot;崩溃-恢复模型&quot;&gt;崩溃-恢复模型&lt;/h3&gt;

&lt;p&gt;我们假设节点可能会在任何时候崩溃，但也许会在未知的时间之后再次开始响应。&lt;strong&gt;在崩溃-恢复（crash-recovery）模型中，假设节点具有稳定的存储（即，非易失性磁盘存储）且会在崩溃中保留，而内存中的状态会丢失。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;拜占庭故障模型&quot;&gt;拜占庭故障模型&lt;/h3&gt;

&lt;p&gt;节点可以做（绝对意义上的）任何事情，&lt;strong&gt;包括调戏和欺骗其他节点&lt;/strong&gt;，如上一节所述。&lt;/p&gt;

&lt;p&gt;对于真实的系统，最普遍的模型组合是，半同步计时模型+崩溃-恢复模型。&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;分布式共识算法&quot;&gt;分布式共识算法&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;问题有了，为了解决问题而抽象出来的模型也有了，接下来就是实实在在的算法和实现了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;可线性化&quot;&gt;可线性化&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;可线性化是最强的一致性模型。后面会讲到的共识算法，都会无限逼近这个模型。其背后的基本思想很简单：使系统看起来好像只有一个数据副本。&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;一个非可线性化的例子：如果读取请求与写入请求并发，则可能会返回旧值或新值：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-c4c211395d4c1e9f724e27a96720b8e0_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;为了使系统线性一致，我们需要添加另一个约束：&lt;strong&gt;任何一个读取返回新值后，所有后续读取（在相同或其他客户端上）也必须返回新值。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-355957734047ba33758cc18a83869946_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;每个操作都在我们认为执行操作的时候用竖线标出（在每个操作的条柱之内）。这些标记按顺序连在一起，其结果必须是一个有效的寄存器读写序列（每次读取都必须返回最近一次写入设置的值），操作标记的连线总是按时间（从左到右）向前移动，而不是向后移动。&lt;strong&gt;这就要求可线性化确保一个条件恒成立：一旦新值被写入或者读取，所有后续的读看到都是最新的值。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-fccb4f0962fe2ef61c737547be83261e_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;实现可线性化&quot;&gt;实现可线性化&lt;/h3&gt;

&lt;p&gt;我们已经见到了几个线性一致性有用的例子，让我们思考一下，如何实现一个提供线性一致语义的系统。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;由于线性一致性本质上意味着“表现得好像只有一个数据副本，而且所有的操作都是&lt;/strong&gt;&lt;strong&gt;原子&lt;/strong&gt;&lt;strong&gt;的”&lt;/strong&gt;，所以最简单的答案就是，真的只用一个数据副本。但是这种方法无法容错：如果持有该副本的节点失效，数据将会丢失，或者至少无法访问，直到节点重新启动。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;使系统容错最常用的方法是使用复制。&lt;/strong&gt;&lt;/p&gt;

&lt;h4 id=&quot;主从复制可能线性一致&quot;&gt;主从复制&lt;strong&gt;&lt;em&gt;（可能线性一致）&lt;/em&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;在具有单主复制功能的系统中（参见“&lt;a href=&quot;https://vonng.gitbooks.io/ddia-cn/content/ch5.html#领导者与追随者&quot;&gt;领导者与追随者&lt;/a&gt;”），主库具有用于写入的数据的主副本，而追随者在其他节点上保留数据的备份副本。&lt;strong&gt;如果从主库或同步更新的从库读取数据，它们可能是线性一致性的。&lt;/strong&gt;然而，并不是每个单主数据库都是实际线性一致性的，无论是通过设计（例如，因为使用快照隔离）还是并发错误。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;从主库读取依赖一个假设，你确定主节点是谁。&lt;/strong&gt;正如在“&lt;a href=&quot;https://vonng.gitbooks.io/ddia-cn/content/ch8.html#真理被多数人定义&quot;&gt;真理在多数人手中&lt;/a&gt;”中所讨论的那样，一个节点很可能会认为它是领导者，而事实上并非如此——如果具有错觉的领导者继续为请求提供服务，可能违反线性一致性。而且，&lt;strong&gt;如果使用异步复制，故障切换时甚至可能会丢失已提交的写入&lt;/strong&gt;（参阅“&lt;a href=&quot;https://vonng.gitbooks.io/ddia-cn/content/ch5.html#处理节点宕机&quot;&gt;处理节点宕机&lt;/a&gt;”），这同时违反了持久性和线性一致性。&lt;/p&gt;

&lt;h4 id=&quot;共识算法线性一致&quot;&gt;共识算法&lt;strong&gt;&lt;em&gt;（线性一致）&lt;/em&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;一些在本章后面讨论的共识算法，与主从复制类似。然而，共识协议包含防止脑裂和陈旧副本的措施。由于这些细节，共识算法可以安全地实现线性一致性存储。例如，Zookeeper 和 etcd 就是这样工作的。&lt;/p&gt;

&lt;h4 id=&quot;多主复制非线性一致&quot;&gt;多主复制&lt;strong&gt;&lt;em&gt;（非线性一致）&lt;/em&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;具有多主程序复制的系统通常不是线性一致的，因为它们同时在多个节点上处理写入，并将其异步复制到其他节点。因此，它们可能会产生冲突的写入，需要解析（参阅“&lt;a href=&quot;https://vonng.gitbooks.io/ddia-cn/content/ch5.html#处理写入冲突&quot;&gt;处理写入冲突&lt;/a&gt;”）。这种冲突是因为缺少单一数据副本人为产生的。&lt;/p&gt;

&lt;h4 id=&quot;无主复制也许不是线性一致的&quot;&gt;无主复制&lt;strong&gt;&lt;em&gt;（也许不是线性一致的）&lt;/em&gt;&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;直觉上 Quorum 是线性一致的，但是实际上存在非线性一致的执行，尽管使用了严格的 Quorum：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-cf717b13d456742325af0668e879890a_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;有趣的是，通过牺牲性能，可以使 Dynamo 风格的 Quorum 读写线性化：&lt;strong&gt;读取者必须在将结果返回给应用之前，同步执行读修复（参阅“&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//vonng.gitbooks.io/ddia-cn/content/ch5.html%23%E8%AF%BB%E6%97%B6%E4%BF%AE%E5%A4%8D%E4%B8%8E%E5%8F%8D%E7%86%B5%E8%BF%87%E7%A8%8B&quot;&gt;读时修复与反熵过程&lt;/a&gt;”）&lt;/strong&gt; ，并且写入者必须在发送写入之前，读取 Quorum 数量节点的最新状态。然而，由于性能损失，Riak不执行同步读修复。 Cassandra 在进行 Quorum 读取时，确实在等待读修复完成；但是由于使用了最后写入为准的冲突解决方案，当同一个键有多个并发写入时，将不能保证线性一致性。&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;全序关系广播&quot;&gt;全序关系广播&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;全序关系广播与共识关系密切。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;单主复制通过选择一个节点作为主库来确定操作的全序，并在主库的单个CPU核上对所有操作进行排序。接下来的挑战是，如果吞吐量超出单个主库的处理能力，这种情况下如何扩展系统；以及，如果主库失效（“&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//vonng.gitbooks.io/ddia-cn/content/ch9.html%23%E5%A4%84%E7%90%86%E8%8A%82%E7%82%B9%E5%AE%95%E6%9C%BA&quot;&gt;处理节点宕机&lt;/a&gt;”），如何处理故障切换。在分布式系统文献中，这个问题被称为&lt;strong&gt;全序广播（total order broadcast）&lt;/strong&gt;或&lt;strong&gt;原子广播（atomic broadcast）。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;全序广播通常被描述为在节点间交换消息的协议。 非正式地讲，&lt;strong&gt;它要满足两个安全属性&lt;/strong&gt;：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;&lt;em&gt;可靠交付（reliable delivery）&lt;/em&gt;&lt;/strong&gt; 没有消息丢失：如果消息被传递到一个节点，它将被传递到所有节点。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;全序交付（totally ordered delivery）&lt;/strong&gt; 消息以相同的顺序传递给每个节点。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;正确的全序广播算法必须始终保证可靠性和有序性，即使节点或网络出现故障。当然在网络中断的时候，消息是传不出去的，但是算法可以不断重试，以便在网络最终修复时，消息能及时通过并送达（当然它们必须仍然按照正确的顺序传递）。&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;共识算法&quot;&gt;共识算法&lt;/h2&gt;

&lt;h3 id=&quot;非容错共识算法&quot;&gt;非容错共识算法&lt;/h3&gt;

&lt;h4 id=&quot;2pc&quot;&gt;2PC&lt;/h4&gt;

&lt;p&gt;两阶段提交（Two Phase Commit, 2PC）是一种在多节点之间实现事务原子提交的共识算法，用来保证所有节点要么全部提交，要么全部终止。（是后面提及到的几个容错共识算法的原型）&lt;/p&gt;

&lt;h5 id=&quot;算法流程&quot;&gt;算法流程&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;2PC 触发时机：协调者向所有数据库发送 write 请求并收到成功之后，准备提交事务&lt;/li&gt;
  &lt;li&gt;2PC 处理流程：2PC 将事务的提交过程分成了准备和提交两个阶段进行处理&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
  &lt;li&gt;阶段一 prepare：
    &lt;ol&gt;
      &lt;li&gt;协调者向所有参与者发送一个&lt;strong&gt;准备&lt;/strong&gt;请求，并打上全局事务ID的标记。如果任意一个请求失败或超时，则协调者向所有参与者发送针对该事务ID的中止请求；&lt;/li&gt;
      &lt;li&gt;参与者收到准备请求时，需要确保在任意情况下都的确可以提交事务（通过写入 undolog、redolog）；&lt;/li&gt;
      &lt;li&gt;参与者向协调者反馈响应；&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
  &lt;li&gt;阶段二 commit：
    &lt;ol&gt;
      &lt;li&gt;当协调者收到所有准备请求的答复时，会就提交或中止事务作出明确的决定（只有在所有参与者投赞成票的情况下才会提交）。协调者必须把这个决定写到磁盘上的事务日志中，如果它随后就崩溃，恢复后也能知道自己所做的决定。这被称为&lt;strong&gt;提交点（commit point）；&lt;/strong&gt;&lt;/li&gt;
      &lt;li&gt;一旦协调者的决定落盘，提交或放弃请求会发送给所有参与者。如果这个请求失败或超时，协调者必须永远保持重试，直到成功为止。&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-fa6d3f7b85bdc1851935636b389bf17f_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;因此，该协议包含两个关键的“不归路”点：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;当参与者投票“yes”时，它承诺它稍后肯定能够提交（尽管协调者可能仍然选择放弃）；&lt;/li&gt;
  &lt;li&gt;一旦协调者做出决定，这一决定是不可撤销的；&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;故障分析&quot;&gt;故障分析&lt;/h5&gt;

&lt;ol&gt;
  &lt;li&gt;任何一个写失败，协调者都不会提交事务，因此安全；&lt;/li&gt;
  &lt;li&gt;任何一个参与者 prepare 失败，协调者不会提交事务，因此安全；&lt;/li&gt;
  &lt;li&gt;协调者在 prepare 中失败，返回给用户错误，且事务没有真正提交，因此安全；&lt;/li&gt;
  &lt;li&gt;参与者 prepare 回包丢失，协调者不会提交事务，因此安全；&lt;/li&gt;
  &lt;li&gt;参与者在 commit 阶段挂了，协调者会无限重试保证事务提交；&lt;/li&gt;
  &lt;li&gt;协调者挂了，可以完成 2PC 的唯一方法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中止请求之前，将其提交或中止决定写入磁盘上的事务日志：协调者恢复后，通过读取其事务日志来确定所有存疑事务的状态。任何在协调者日志中没有提交记录的事务都会终止。&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;3pc&quot;&gt;3PC&lt;/h4&gt;

&lt;p&gt;3PC 假定一个&lt;strong&gt;有界的网络延迟并且节点能够在规定时间内响应&lt;/strong&gt;，所以 3PC 通过连接是否超时来判断节点是否故障：如果参与者等待第二阶段指令超时，则自动 abort 抛弃事务，若等待第三阶段指令超时，则自动 commit 提交事务。&lt;strong&gt;相较于两阶段提交，三阶段提交协议最大的优点是降低了参与者的阻塞范围，并且能够在出现单点故障后继续保持一致。&lt;/strong&gt;&lt;/p&gt;

&lt;h3 id=&quot;容错共识算法&quot;&gt;容错共识算法&lt;/h3&gt;

&lt;p&gt;本节的所有算法都可以归纳为类 Paxos 算法， 并且他们的实现流程与 2PC 非常类似。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;最大的区别在于，&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;2PC 的主节点是由外部指定的，而类 Paxos 算法可以在主节点崩溃失效后重新选举出新的主节点并进入一致状态。&lt;/li&gt;
  &lt;li&gt;容错共识算法只要收到多数节点的投票结果即可通过决议，而 2PC 则要每个参与者都必须做出 Yes 响应才能通过。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这些差异是确保共识算法的正确性和容错性的关键。&lt;/p&gt;

&lt;h4 id=&quot;paxos&quot;&gt;Paxos&lt;/h4&gt;

&lt;h5 id=&quot;overview&quot;&gt;Overview&lt;/h5&gt;

&lt;blockquote&gt;
  &lt;p&gt;个人觉得最好的学习路线：&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//www.youtube.com/watch%3Fv%3DJEpsBg0AO6o%26t%3D6s&quot;&gt;Paxos lecture(Recommended)&lt;/a&gt; + &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Paxos_(computer_science)%23&quot;&gt;Paxos Wikipedia&lt;/a&gt; + &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//lamport.azurewebsites.net/pubs/paxos-simple.pdf&quot;&gt;Paper(optional)&lt;/a&gt;（&lt;strong&gt;有严格先后顺序&lt;/strong&gt;）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-c336f2bf48bfd56070ada21b99bcc9a4_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Paxos 的目标：Leader 想把自己的日志，一行不漏同步到其他所有 Follower。&lt;/p&gt;

&lt;p&gt;在多个节点中，对日志的一致性达成共识。一旦日志相同，则能保证每个节点的状态机相同（按照相同的顺序，执行相同的命令）&lt;/p&gt;

&lt;h6 id=&quot;状态机&quot;&gt;状态机&lt;/h6&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;安全性：&lt;/strong&gt;所有节点的日志的顺序必须相同&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;活性：&lt;/strong&gt;最终，所有节点拥有相同且完整的日志，执行了相同的日志序列，拥有相同的状态&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/80/v2-dea1f43408d73b93266903a9407bf179_1440w.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h6 id=&quot;概念&quot;&gt;概念&lt;/h6&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;an paxos instance：一轮法案，一个任期（一个任期范围内，有且只有一个被选定的提案值）&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;a round：一个新的 proposal 的提出过程（包含两阶段），每轮法案可能包含多个 proposal（or say, round）&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;proposal number：提案编号（日志的 offset），通常是 n；每一个新的提案编号，都会严格大于旧提案编号&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;proposal value：提案的内容（日志的内容），通常是 v；&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Basic Paxos：&lt;/p&gt;

    &lt;p&gt;&lt;strong&gt;对某行日志达成共识的过程&lt;/strong&gt;&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;一个或多个 server 提出议案（propose value）&lt;/li&gt;
      &lt;li&gt;系统有且选择一个议案（single value chosen）&lt;/li&gt;
      &lt;li&gt;系统不曾选择第二个议案&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Multi-Paxos&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;由多轮 Basic Paxos instance，&lt;strong&gt;达成一个一致的日志序列的过程&lt;/strong&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;basic-paxos&quot;&gt;Basic Paxos&lt;/h5&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;Each “instance” (or “execution”) of the basic Paxos protocol&lt;/em&gt; &lt;strong&gt;&lt;em&gt;decides on a single output value&lt;/em&gt;&lt;/strong&gt;&lt;em&gt;. The protocol proceeds over several rounds.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-e642a8ff9c0e7a6d1f96db82523134bf_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-ca5cb6669e185e2c75ed00864c9489bf_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h5 id=&quot;multi-paxos&quot;&gt;Multi Paxos&lt;/h5&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;em&gt;In Paxos, clients send commands to a leader. During normal operation, the leader receives a client’s command, assigns it a new command number i, and then&lt;/em&gt; &lt;strong&gt;&lt;em&gt;begins the ith instance of the consensus algorithm&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;by sending messages to a set of acceptor processes.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;引入多个 Paxos instance 之后，需要解决的问题&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;性能优化：多主导致延迟收敛、多轮 Paxos instance 的冗余 prepare RPCs&lt;/li&gt;
  &lt;li&gt;如何选择一个 proposal 编号（日志 offset）&lt;/li&gt;
  &lt;li&gt;如何保证日志的完整性&lt;/li&gt;
  &lt;li&gt;如何与客户端的交互&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;raft--zab&quot;&gt;Raft &amp;amp; Zab&lt;/h4&gt;

&lt;p&gt;篇幅关系，可以参阅其他博文，相比于 paxos，raft 可以找到更多更好的教程。&lt;/p&gt;

&lt;h3 id=&quot;拜占庭故障容错算法&quot;&gt;拜占庭故障容错算法&lt;/h3&gt;

&lt;p&gt;当一个系统在部分节点发生故障、不遵守协议、甚至恶意攻击、扰乱网络时仍然能继续正确工作，称之为&lt;strong&gt;拜占庭容错（Byzantine fault-tolerant）&lt;/strong&gt;的，在特定场景下，这种担忧在是有意义的：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;在航空航天环境中，计算机内存或CPU寄存器中的数据可能被辐射破坏，导致其以任意不可预知的方式响应其他节点。由于系统故障将非常昂贵（例如，飞机撞毁和炸死船上所有人员，或火箭与国际空间站相撞），飞行控制系统必须容忍拜占庭故障。&lt;/li&gt;
  &lt;li&gt;在多个参与组织的系统中，一些参与者可能会试图欺骗或欺骗他人。在这种情况下，节点仅仅信任另一个节点的消息是不安全的，因为它们可能是出于恶意的目的而被发送的。例如，像比特币和其他区块链一样的对等网络可以被认为是让互不信任的各方同意交易是否发生的一种方式，而不依赖于中央当局。&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;工程化实现与应用&quot;&gt;工程化实现与应用&lt;/h2&gt;

&lt;h3 id=&quot;zookeeper&quot;&gt;Zookeeper&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;p&gt;Ref &lt;a href=&quot;https://wingsxdu.com/post/database/zookeeper/#gsc.tab=0&quot;&gt;ZooKeeper 与 Zab 协议 · Analyze - beihai blog (wingsxdu.com)&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Zab 协议的全称是 ZooKeeper 原子广播协议（ZooKeeper Atomic Broadcast Protocol），实际上实现了前面提及的全序关系广播。&lt;/p&gt;

&lt;h4 id=&quot;节点状态&quot;&gt;节点状态&lt;/h4&gt;

&lt;p&gt;ZooKeeper 中&lt;strong&gt;所有的写请求必须由一个全局唯一的 Leader 服务器来协调处理&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;ZooKeeper &lt;strong&gt;客户端会随机连接（长连接，并通过心跳维护&lt;/strong&gt; &lt;strong&gt;session&lt;/strong&gt;&lt;strong&gt;）到 ZooKeeper 集群中的一个节点，如果是读请求，就直接从当前节点中读取数据；如果是写请求，那么该节点就会向 Leader 转发请求&lt;/strong&gt;，Leader 接收到读写事务后，会将事务转换为一个事务提案（Proposal），并向集群广播该提案，只要超过半数节点写入数据成功，Leader 会再次向集群广播 Commit 消息，将该提案提交。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ZooKeeper 集群节点可能处于下面四种状态之一&lt;/strong&gt;，分别是：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;LOOKING&lt;/strong&gt;：进入 Leader 选举状态；&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;LEADING&lt;/strong&gt;：某个节点成为 Leader 并负责协调事务 ；&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;FOLLOWING&lt;/strong&gt;：当前节点是 Follower，服从 Leader 节点的命令并参与共识；&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;OBSERVING&lt;/strong&gt;：Observer 节点是只读节点，用于增加集群的只读事务性能，不参与共识与选举。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zab 协议使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZXID&lt;/code&gt; 来表示全局事务编号，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ZXID&lt;/code&gt; 是一个 64 位数字，其中低 32 位是一个单调递增的计数器，针对客户端每一个事务请求，计数器都加 1；高 32 位则表示当前 Leader 的 Epoch，每当选举出一个新的主服务器，就会从集群日志中取出最大的 ZXID，从中读取出 Epoch 值，然后加 1，以此作为新的 Epoch，同时将低 32 位从 0 开始计数。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-ba909bc061d73dfee784b0bad663c120_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;全序广播&quot;&gt;全序广播&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-31126e293c16a3d9622501ae1a7dbbdf_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;ZooKeeper 的消息广播过程&lt;strong&gt;类似于两阶段提交&lt;/strong&gt;，针对客户端的读写事务请求，Leader 会生成对应的事务提案，并为其分配 ZXID，随后将这条提案广播给集群中的其它节点。Follower 节点接收到该事务提案后，会先将其以事务日志的形式写入本地磁盘中，写入成功后会给 Leader 反馈一条 Ack 响应。当 Leader 收到超过半数 Follower 节点的 Ack 响应后，会返回客户端成功，并向集群发送 Commit 消息，将该事务提交。Follower 服务器接收到 Commit 消息后，会完成事务的提交，将数据应用到数据副本中。&lt;/p&gt;

&lt;p&gt;在消息广播过程中，&lt;strong&gt;Leader 服务器会为每个 Follower 维护一个消息队列&lt;/strong&gt;，然后将需要广播的提案依次放入队列中，并根据「先入先出」的规则逐一发送消息。因为只要超过半数节点响应就可以认为写操作成功，所以少数的慢节点不会影响整个集群的性能。&lt;/p&gt;

&lt;p&gt;各个阶段的写失败可以参阅上文的两阶段提交，其实是一样的。&lt;/p&gt;

&lt;h4 id=&quot;崩溃恢复&quot;&gt;崩溃恢复&lt;/h4&gt;

&lt;p&gt;Zab 集群使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Epoch&lt;/code&gt;（纪元）来表示当前集群所处的周期，每个 Leader 都有自己的任期值，所以&lt;strong&gt;每次 Leader 变更之后，都会在前一个&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Epoch&lt;/code&gt;的基础上加 1&lt;/strong&gt;。&lt;strong&gt;Follower 只听从当前纪元 Leader 的命令，旧 Leader 崩溃恢复后，发现集群中存在更大的纪元，会切换为 FOLLOWING 状态。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;触发时机：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;当 Leader 节点出现崩溃中止，Follower 无法连接到 Leader 时，Follower 会切换为 LOOKING 状态，发起新一轮的选举；&lt;/li&gt;
  &lt;li&gt;如果 Leader 节点无法与过半的服务器正常通信，Leader 节点也会主动切换为 LOOKING 状态，将领导权让位于网络环境较好的节点。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zab 协议需要在崩溃恢复的过程中保证下面两个特性：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Zab 协议需要确保&lt;strong&gt;已经在 Leader 服务器上提交（Commit）的事务最终被所有服务器提交&lt;/strong&gt;；&lt;/li&gt;
  &lt;li&gt;Zab 协议需要确保&lt;strong&gt;丢弃那些只在 Leader 上提出但没有被提交的事务&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;一致性分析&quot;&gt;一致性分析&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;可线性化写：&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zookeeper 仅将写操作交给 Leader 串行执行，也就保证了写操作线性。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;顺序一致性读&lt;/p&gt;

    &lt;p&gt;（没有满足可线性化的条件）&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;同一个 client 看到的是与 leader 相同的变更序列&lt;/li&gt;
      &lt;li&gt;不同 client 看到的值变更（时间）有可能不同&lt;/li&gt;
    &lt;/ul&gt;

    &lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-2884b287105c41dbda28f08aec016db5_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;可线性化读：&lt;/strong&gt;可以利用写操作的可线性化特性，在读取之前执行一个写操作。原理是，Zookeeper 给每个写入后的状态一个唯一自增的 Zxid，并通过写请求的 resp 告知客户端，客户端之后的读请求都会携带这个 Zxid，直连的 Server 通过比较 Zxid 判断自己是否滞后，如果是则让读操作等待。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;chubby&quot;&gt;Chubby&lt;/h3&gt;

&lt;blockquote&gt;
  &lt;table&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;Ref [Chubby的锁服务&lt;/td&gt;
        &lt;td&gt;CatKang的博客](http://catkang.github.io/2017/09/29/chubby.html)，&lt;a href=&quot;https://www2.cs.uic.edu/~brents/cs494-cdcs/slides/thegooglechubbylockservice.pdf&quot;&gt;thegooglechubbylockservice.pdf (uic.edu)&lt;/a&gt;&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;

  &lt;p&gt;Chubby provide coarse-grained locking as well as reliable storage for a loosely-coupled distributed system.（读多写少）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;主节点选举&quot;&gt;主节点选举&lt;/h4&gt;

&lt;p&gt;Chubby 实际上实现了 Multi-Paxos，其概要实现如下：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;多个副本组成一个集群，副本通过一致性协议选出一个Master，集群在一个确定的租约时间内保证这个Master的领导地位；&lt;/li&gt;
  &lt;li&gt;Master周期性的向所有副本刷新延长自己的租约时间；&lt;/li&gt;
  &lt;li&gt;每个副本通过一致性协议维护一份数据的备份，&lt;strong&gt;而只有 Master 可以发起读写操作&lt;/strong&gt;；&lt;/li&gt;
  &lt;li&gt;Master挂掉或脱离集群后，其他副本发起选主，得到一个新的Master；&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;session-and-keepalives&quot;&gt;Session And KeepAlives&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;心跳时机：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Master 和 Client 之间通过 KeepAlive 进行通信，初始化时 Client 发起 KeepAlive，会被 Master 阻塞在本地&lt;/strong&gt;，直到Session租约临近过期，此时Master会延长租约时间，并返回阻塞的KeepAlive通知Client；&lt;/li&gt;
  &lt;li&gt;除此之外，Master 还可能在 Cache 失效或 Event 发生时返回 KeepAlive；&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-a163e42bb7d2fbb6e72840fac22114c7_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;cache&quot;&gt;&lt;strong&gt;Cache&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;从这里开始要提到的 Chubby 的机制是对 Client 透明的了。Chubby 对自己的定位是需要支持大量的Client，并且读请求远大于写请求的场景，因此引入一个对读请求友好的 Client 端 Cache，来减少大量读请求对 Chubby Master 的压力便十分自然，客户端可以完全不感知这个 Cache 的存在。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache 对读请求的极度友好体现在它牺牲写性能实现了一个一致语义的Cache：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Cache 可以缓存几乎所有的信息，包括数据，数据元信息，Handle 信息及 Lock；&lt;/li&gt;
  &lt;li&gt;Master 收到写请求时，会先阻塞写请求，通过返回所有客户端的 KeepAlive 来通知客户端 Invalidate 自己的 Cache；&lt;/li&gt;
  &lt;li&gt;Client 直接将自己的 Cache 清空并标记为 Invalid，并发送 KeepAlive 向 Master 确认；&lt;/li&gt;
  &lt;li&gt;Master 收到所有 Client 确认或等到超时后再执行写请求。（如果超时的话，会导致两个 client 同时持有锁吗？——租约）&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;fail-over&quot;&gt;Fail-over&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-7bedd8bef132f01226498037c6ba2ef2_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这里的多个临界条件，有没有可能存在锁冲突问题？ 下面进行分析&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;lease C2 还在，该 client 可以正常获取锁；lease M2 还在，其他 client 不能获取锁；&lt;/li&gt;
  &lt;li&gt;lease C2 不在，该 client 不可以获取锁；lease M2 还在，其他 client 不能获取锁；&lt;/li&gt;
  &lt;li&gt;新 master 启动，
    &lt;ol&gt;
      &lt;li&gt;选择新的 epoch，拒绝老 epoch 的所有 client 请求&lt;/li&gt;
      &lt;li&gt;与客户端重新建立 session，并携带新 epoch，将所有 client cache 置为失效状态&lt;/li&gt;
      &lt;li&gt;等待 client ack，若某 client 超时则终止其 session（保证了该时刻，所有有效 client 状态一致）&lt;/li&gt;
      &lt;li&gt;单 master 模型保证了竞争锁的 client 有且只有一个成功&lt;/li&gt;
    &lt;/ol&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;一致性分析-1&quot;&gt;一致性分析&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;可线性化：相当于「主从复制」模型，所有的读写操作都是走主节点来解决，实际上也实现了「全序关系广播」。&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;zookeeper-vs-chubby&quot;&gt;Zookeeper VS Chubby&lt;/h3&gt;

&lt;p&gt;先看看两者的定位：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;blockquote&gt;
      &lt;p&gt;&lt;strong&gt;Chubby&lt;/strong&gt;：provide coarse-grained locking as well as reliable storage for a loosely-coupled distributed system.&lt;/p&gt;
    &lt;/blockquote&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;blockquote&gt;
      &lt;p&gt;&lt;strong&gt;Zookeeper&lt;/strong&gt;：provide a simple and high performance kernel for building more complex coordination primitives for the client.&lt;/p&gt;
    &lt;/blockquote&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以看出，Chubby 旗帜鲜明的表示自己是为分布式锁服务的，而 Zookeeper 则倾向于构造一个“Kernel”，而利用这个“Kernel”客户端可以自己实现众多更复杂的分布式协调机制。自然的，&lt;strong&gt;Chubby倾向于提供更精准明确的操作来免除使用者的负担，Zookeeper 则需要提供更通用，更原子的原材料，留更多的空白和自由给 Client&lt;/strong&gt;。也正是因此，为了更适配到更广的场景范围，Zookeeper 对性能的提出了更高的要求。&lt;/p&gt;

&lt;h4 id=&quot;一致性&quot;&gt;&lt;strong&gt;一致性&lt;/strong&gt;&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;Chubby：线性一致性(Linearizability)&lt;/li&gt;
  &lt;li&gt;Zookeeper：写操作线性(Linearizable writes) + 客户端有序(FIFO client order)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Chubby 所要实现的一致性是分布式系统中所能实现的最高级别的一致性，简单的说就是&lt;strong&gt;每次操作时都可以看到其之前的所有成功操作按顺序完成&lt;/strong&gt;，而 Zookeeper 将一致性弱化为两个保证，其中写操作线性（Linearizable writes）指的是&lt;strong&gt;所有修改集群状态的操作按顺序完成&lt;/strong&gt;，客户端有序（FIFO client order）指&lt;strong&gt;对任意一个client来说，他所有的读写操作都是按顺序完成&lt;/strong&gt;。&lt;/p&gt;

&lt;h4 id=&quot;分布式锁&quot;&gt;分布式锁&lt;/h4&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-ff22f4e9b7cdf4d95219ec5f04d4a50b_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Chubby：提供准确语义的Lock，Release操作，内部完成了一致性协议，锁的实现的内容，仅将锁的使用部分留给用户；&lt;/li&gt;
  &lt;li&gt;Zookeeper：并没有提供加锁放锁操作，用户需要利用Zookeeper提供的基础操作，完成锁的实现和锁的使用部分的内容；&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;Netflix created &lt;a href=&quot;https://github.com/Netflix/curator&quot;&gt;curator&lt;/a&gt; library which later moved to &lt;a href=&quot;http://curator.apache.org/&quot;&gt;Apache&lt;/a&gt; foundation, &lt;strong&gt;this library provides the commonly used functionality and cache management&lt;/strong&gt;. This additional layer to zookeeper allows it providing strong consistency needed by some users. So whenever you want to use zookeeper, use curator library instead of native library unless you know what you are doing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h4 id=&quot;what-about-the-redlock-problem&quot;&gt;What about the RedLock Problem？&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;回到一开始的问题，redlock 的问题，zookeeper、chubby 能否解决呢？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;一个正确的算法一旦返回结果，那必须是正确的结果，这点 zk、chubby 都可以保证（例如 zk 在 commit point 的时候，多数节点状态达成一致；chubby 维护一致性 cache 保证所有正常 client 状态一致）；而 redlock 需要对返回的结果基于不可靠的时间进行判断，因此本身也是”neither fish nor fowl”&lt;/p&gt;

&lt;p&gt;&lt;em&gt;另外，Redis 锁需要自己实现续租逻辑，而 zk、chubby 不需要（使用 keepalive 长连接实现）。&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;一些与共识等价的问题&quot;&gt;一些与共识等价的问题&lt;/h2&gt;

&lt;p&gt;我们看到，达成共识意味着以这样一种方式决定某件事：所有节点一致同意所做决定，且这一决定不可撤销。通过深入挖掘，结果我们发现很广泛的一系列问题实际上都可以归结为共识问题，并且彼此等价（从这个意义上来讲，如果你有其中之一的解决方案，就可以轻易将它转换为其他问题的解决方案）。这些等价的问题包括：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;线性一致性的 CAS 寄存器&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;寄存器需要基于当前值是否等于操作给出的参数，原子地&lt;strong&gt;决定&lt;/strong&gt;是否设置新值。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;原子事务提交&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;数据库必须&lt;strong&gt;决定&lt;/strong&gt;是否提交或中止分布式事务。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;全序广播&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;消息系统必须&lt;strong&gt;决定&lt;/strong&gt;传递消息的顺序。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;锁和租约&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;当几个客户端争抢锁或租约时，由锁来&lt;strong&gt;决定&lt;/strong&gt;哪个客户端成功获得锁。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;成员/协调服务&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;给定某种故障检测器（例如超时），系统必须&lt;strong&gt;决定&lt;/strong&gt;哪些节点活着，哪些节点因为会话超时需要被宣告死亡。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;唯一性约束&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;当多个事务同时尝试使用相同的键创建冲突记录时，约束必须&lt;strong&gt;决定&lt;/strong&gt;哪一个被允许，哪些因为违反约束而失败。&lt;/p&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://redis.io/topics/distlock&quot;&gt;Distributed locks with Redis – Redis&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html&quot;&gt;How to do distributed locking — Martin Kleppmann’s blog&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;
    &lt;table&gt;
      &lt;tbody&gt;
        &lt;tr&gt;
          &lt;td&gt;[Is Redlock Safe? Reply to Redlock Analysis&lt;/td&gt;
          &lt;td&gt;Hacker News (ycombinator.com)](https://news.ycombinator.com/item?id=11065933)&lt;/td&gt;
        &lt;/tr&gt;
      &lt;/tbody&gt;
    &lt;/table&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www2.cs.uic.edu/~brents/cs494-cdcs/slides/thegooglechubbylockservice.pdf&quot;&gt;The Chubby lock service for loosely-coupled distributed systems.&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://vonng.gitbooks.io/ddia-cn/content/ch9.html&quot;&gt;第九章：一致性与共识 · ddia-cn (gitbooks.io)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Paxos：&lt;a href=&quot;https://www.youtube.com/watch?v=JEpsBg0AO6o&amp;amp;t=6s&quot;&gt;Paxos lecture(Recommended)&lt;/a&gt; + &lt;a href=&quot;https://en.wikipedia.org/wiki/Paxos_(computer_science)#&quot;&gt;Paxos Wikipedia&lt;/a&gt; + &lt;a href=&quot;https://lamport.azurewebsites.net/pubs/paxos-simple.pdf&quot;&gt;Paper(optional)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://wingsxdu.com/post/algorithms/distributed-consensus-and-data-consistent/#raft-与-zab&amp;amp;gsc.tab=0&quot;&gt;漫谈分布式共识算法与数据一致性 - beihai blog (wingsxdu.com)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
			<pubDate>Wed, 13 Oct 2021 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2021/10/13/%E4%BB%8E-RedLock-%E5%88%B0%E5%85%B1%E8%AF%86%E7%AE%97%E6%B3%95/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2021/10/13/%E4%BB%8E-RedLock-%E5%88%B0%E5%85%B1%E8%AF%86%E7%AE%97%E6%B3%95/</guid>
        
			<category>distributed-system</category>
        
        
		</item>
    
		<item>
			<title>优化程序性能</title>
			<description>&lt;h1 id=&quot;第五章-优化程序性能&quot;&gt;第五章 优化程序性能&lt;/h1&gt;

&lt;h1 id=&quot;0-优化之路咋走&quot;&gt;0. 优化之路咋走？&lt;/h1&gt;

&lt;p&gt;首先需要知道优化之路可以怎么走，下面摘自 CMU 的公开课 slides。主要有两方面因素：&lt;/p&gt;

&lt;p&gt;代码风格：良好的数据结构与算法思想，循环、变量等人为因素；
编译器与操作系统：了解编译器优化，从汇编、profile 等角度分析性能瓶颈，了解操作系统层面因素；&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-52dafcb512911ab2b882c041cc3cb9bf_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;1-量化程序性能&quot;&gt;1. 量化程序性能&lt;/h1&gt;

&lt;p&gt;使用 CPE（Cycles Per Element）来量化程序性能。其表示每个循环执行了多少个时钟周期（可以预估执行了多少个指令）。&lt;/p&gt;

&lt;p&gt;对于一个“4GHz”的处理器而言，时钟运行的频率是每秒 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4x10^9&lt;/code&gt; 个周期，即&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;一个时钟周期 = 0.25ns&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;例如，&lt;strong&gt;将程序的循环的&lt;/strong&gt; &lt;strong&gt;CPE&lt;/strong&gt; &lt;strong&gt;从 9 优化到 6，表示我们将程序中的每个循环，从消耗 9 个时钟周期，优化到 6 个时钟周期，节省了 3 个时钟周期。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-864b70095ba5024e206de2adceac5696_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;CPE 其实就是右图中的斜率，&lt;strong&gt;我们的优化目标是尽可能让直线躺平。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-84870d35766af848946b06c997c75a20_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;为什么要用-cpe&quot;&gt;为什么要用 CPE？&lt;/h2&gt;

&lt;p&gt;其实就是比较直观，而且容易跟运算单元的速度进行关联，&lt;strong&gt;可以很方便地估计出优化的幅度&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;下面是常见操作的CPE：&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-de7a80a0273f5bd7777f91bbf6a51f32_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;下面是对 capacity、latency、issue 的图形化描述：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;capacity：描述处理器单位时间能过处理的指令数量&lt;/li&gt;
  &lt;li&gt;latency：描述单位指令执行时间&lt;/li&gt;
  &lt;li&gt;issue：描述相同指令之间的执行间隔（可以翻译为发射时间）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-9ea27582d24adcc0edb4e9c9a135b6d3_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;通过各个模块的 CPE，快速估算出优化的幅度：&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;对于某台机器 load=2, mul=3, add=1&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
  &lt;li&gt;左图：关键路径消耗 (load+mul)&lt;em&gt;n=(2+3)&lt;/em&gt;n=&lt;strong&gt;5n 个时钟周期&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;右图：关键路径消耗 (load+mul+mul)&lt;em&gt;n/2=(2+3+3)&lt;/em&gt;n/2=&lt;strong&gt;4n 个时钟周期，速度提高 20%&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-bdf91c3d07ed070f8ffabad1a1f38643_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;当然，这个 CPE 在实际工作中计算出来是比较麻烦的，但是对于了解常见汇编指令的速度还是有一定帮助。&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;2-编译器优化的能力和局限性&quot;&gt;2. 编译器优化的能力和局限性&lt;/h1&gt;

&lt;h2 id=&quot;编译器能做啥&quot;&gt;编译器能做啥？&lt;/h2&gt;

&lt;p&gt;这里介绍几个编译器能做的优化。&lt;/p&gt;

&lt;p&gt;优化计算表达式：将复杂表达式转换为简单表达式，例如用位操作来代替乘法；&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-0c6fdf42f60d30b2d9e23ba9ccdcfe0a_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;使用公共变量：使用临时变量等方式来避免重复几计算；&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-4876145c4a99ab70ff6bda94a3d06352_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;编译器挫在哪&quot;&gt;编译器挫在哪？&lt;/h2&gt;

&lt;h3 id=&quot;挫一处理函数调用&quot;&gt;挫一：处理函数调用&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-b28bec969a58c881677b441585a503d6_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么编译器不能够发现，并将&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strlen&lt;/code&gt; &lt;strong&gt;提取到循环外？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-5fbb65b4fe971565c69f10791da31014_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;函数可能有副作用，例如修改某个全局变量&lt;/li&gt;
  &lt;li&gt;函数不一定是幂等的，例如依赖某个全局变量&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;编译器倾向于把函数调用当作黑盒，因为无法知道其副作用，所以不会对此进行优化。 —— 鲁迅&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;挫二内存别名引用&quot;&gt;挫二：内存别名引用&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-920888a4c2d7010a91086abbc4731738_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-5d07e083c4c3f0bee6926f69ffbba1b3_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;编译器不能优化这点吗？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;看上去编译器似乎能够优化这点，但是编译器会假设两个指针地址可能相同，因此必须非常谨慎，躺平不动。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-182e16d4946b894686efb14e72f13e9c_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;程序员要养成使用局部变量的习惯，显式地告诉编译器，这里没有内存别名。 —— 鲁迅&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;3-现代微处理器&quot;&gt;3. 现代微处理器&lt;/h1&gt;

&lt;p&gt;到目前为止，我们提及到的优化技巧，都不依赖于目标机器的任何特性。我们目前的优化，只是简单的降低了过程调用的开销、避开了编译器的挫。如果要进一步进行优化，必须考虑利用现代微处理器的优化，也就是处理器用来执行指令的底层系统设计。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-9c33589c72e723cddf021d921976b30d_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这样的处理器，在工业界称为超标量（Superscalar），&lt;strong&gt;即每个时钟周期可以执行多个指令，且是乱序执行&lt;/strong&gt;。整个处理器分为两大部分，指令控制单元（ICU）和执行单元（EU）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-aca2a7b65d4f556495410e48088c2d8d_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;指令控制单元&lt;/strong&gt;：从高速缓存中取指、译码，生成一组 low level 的基本操作；&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;执行单元&lt;/strong&gt;：执行上述基本操作；包括多个功能单元，比如 arith（算术运算）、load（内存读）、store（内存写）等等，分别负责各自独立的计算和存取内存操作。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;当程序遇到分支的时候，程序可能有两个前进的方向。现代处理器使用了&lt;strong&gt;分支预测&lt;/strong&gt;技术（Branch Prediction），会猜测是否选择分支，且会预测分支跳转的目标地址。然后使用&lt;strong&gt;投机执行&lt;/strong&gt;（Speculative Execution）技术对目标分支跳转到的指令进行取指和译码（甚至在分支预测之前就开始投机执行）。如果之后确定分支预测错误，则会将寄存器状态重置为分支点的状态，并开始取出和执行另一个分支上的指令。所以可以看到，&lt;strong&gt;分支预测错误会导致很大的性能开销。&lt;/strong&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;4-让处理器助你一臂之力吧&quot;&gt;4. 让处理器助你一臂之力吧！&lt;/h1&gt;

&lt;h2 id=&quot;循环展开&quot;&gt;循环展开&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-8361747f231f7e18824b729bba85b60a_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;优化前后的关键路径对比，&lt;strong&gt;可以看到每 2 个 Element，节省了一个 load 操作，速度提高 20%：&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-5f4d6cdda7330beaf4c1d909ba6348d6_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;提高并行性&quot;&gt;提高并行性&lt;/h2&gt;

&lt;h3 id=&quot;分治法&quot;&gt;分治法&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-a4c2a5ae6b34407ad52fa506fa295d20_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;优化前后的关键路径对比，&lt;strong&gt;可以看到每个 2 个 Element，节省了 mul 操作，速度提高 1x 多：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;左边关键路径：load+2*mul=8&lt;/li&gt;
  &lt;li&gt;右边关键路径：load+mul=5&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-25b4ed4e4cccad04993fc2d01a30c17b_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;重新结合运算&quot;&gt;重新结合运算&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/v2-591a7d89ac9e4b205aa415353aa9dda0_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;优化前后的关键路径对比，&lt;strong&gt;可以看到每个 4 个 Element，节省了 2mul+2load 操作，速度提高 2x 多：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;左边关键路径：(load+2mul)2=16&lt;/li&gt;
  &lt;li&gt;右边关键路径：2*mul=6&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-f18d1306b38b8c0a0f3e7e955c7764b3_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;

&lt;p&gt;本质：解除数据依赖&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;分治法：利用每个子问题的并行执行提高速度&lt;/li&gt;
  &lt;li&gt;重新结合变换：解除多项式操作中，项之间的关键依赖&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;分支&quot;&gt;分支&lt;/h2&gt;

&lt;h3 id=&quot;书写容易预测的代码&quot;&gt;书写容易预测的代码&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//stackoverflow.com/questions/11227809/why-is-processing-a-sorted-array-faster-than-processing-an-unsorted-array&quot;&gt;Why is processing a sorted array faster than processing an unsorted array? - Stack Overflow&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;现代处理器可以很好预测分支指令的&lt;strong&gt;有规律模式&lt;/strong&gt;。&lt;/p&gt;

&lt;div class=&quot;language-text highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;if (data[c] &amp;gt;= 128)
    sum += data[c];

T = branch taken
N = branch not taken


data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...
       = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT  (easy to predict)


data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118,  14, 150, 177, 182, ...
branch =   T,   T,   N,   T,   T,   T,   T,  N,   T,   N,   N,   T,   T,   T  ...
       = TTNTTTTNTNNTTT ...   (completely random - impossible to predict)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://www.zhihu.com/question/23973128&quot;&gt;CPU 的分支預測器是怎樣工作的？&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-765bcfa1a3f9a7cb2392c7395688557b_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;书写适合条件传送的代码&quot;&gt;书写适合&lt;strong&gt;条件传送&lt;/strong&gt;的代码&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-a8140e15385d26862c0ca72064e3303d_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;条件跳转&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-fb189135b7a1e068850edcab11e51d7b_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;条件传送&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-3e10044c588dfe84a290b56be716359f_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么条件传送更快？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;先计算，再选择结果。且这些计算没有数据依赖，可以充分利用处理器的指令流水。&lt;/p&gt;

&lt;h2 id=&quot;simd&quot;&gt;SIMD&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;SIMD（&lt;strong&gt;Single Instruction Multiple Data&lt;/strong&gt;&lt;strong&gt;）&lt;/strong&gt;即单指令流多数据流，是一种采用一个控制器来控制多个执行器，同时对一组数据（又称“数据向量”）中的每一个分别执行相同的操作从而实现空间上的并行性的技术。简单来说就是一个指令能够同时处理多个数据。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href=&quot;https://link.zhihu.com/?target=http%3A//csapp.cs.cmu.edu/3e/waside/waside-simd.pdf&quot;&gt;Achieving Greater Parallelism with SIMD Instructions&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/v2-fac68f950f2acc170c6ea3bd4d48fb9e_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;5-小结&quot;&gt;5. 小结&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;良好的编码习惯
    &lt;ul&gt;
      &lt;li&gt;数据结构与算法&lt;/li&gt;
      &lt;li&gt;消除编译器优化障碍
        &lt;ul&gt;
          &lt;li&gt;过程调用&lt;/li&gt;
          &lt;li&gt;内存别名引用&lt;/li&gt;
        &lt;/ul&gt;
      &lt;/li&gt;
      &lt;li&gt;优化循环&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;帮助机器更好地优化
    &lt;ul&gt;
      &lt;li&gt;利用指令并行&lt;/li&gt;
      &lt;li&gt;避免不可预测的分支&lt;/li&gt;
      &lt;li&gt;提高指令缓存命中率&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;本文所讲的这些优化方法，在大部分编译器中都已经实现了。&lt;/strong&gt;但是它们有可能不会实行这些优化，或需要我们手动设置更高级别的优化选项才行。所以，&lt;strong&gt;作为一个程序员，我们应该做的是尽量引导编译器执行这些优化，或者说排除阻碍编译器优化的障碍。&lt;/strong&gt;这样可以使我们的代码在保持简洁的情况下获得更高的性能。迫不得已时，我们才去手动做这些优化。&lt;/p&gt;

&lt;p&gt;另外，循环展开、多路并行并不是越多越好。因为寄存器的个数是有限的，x86-64 最多只能有 12 个寄存器用于累加，如果局部变量的个数多于 12 个，就会被放进存储器，反倒严重拉低程序性能。&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;6-身边活生生的例子&quot;&gt;6. 身边活生生的例子&lt;/h1&gt;

&lt;h2 id=&quot;数据依赖&quot;&gt;数据依赖&lt;/h2&gt;

&lt;p&gt;组内有同学反馈，跑 pprof 的时候，发现一个简单的函数调用占用了 20% 左右的 CPU 时间，这个函数只是取了一个对象里面的列表元素做简单计算。只不过对象潜套了多个指针，于是怀疑是指针嵌套的问题。&lt;/p&gt;

&lt;p&gt;我把对应的代码抽出来，对比了直接引用（左）与指针嵌套引用（右）的汇编代码区别：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic2.zhimg.com/v2-a11bafb119b4992bf2f2b041e795ced9_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;可以发现，右侧代码中，红圈中的 5 行指令都是在 DX 寄存器中进行操作，形成了数据依赖，无法充分利用指令流水。&lt;/p&gt;

&lt;h2 id=&quot;指令缓存&quot;&gt;指令缓存&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;摘自内部 infra 组同学的一篇文章，描述的是 RPC 框架一个小改动带来的性能问题。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;事故现场：&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//editor.mergely.com/HPqQpVCJ/&quot;&gt;Mergely - Diff online, merge documents&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;可以看到，相比旧版本的生成代码，该 commit 在返回错误的时候会额外包装一下：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;if err := ...; err != nil {
    return thrift.PrependError(fmt.Sprintf(&quot;%T read field x &apos;xxx&apos; error: &quot;, p), err)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;而旧版本的生成代码是直接返回的错误：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&quot;language-Go&quot;&gt;if err := ...; err != nil {
    return err
}
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;虽然这些只是在发生错误的时候才会调用到，在正常流程中不会用到，但是生成的汇编代码中这段逻辑占了相当大的比例：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic4.zhimg.com/v2-a7628ca8d1e68fc73db83d78e0c64fa7_r.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;而 Go 的编译器并没有帮我们重排这些指令，导致在真正运行的时候，L1 cache miss 大大提高，极大地降低了性能。&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;拓展阅读&quot;&gt;拓展阅读&lt;/h1&gt;

&lt;p&gt;以下是 golang 一些性能分析的拓展阅读，有兴趣可以看。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//github.com/sxs2473/go-performane-tuning/blob/master/3.%E6%80%A7%E8%83%BD%E6%B5%8B%E9%87%8F%E5%92%8C%E5%88%86%E6%9E%90/%E6%80%A7%E8%83%BD%E6%B5%8B%E9%87%8F%E5%92%8C%E5%88%86%E6%9E%90.md&quot;&gt;Golang 性能测量和分析&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//blog.wolfogre.com/posts/go-ppof-practice/&quot;&gt;golang pprof 实战&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//eddycjy.com/posts/go/tools/2018-09-15-go-tool-pprof/&quot;&gt;Go 大杀器之性能剖析 PProf&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://link.zhihu.com/?target=https%3A//juejin.cn/post/6844903887757901831&quot;&gt;Golang 大杀器之跟踪剖析 trace&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;CMU 课件 &lt;a href=&quot;https://link.zhihu.com/?target=http%3A//www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/lectures/10-optimization.pdf&quot;&gt;10-optimization&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;CMU 电子书、课程等资料地址 &lt;a href=&quot;https://link.zhihu.com/?target=https%3A//github.com/wangmu89/Book-CSAPP&quot;&gt;wangmu89/Book-CSAPP: 深入理解计算机系统&lt;/a&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;https://github.com/wangmu89/Book-CSAPP)&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Sun, 22 Aug 2021 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2021/08/22/%E4%BC%98%E5%8C%96%E7%A8%8B%E5%BA%8F%E6%80%A7%E8%83%BD-CSAPP-%E7%AC%94%E8%AE%B0/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2021/08/22/%E4%BC%98%E5%8C%96%E7%A8%8B%E5%BA%8F%E6%80%A7%E8%83%BD-CSAPP-%E7%AC%94%E8%AE%B0/</guid>
        
			<category>os</category>
        
        
		</item>
    
		<item>
			<title>Elasticsearch 概览</title>
			<description>&lt;h1 id=&quot;elasticsearch---an-overview&quot;&gt;Elasticsearch - An Overview&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;TL;DR; 本文从分布式架构、数据读写、应用场景对 Elasticsearch 做一个概括性介绍，让读者了解 ES 是什么，能在哪些场景应用，为什么 ES 这么快。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;introduction&quot;&gt;Introduction&lt;/h1&gt;

&lt;p&gt;这个世界已然被数据淹没，但是如果数据只是躺在磁盘里面根本就毫无用处。Elasticsearch（ES）是一个分布式、可扩展、文档存储、实时的搜索与数据分析引擎。 它能从落库开始就赋予你的数据以检索、搜索、分析的能力。Elasticsearch 基于 Lucene。
Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库—无论是开源还是私有。
Elasticsearch 底层依赖 Lucene，通过隐藏 Lucene 的复杂性，取而代之的提供一套简单一致的 RESTful API。&lt;/p&gt;

&lt;h1 id=&quot;concept&quot;&gt;Concept&lt;/h1&gt;

&lt;p&gt;首先看下 ES 文档存储的几个重要概念：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;&lt;strong&gt;名称&lt;/strong&gt;&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;概念&lt;/strong&gt;&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;对应关系型数据库概念&lt;/strong&gt;&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;说明&lt;/strong&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;index&lt;/td&gt;
      &lt;td&gt;索引&lt;/td&gt;
      &lt;td&gt;Database&lt;/td&gt;
      &lt;td&gt;具有相似特点的文档的集合，可以对应为关系型数据库中的数据库，通过名字在集群内唯一标识&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;type&lt;/td&gt;
      &lt;td&gt;文档类别&lt;/td&gt;
      &lt;td&gt;Table&lt;/td&gt;
      &lt;td&gt;索引内部的逻辑分类，可以对应为 Mysql 中的表&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;document&lt;/td&gt;
      &lt;td&gt;文档&lt;/td&gt;
      &lt;td&gt;Row&lt;/td&gt;
      &lt;td&gt;构成索引的最小单元，属于一个索引的某个类别，从属关系为： Index -&amp;gt; Type -&amp;gt; Document，通过 id 在Type 内唯一标识&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;field&lt;/td&gt;
      &lt;td&gt;字段&lt;/td&gt;
      &lt;td&gt;Column&lt;/td&gt;
      &lt;td&gt;构成文档的单元&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;然后是 ES 集群相关几个重要概念：&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;&lt;strong&gt;名称&lt;/strong&gt;&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;概念&lt;/strong&gt;&lt;/th&gt;
      &lt;th&gt;&lt;strong&gt;说明&lt;/strong&gt;&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;cluster&lt;/td&gt;
      &lt;td&gt;集群&lt;/td&gt;
      &lt;td&gt;一个或多个 Node 的集合，ES 可以通过跨集群的备份，来提高服务稳定性&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Node&lt;/td&gt;
      &lt;td&gt;节点&lt;/td&gt;
      &lt;td&gt;运行 ES 的单个实例，保存数据并具有索引和搜索的能力，可以包含多个 Shard&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Shard&lt;/td&gt;
      &lt;td&gt;分片&lt;/td&gt;
      &lt;td&gt;索引分为多个块，每块叫做一个 Shard。索引定义时需要指定分片数且不能更改（因为再分片相当于重建索引）。&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Replica&lt;/td&gt;
      &lt;td&gt;分片的备份&lt;/td&gt;
      &lt;td&gt;每个分片默认一个 Replica，它可以提升节点的可用性，同时能够提升搜索时的并发性能（搜索可以在全部分片上并行执行）&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h1 id=&quot;distributed-architecture&quot;&gt;Distributed Architecture&lt;/h1&gt;

&lt;h2 id=&quot;data-replication&quot;&gt;Data Replication&lt;/h2&gt;

&lt;p&gt;ES 为主备架构，即 Shard 分为 Primary Shard 及 Replica Shard，写请求走 Primary Shard，读请求则均衡打到所有 Shard，Replica Shard 的数据从 Primary Shard 同步。
下图是含有 3 个 Node 的集群：
&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1614694387246_13f3040b660770188c1e520890d0d88f&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;蓝色部分：有 3 个 Shard，分别是 P1，P2，P3，位于 3 个不同的 Node 中，这里没有 Replica&lt;/li&gt;
  &lt;li&gt;绿色部分：有 2 个 Shard，分别是 P1、P2，位于 2 个不同的 Node 中，每个 Shard 都有一个 Replica Shard，分别是 R1，R2。基于可用性考虑，同一个 Shard 的 Primary 和 Replica 节点不能处于同一个 Node 中。这里 Shard1 的 P1 和 R1 分别位于 Node3 和 Node2 中，如果某一刻 Node2 发生宕机，服务基本不会受影响，因为还有一个 P1 和 R2 都还是可用的。因为是主备架构，当主分片发生故障时，需要切换，这时候需要选举一个副本作为新主，这里除了会耗费一些时间外，也会有丢失数据的风险。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;distributed-searching&quot;&gt;Distributed Searching&lt;/h2&gt;

&lt;p&gt;现在从全局视角，来看一个分布式搜索是如何执行的。搜索不同于 CRUD 操作，在 CRUD 操作中，我们是知道具体集群中哪个 Shard 含有该文档。搜索则比较复杂，因为目标文档可能存在集群中任何 Shard 上面。&lt;/p&gt;

&lt;h3 id=&quot;query&quot;&gt;Query&lt;/h3&gt;

&lt;p&gt;在 Query 阶段， 查询会广播到索引中每一个分片拷贝。 每个分片在本地执行搜索并构建一个匹配文档的 topk 队列。
&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1614694387149_b82a505e0413c9985a81f92e05622eaa&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;客户端发送一个 search 请求到随机一个节点，这里是 Node3，Node3 会创建一个大小为 K 的优先队列（K 为请求的分页参数 from 和 size 决定）&lt;/li&gt;
  &lt;li&gt;Node3 将请求转发到索引的每个分片中（primary 或者 replica 都有可能）。每个分片在本地执行搜索请求，将结果排序并放到大小 K 的优先队列中。&lt;/li&gt;
  &lt;li&gt;每个分片返回各自优先队列中的文档 ID 以及排序 Score 给协调节点，即 Node3，Node3 对所有结果进行合并、排序，得到一个全局排序后的文档 ID 列表。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;fetch&quot;&gt;Fetch&lt;/h3&gt;

&lt;p&gt;查询阶段标识哪些文档 ID 满足搜索请求，Fetch 阶段则用来取回这些文档。
&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1614694387155_a7342b7ecc5ae3c96661b829e20a2822&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;协调节点即 Node3，根据 Query 得到的文档 ID 列表，向相关分片提交多个 GET 请求。&lt;/li&gt;
  &lt;li&gt;每个分片加载文档返回给 Node3&lt;/li&gt;
  &lt;li&gt;等待所有文档取回，将结果合并返回给客户端&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;data-in&quot;&gt;Data in&lt;/h1&gt;

&lt;p&gt;通过使用 index API ，文档可以被索引（indexing） —— 存储和使文档可被搜索。 但是首先，我们要确定文档的位置。一个文档的唯一标示，由 _index 、 _type 和 _id 决定。 我们可以提供自定义的 _id 值，或者让 index API 自动生成。
下面简单看下 ES 提供的 CRUD 接口。&lt;/p&gt;

&lt;h2 id=&quot;write&quot;&gt;Write&lt;/h2&gt;

&lt;p&gt;我们可以提供自定义的 _id 值，或者让 index API 自动生成。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;PUT /{index}/{type}/{id}
{
  &quot;field&quot;: &quot;value&quot;,
  ...
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;客户端发送请求给随机一个 Node，这个 Node 就是 coordinating node （协调节点）。&lt;/li&gt;
  &lt;li&gt;coordinating node 对 document 进行&lt;strong&gt;路由&lt;/strong&gt;，将请求转发给对应的 Node（有 primary shard）。&lt;/li&gt;
  &lt;li&gt;实际的 node 上的 primary shard 处理请求，然后将数据同步到 replica node 。&lt;/li&gt;
  &lt;li&gt;coordinating node 如果发现 primary node 和所有 replica node 都写入完成之后，就返回响应结果给客户端。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;read&quot;&gt;Read&lt;/h2&gt;

&lt;p&gt;为了从 Elasticsearch 中检索出文档，我们仍然使用相同的 _index , _type , 和 _id ，但是 HTTP 方法更改为 GET :&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GET /{index}/{type}/{id}?pretty
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;响应体包括目前已经熟悉了的元数据元素，再加上 _source 字段，这个字段包含我们索引数据时发送给 Elasticsearch 的原始 JSON 文档：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;_index&quot; :   &quot;website&quot;,
  &quot;_type&quot; :    &quot;blog&quot;,
  &quot;_id&quot; :      &quot;123&quot;,
  &quot;_version&quot; : 1,
  &quot;found&quot; :    true,
  &quot;_source&quot; :  {
      &quot;field&quot;: &quot;value&quot;,
      ...
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;客户端发送请求到&lt;strong&gt;任意&lt;/strong&gt;一个 Node，称为 coordinate node 。&lt;/li&gt;
  &lt;li&gt;coordinate node 对 doc id 进行哈希路由，将请求转发到对应的 Node，此时会使用 round-robin &lt;strong&gt;随机轮询算法&lt;/strong&gt;，在 primary shard 以及其所有 replica 中随机选择一个，让读请求负载均衡。&lt;/li&gt;
  &lt;li&gt;接收请求的 Node 返回 document 给 coordinate node 。&lt;/li&gt;
  &lt;li&gt;coordinate node 返回 document 给客户端。&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;当然，ES 还提供 update、delete 接口用来更新、删除文档，形式与上述相同在此不再赘述。另外，还有用于批量查询、批量操作的 mget、bulk 接口，感兴趣可以查阅官方权威指南。
需要注意的是，update 操作其实相当于创建一个新文档、删除旧文档的过程。此外，ES 通过使用多版本控制算法来进行并发写冲突解决。&lt;/p&gt;

&lt;h1 id=&quot;information-out&quot;&gt;Information out&lt;/h1&gt;

&lt;p&gt;Elasticsearch 除了可以提供文档及其元数据存储之外，其最强大的莫过于基于 Lucene 而提供的搜索能力。&lt;/p&gt;

&lt;h2 id=&quot;search&quot;&gt;Search&lt;/h2&gt;

&lt;p&gt;一个搜索请求可以包含一个或多个 query 来指定搜索参数。匹配结果则返回在 response 中的 hits 中。
下面例子列举了一个简单的搜索请求，即查询 user.id 为 kimchy 的所有文档（假设该字段为 keyword，即精确匹配。字段类型，即 mapping 可以参考 Indexing 章节）&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;GET /my-index-000001/_search
{
  &quot;query&quot;: {
    &quot;match&quot;: {
      &quot;user.id&quot;: &quot;kimchy&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;默认返回匹配搜索结果的 top10 文档，上面例子只返回一条：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  &quot;took&quot;: 5,
  &quot;timed_out&quot;: false,
  &quot;_shards&quot;: {
    &quot;total&quot;: 1,
    &quot;successful&quot;: 1,
    &quot;skipped&quot;: 0,
    &quot;failed&quot;: 0
  },
  &quot;hits&quot;: {
    &quot;total&quot;: {
      &quot;value&quot;: 1,
      &quot;relation&quot;: &quot;eq&quot;
    },
    &quot;max_score&quot;: 1.3862942,
    &quot;hits&quot;: [
      {
        &quot;_index&quot;: &quot;my-index-000001&quot;,
        &quot;_type&quot;: &quot;_doc&quot;,
        &quot;_id&quot;: &quot;kxWFcnMByiguvud1Z8vC&quot;,
        &quot;_score&quot;: 1.3862942,
        &quot;_source&quot;: {
          &quot;@timestamp&quot;: &quot;2099-11-15T14:12:12&quot;,
          &quot;http&quot;: {
            &quot;request&quot;: {
              &quot;method&quot;: &quot;get&quot;
            },
            &quot;response&quot;: {
              &quot;bytes&quot;: 1070000,
              &quot;status_code&quot;: 200
            },
            &quot;version&quot;: &quot;1.1&quot;
          },
          &quot;message&quot;: &quot;GET /search HTTP/1.1 200 1070000&quot;,
          &quot;source&quot;: {
            &quot;ip&quot;: &quot;127.0.0.1&quot;
          },
          &quot;user&quot;: {
            &quot;id&quot;: &quot;kimchy&quot;
          }
        }
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;此外，ES 搜索请求还支持很多选项，例如&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Query DSL：ES 提供一种基于 JSON 的请求体，并提供一些内置的请求类型，可以供用户自由进行组合。&lt;/li&gt;
  &lt;li&gt;聚合：可以使用 aggregation 即聚合操作，对搜索结果进行统计分析&lt;/li&gt;
  &lt;li&gt;多重搜索：可以使用使用正则式或者逗号分隔符搜索多个索引，例如 GET /my-index-000001,my-index-000002/_search&lt;/li&gt;
  &lt;li&gt;分页：默认 ES 只返回前 10 条匹配结果，但是 ES 也提供了由 from+size 组合的分页参数&lt;/li&gt;
  &lt;li&gt;获取指定字段：默认搜索结果返回整个文档，ES 也支持获取文档字段的子集&lt;/li&gt;
  &lt;li&gt;排序：默认搜索结果按照相关度进行排序，ES 也支持&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-script-score-query.html&quot;&gt;script_score&lt;/a&gt; 定制化分数计算。&lt;/li&gt;
  &lt;li&gt;异步搜索：某些搜索请求可能需要跨多个分片进行，并且分片数据很大，这时候搜素可能要花费较长时间。ES 提供异步接口，可以提交搜索请求、查询搜索状态、获取搜索结果。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;analyze&quot;&gt;Analyze&lt;/h2&gt;

&lt;p&gt;分析（analyze）其实就是基于聚合（aggregation）能力进行各种统计分析，得到一些统计报表。aggregation 可以帮你回答类似下面的问题：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;在大海里有多少针？&lt;/li&gt;
  &lt;li&gt;针的平均长度是多少？&lt;/li&gt;
  &lt;li&gt;按照针的制造商来划分，针的长度中位值是多少？&lt;/li&gt;
  &lt;li&gt;每月加入到海中的针有多少？&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;总的来说，Elasticsearch（7.11 版本）提供三种类型的聚合功能。&lt;/p&gt;

&lt;h3 id=&quot;metric-aggregation&quot;&gt;Metric Aggregation&lt;/h3&gt;

&lt;p&gt;统计指标，类似字段的和、均值、方差等等，可以从文档中提取某字段通过内置聚合函数进行聚合，或者通过脚本进行求值。
ES 提供非常丰富的聚合函数，下面举几个例子：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Geo-bounds：返回给定字段的边界点坐标，即 top_left、bottom_right&lt;/li&gt;
  &lt;li&gt;Stats：返回给定字段的 min, max, sum, count 和 avg 信息&lt;/li&gt;
  &lt;li&gt;Cardinality：给定字段和计算精度，返回给定字段的近似基数&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;bucket-aggregation&quot;&gt;Bucket Aggregation&lt;/h3&gt;

&lt;p&gt;将文档进行聚合，类似关系型数据库的 groupby，当然 bucket 也可以支持嵌套，例如 province bucket 下面嵌套 city bucket。
ES 提供非常丰富的聚合函数，下面举几个例子：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Sampler：一种过滤器，只返回相关性高的文档，去掉长尾的低质文档&lt;/li&gt;
  &lt;li&gt;Date range：提供人性化的时间区间（相对、绝对），根据时间进行聚合&lt;/li&gt;
  &lt;li&gt;Geo-Distance：给定点，在二维坐标点中根据距离区间进行聚合&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;pipeline-aggregation&quot;&gt;Pipeline Aggregation&lt;/h3&gt;

&lt;p&gt;将其他 aggregation 结果进行聚合，而不是简单对匹配文档进行聚合计算。例如，你想聚合统计全国每个省份的 GDP，得到一个 province_gdp 的 bucket；你可以通过 pipeline aggregation 对 province_gdp 进一步进行聚合，例如取得 max 的省份的 GDP。
ES 提供非常丰富的聚合函数，总的来说与 Metric Aggregation 类似，具体可以参阅 &lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline.html&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;

&lt;h1 id=&quot;indexing&quot;&gt;Indexing&lt;/h1&gt;

&lt;h2 id=&quot;mapping&quot;&gt;Mapping&lt;/h2&gt;

&lt;p&gt;为了能够将时间域视为时间，数字域视为数字，字符串域视为全文或精确值字符串， Elasticsearch 需要知道每个域中数据的类型。这个信息包含在映射（mapping）中。
当你索引一个包含新域的文档的时候，Elasticsearch 会使用 &lt;a href=&quot;https://www.elastic.co/guide/cn/elasticsearch/guide/2.x/dynamic-mapping.html&quot;&gt;动态映射&lt;/a&gt; ，通过JSON中基本数据类型，尝试猜测域类型。
这意味着如果你通过引号( “123” )索引一个数字，它会被映射为 string 类型，而不是 long 。但是，如果这个域已经映射为 long ，那么 Elasticsearch 会尝试将这个字符串转化为 long ，如果无法转化，则抛出一个异常。
尽管在很多情况下基本数据类型已经够用，但你经常需要为某些域自定义映射，特别是字符串域。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;域最重要的属性是 type 。对于不是 string 的域，一般只需要设置 type 。&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
    &quot;number_of_clicks&quot;: {
        &quot;type&quot;: &quot;integer&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;string 类型域则默认包含全文。就是说，它们的值在创建索引前，会通过一个分析器，针对于这个域的查询在搜索前也会经过一个分析器。string 域映射的两个最重要属性是 index 和 analyzer 。&lt;/p&gt;

    &lt;ul&gt;
      &lt;li&gt;
        &lt;p&gt;index：控制如何索引字符串，string 类型默认是 analized&lt;/p&gt;

        &lt;ul&gt;
          &lt;li&gt;analyzed：首先分析字符串，然后构建全文索引&lt;/li&gt;
          &lt;li&gt;not_analyzed：索引这个域，但索引的是精确值&lt;/li&gt;
          &lt;li&gt;no：不索引这个域，即无法被搜索&lt;/li&gt;
        &lt;/ul&gt;

        &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
    &quot;tag&quot;: {
        &quot;type&quot;:     &quot;string&quot;,
        &quot;index&quot;:    &quot;not_analyzed&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/li&gt;
      &lt;li&gt;
        &lt;p&gt;analyzer：对于 analyzed 字符串域，用 analyzer 属性指定在搜索和索引时使用的分析器&lt;/p&gt;

        &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
    &quot;tweet&quot;: {
        &quot;type&quot;:     &quot;string&quot;,
        &quot;analyzer&quot;: &quot;english&quot;
    }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;        &lt;/div&gt;
      &lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;当你首次创建一个索引的时候，可以指定类型的映射。你也可以使用 /_mapping 为新类型增加映射，但是不能修改现有的映射，例如将一个存在的域从 analyzed 改为 not_analyzed。&lt;/p&gt;

&lt;h2 id=&quot;index-structure&quot;&gt;Index Structure&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;索引创建：就是从语料库中提取信息，创建索引的过程。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;搜索索引：就是得到用户的查询请求，搜索创建的索引，然后返回结果的过程。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;ES 基于 Lucene，而 Lucene 的索引结构为倒排索引，大致如图所示
&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1614694387216_ac8a56b9db9758cf5f1eebdd3ff69ccc&quot; alt=&quot;&quot; /&gt;
我们通常习惯使用正排索引，即形如 user_id =&amp;gt; user_info 的映射；而倒排索引则反过来，通过形如 user_info.name =&amp;gt; user_ids，或者 user_info.age =&amp;gt; user_ids 得到一个逆映射，并且映射的 ID 值通常是一个列表。&lt;/p&gt;

&lt;h3 id=&quot;posting-list&quot;&gt;Posting List&lt;/h3&gt;

&lt;p&gt;Posting List 其实就是文档 ID 列表，例如上述例子的 user_ids。&lt;/p&gt;

&lt;h3 id=&quot;term-dictionary&quot;&gt;Term Dictionary&lt;/h3&gt;

&lt;p&gt;Term 指的是文档中的字段值。例如，name 字段可以有很多个 Term，比如：Carla，Sara，Elin，Ada，Patty，Kate，Selena。如果没有排序，那么找出某个特定的 Term 会很慢，因为 Term 没有排序，需要全部遍历一遍才能找到特定的 Term，排序之后就变成了：Ada，Carla，Elin，Kate，Patty，Sara，Selena，这样就可以用二分搜索，快速找到目标的 Term。
而如何组织这些 Term的方式就是 Term Dictionary，除了存储 Term 的值之外，还存储 Term 的统计值例如词频。有了 Term Dictionary 之后，就可以用比较少的比较次数和磁盘读次数查找目标。&lt;/p&gt;

&lt;h3 id=&quot;term-index&quot;&gt;Term Index&lt;/h3&gt;

&lt;p&gt;通常 Index 的量级非常大，因此 Term Dictionary 也非常大，无法直接 load 到内存中，因此需要一种保存在内存中的压缩的数据结构来加速读取。Term Index 其实就是一种前缀树（也是一种有限状态机，FST），通过 Term Index 可以快速定位到目标 Term 在 Dictionary 文件中的 offset。
因此通过这么一条链路：Term Index =&amp;gt; Term Dictionary =&amp;gt; Posting List，通过 Posting List 里的文档 ID 查询，得到我们的结果文档，并根据相关度进行排序。&lt;/p&gt;

&lt;h1 id=&quot;application&quot;&gt;Application&lt;/h1&gt;

&lt;p&gt;ElaticSearch 可以有非常丰富的应用场景，笔者没办法进行全面列举，只选择了主要的三个：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;数据存储：建立 searchable 的文档数据、目录数据、日志数据系统&lt;/li&gt;
  &lt;li&gt;作为数据库的补充：例如利用 CDC 功能，对数据库内容添加 ES 索引，可以进行可视化等分析操作；甚至与 Hadoop 进行交互，对 Hadoop 数据提供快速的搜索、分析、可视化能力&lt;/li&gt;
  &lt;li&gt;数据分析：对存储的数据进行统计、分析、可视化&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;conclusions&quot;&gt;Conclusions&lt;/h1&gt;

&lt;p&gt;Elasticsearch 是一个分布式的，RESTful 的分析引擎及搜索引擎。很多公司都转型使用 ES 融入其后端基础架构，因为 ES 提供很多能力：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;对海量数据进行聚合分析，得到一个数据的宏观模型&lt;/li&gt;
  &lt;li&gt;支持多样化的搜索及分析能力：精确匹配、模糊匹配、地理坐标搜索、统计分析&lt;/li&gt;
  &lt;li&gt;实时的处理能力&lt;/li&gt;
  &lt;li&gt;提供多种语言的客户端或 SDK，例如 SQL、Python、Java&lt;/li&gt;
  &lt;li&gt;ELK Stack 提供方便的数据收集、可视化功能&lt;/li&gt;
&lt;/ul&gt;

&lt;h1 id=&quot;references&quot;&gt;References&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.elastic.co/guide/cn/elasticsearch/guide/2.x/index.html&quot;&gt;Elasticsearch: 权威指南&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html&quot;&gt;Elasticsearch: Reference&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Mon, 08 Mar 2021 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2021/03/08/Elasticsearch-%E6%A6%82%E8%A7%88/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2021/03/08/Elasticsearch-%E6%A6%82%E8%A7%88/</guid>
        
			<category>database</category>
        
        
		</item>
    
		<item>
			<title>Flink 概览</title>
			<description>&lt;h1 id=&quot;flink---an-overview&quot;&gt;Flink - An Overview&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;TL;DR 本文简要介绍了 Flink 的架构及提供的特性，梳理了流处理中几个关键概念，例如时间、窗口、流状态，并对比了流处理在一些应用场景下的优势。让读者了解，Flink 是什么，用 Flink 能做些什么。如有疏漏，欢迎指正。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1 id=&quot;overview&quot;&gt;Overview&lt;/h1&gt;

&lt;p&gt;Apache Flink 是一个在无界和有界数据流上进行状态计算的框架和分布式处理引擎。 Flink 已经可以在所有常见的集群环境中运行，并以 in-memory 的速度和任意的规模进行计算。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841322_229f7ce9bacfa415e49cbd222466df16&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;批处理&lt;/strong&gt;针对的是有界数据流。在这种模式下，你可以选择在计算结果输出之前输入整个数据集，这也就意味着你可以对整个数据集的数据进行排序、统计或汇总计算后再输出结果。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;流处理&lt;/strong&gt;正相反，其涉及无界数据流。至少理论上来说，它的数据输入永远不会结束，因此程序必须持续不断地对到达的数据进行处理。&lt;/p&gt;

&lt;p&gt;在 Flink 中，应用程序由用户自定义算子组成，即 &lt;strong&gt;streaming dataflows&lt;/strong&gt;。这些 dataflows 形成了有向图，以一个或多个 &lt;strong&gt;source&lt;/strong&gt; 开始，并以一个或多个 &lt;strong&gt;sink&lt;/strong&gt; 结束。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841344_7b5cb803c44619374e0e0cf271335a08&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Flink 应用程序可以消费来自消息队列或分布式日志这类流式数据源（例如 Apache Kafka 或 Kinesis）的实时数据，也可以从各种的数据源中消费有界的历史数据。同样，Flink 应用程序生成的结果流也可以 sink 到各种存储系统中。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841510_fa366a1efea01c0287013d314e3b88c2&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;架构&quot;&gt;架构&lt;/h1&gt;

&lt;p&gt;当 Flink 集群启动后，首先会启动一个 JobManger 和一个或多个的 TaskManager。由 Client 提交任务给 JobManager，JobManager 再调度任务到各个 TaskManager 去执行，然后 TaskManager 将心跳和统计信息汇报给 JobManager。TaskManager 之间以流的形式进行数据的传输。上述三者均为独立的 JVM 进程。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Client&lt;/strong&gt; 为提交 Job 的客户端，可以是运行在任何机器上（与 JobManager 环境连通即可）。提交 Job 后，Client 可以结束进程（Streaming的任务），也可以不结束并等待结果返回。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;JobManager&lt;/strong&gt; 主要负责调度 Job 并协调 Task 做 checkpoint。从 Client 处接收到 Job 和 JAR 包等资源后，会生成优化后的执行计划，并以 Task 的单元调度到各个 TaskManager 去执行。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;TaskManager&lt;/strong&gt; 在启动的时候就设置好了槽位数（Slot），每个 slot 能启动一个 Task，Task 为线程。从 JobManager 处接收需要部署的 Task，部署启动后，与自己的上游建立 Netty 连接，接收数据并处理。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;可以看到 Flink 的任务调度是多线程模型，并且不同 Job/Task 混合在一个 TaskManager 进程中。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841354_489da73b2a36833bd8976a7861518bc6&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;jobmanager&quot;&gt;JobManager&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;JobManager&lt;/em&gt; 具有许多与协调 Flink 应用程序的分布式执行有关的职责：它决定何时调度下一个 task（或一组 task）、对完成的 task 或执行失败做出反应、协调 checkpoint、并且协调从失败中恢复等等。这个进程由三个不同的组件组成：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;ResourceManager：&lt;/strong&gt; &lt;em&gt;ResourceManager&lt;/em&gt; 负责 Flink 集群中的资源提供、回收、分配 - 它管理 &lt;strong&gt;task slots&lt;/strong&gt;，这是 Flink 集群中资源调度的单位。Flink 为不同的环境和资源提供者（例如 YARN、Mesos、Kubernetes 和 standalone 部署）实现了对应的 ResourceManager。在 standalone 设置中，ResourceManager 只能分配可用 TaskManager 的 slots，而不能自行启动新的 TaskManager。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Dispatcher：&lt;/strong&gt; &lt;em&gt;Dispatcher&lt;/em&gt; 提供了一个 REST 接口，用来提交 Flink 应用程序执行，并为每个提交的作业启动一个新的 JobMaster。它还运行 Flink WebUI 用来提供作业执行信息。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;JobMaster：&lt;/strong&gt; &lt;em&gt;JobMaster&lt;/em&gt; 负责管理单个&lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/concepts/glossary.html#logical-graph&quot;&gt;JobGraph&lt;/a&gt;的执行。Flink 集群中可以同时运行多个作业，每个作业都有自己的 JobMaster。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;始终至少有一个 JobManager。高可用（HA）设置中可能有多个 JobManager，其中一个始终是 &lt;em&gt;leader&lt;/em&gt;，其他的则是 &lt;em&gt;standby&lt;/em&gt;（请参考 &lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/deployment/ha/&quot;&gt;高可用（HA）&lt;/a&gt;）。&lt;/p&gt;

&lt;h2 id=&quot;taskmanagers&quot;&gt;TaskManagers&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;TaskManager&lt;/em&gt;（也称为 &lt;em&gt;worker&lt;/em&gt;）执行作业流的 task，并且缓存和交换数据流。&lt;/p&gt;

&lt;p&gt;必须始终至少有一个 TaskManager。在 TaskManager 中资源调度的最小单位是 task &lt;em&gt;slot&lt;/em&gt;。TaskManager 中 task slot 的数量表示并发处理 task 的数量。请注意一个 task slot 中可以执行多个算子。&lt;/p&gt;

&lt;h2 id=&quot;task&quot;&gt;Task&lt;/h2&gt;

&lt;p&gt;对于分布式执行，Flink 将算子的 subtasks &lt;em&gt;链接&lt;/em&gt;成 &lt;em&gt;tasks&lt;/em&gt;。每个 task 由一个线程执行。将算子链接成 task 是个有用的优化：它减少线程间切换、缓冲的开销，并且减少延迟的同时增加整体吞吐量。&lt;/p&gt;

&lt;p&gt;下图中样例数据流用 5 个 subtask 执行，因此有 5 个并行线程。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841313_5bdb3ff13b15788f9b1333e26dccd95d&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;task-slots-和资源&quot;&gt;Task Slots 和资源&lt;/h2&gt;

&lt;p&gt;每个 worker（TaskManager）都是一个 &lt;em&gt;JVM 进程&lt;/em&gt;，可以在单独的线程中执行一个或多个 subtask。为了控制一个 TaskManager 中接受多少个 task，就有了所谓的 &lt;strong&gt;task slots&lt;/strong&gt;（至少一个）。&lt;/p&gt;

&lt;p&gt;每个 &lt;em&gt;task slot&lt;/em&gt; 代表 TaskManager 中资源的固定子集。例如，具有 3 个 slot 的 TaskManager，会将其托管内存 1/3 用于每个 slot。分配资源意味着 subtask 不会与其他作业的 subtask 竞争托管内存，而是具有一定数量的保留托管内存。&lt;strong&gt;注意此处没有 CPU 隔离；当前 slot 仅分离 task 的托管内存。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;通过调整 task slot 的数量，用户可以定义 subtask 如何互相隔离。每个 TaskManager 有一个 slot，这意味着每个 task 组都在单独的 JVM 中运行（例如，可以在单独的容器中启动）。具有多个 slot 意味着更多 subtask 共享同一 JVM。同一 JVM 中的 task 共享 TCP 连接（通过多路复用）和心跳信息。它们还可以共享数据集和数据结构，从而减少了每个 task 的开销。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841431_1e0decbca08860a5600bf5a1a81d1aa3&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;默认情况下，Flink 允许 subtask 共享 slot，即便它们是不同的 task 的 subtask，只要是来自于同一作业即可。结果就是一个 slot 可以持有整个作业管道。允许 &lt;em&gt;slot 共享&lt;/em&gt;有两个主要优点：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Flink 集群所需的 task slot 和作业中使用的最大并行度恰好一样。无需计算程序总共包含多少个 task（具有不同并行度）。&lt;/li&gt;
  &lt;li&gt;容易获得更好的资源利用。如果没有 slot 共享，非密集 subtask（&lt;em&gt;source/map()&lt;/em&gt; ）将阻塞和密集型 subtask（&lt;em&gt;window&lt;/em&gt;） 一样多的资源。通过 slot 共享，我们示例中的基本并行度从 2 增加到 6，可以充分利用分配的资源，同时确保繁重的 subtask 在 TaskManager 之间公平分配。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841462_d5cc711a8ec9e0db23d206bf9abdcff8&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h1 id=&quot;概念&quot;&gt;概念&lt;/h1&gt;

&lt;h2 id=&quot;dataflows&quot;&gt;Dataflows&lt;/h2&gt;

&lt;p&gt;Flink 程序本质上是分布式并行程序。在程序执行期间，一个流有一个或多个&lt;strong&gt;流分区&lt;/strong&gt;（Stream Partition），每个算子有一个或多个&lt;strong&gt;子任务&lt;/strong&gt;（Operator Subtask）。每个子任务彼此独立，并在不同的线程中运行，或在不同的计算机或容器中运行。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841374_3d3e23fad83b148e3ef4af64ca2318b4&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Flink 算子之间可以通过&lt;em&gt;一对一&lt;/em&gt;（one-to-one）模式或&lt;em&gt;重分配（redistributing）&lt;/em&gt; 模式传输数据：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;一对一&lt;/strong&gt;模式（例如上图中的 &lt;em&gt;Source&lt;/em&gt; 和 &lt;em&gt;map()&lt;/em&gt; 算子之间）可以保留元素的分区和顺序信息。这意味着 &lt;em&gt;map()&lt;/em&gt; 算子的 subtask [1] 输入的数据以及其顺序与 &lt;em&gt;Source&lt;/em&gt; 算子的 subtask [1] 输出的数据和顺序完全相同，即同一分区的数据只会进入到下游算子的同一分区。&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;重分配&lt;/strong&gt;模式（例如上图中的 &lt;em&gt;map()&lt;/em&gt; 和 &lt;em&gt;keyBy/window&lt;/em&gt; 之间，以及 &lt;em&gt;keyBy/window&lt;/em&gt; 和 &lt;em&gt;Sink&lt;/em&gt; 之间）则会更改数据所在的流分区。该模式下，每个算子会将数据发送到多个目标子任务中，例如 &lt;em&gt;keyBy()&lt;/em&gt; （通过散列键重新分区）、&lt;em&gt;broadcast()&lt;/em&gt; （广播）或 &lt;em&gt;rebalance()&lt;/em&gt; （随机重新分发）。在重分配数据的过程中，元素只有在每对输出和输入子任务之间才能保留其之间的顺序信息（例如， &lt;em&gt;keyBy/window&lt;/em&gt; 的 subtask [2] 接收到的 &lt;em&gt;map()&lt;/em&gt; 的 subtask [1] 中的元素都是有序的）。因此，上图所示的 &lt;em&gt;keyBy/window&lt;/em&gt; 和 &lt;em&gt;Sink&lt;/em&gt; 算子之间数据的重新分发时，不同键（key）的聚合结果到达 Sink 的顺序是不确定的。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;时间&quot;&gt;时间&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;详情可见👉 &lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/concepts/timely-stream-processing.html&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;对于大多数流应用而言，能够使用同一份代码处理实时数据及重新处理历史数据，产生确定并且一致的结果非常有价值。&lt;/p&gt;

&lt;p&gt;在处理流式数据时，我们通常更需要关注事件本身发生的顺序而不是事件被传输以及处理的顺序，因为这能够帮助我们推理出一组事件（事件集合）是何时发生以及结束的。例如电子商务交易或金融交易中涉及到的事件集合。&lt;/p&gt;

&lt;p&gt;为了满足上述这类的实时流处理场景，我们通常会使用记录在数据流中的事件时间的时间戳，而不是处理数据的机器时钟的时间戳。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;事件时间（Event-Time）：设备时钟，记录事件发生的时间&lt;/li&gt;
  &lt;li&gt;摄入时间（Ingestion-Time）：设备时钟，记录事件发送到服务器的时间&lt;/li&gt;
  &lt;li&gt;处理时间（Processing-Time）：服务器时钟，记录服务器处理事件时的时间&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;使用事件时间&quot;&gt;使用事件时间&lt;/h3&gt;

&lt;p&gt;事件时间的强大之处在于，无论是在处理实时的数据还是重新处理历史的数据，基于事件时间创建的流计算应用都能保证结果是一样的，即幂等性。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;一个使用处理时间引发的问题：&lt;/strong&gt; 如果根据处理时间来衡量请求频率，看起来重启后出现了请求高峰，但是实际上请求频率是稳定的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841475_aca2aaaea24a40c7f2235fcb53dd550b&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;流状态&quot;&gt;流状态&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;详情可见👉 &lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/concepts/stateful-stream-processing.html&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Flink 中的算子可以是有状态的。这意味着如何处理一个事件可能取决于该事件之前所有事件数据的累积结果。Flink 中的状态不仅可以用于简单的场景（例如统计仪表板上每分钟显示的数据），也可以用于复杂的场景（例如训练作弊检测模型）。&lt;/p&gt;

&lt;p&gt;Flink 应用程序可以在分布式群集上并行运行，其中每个算子的各个并行实例会在单独的线程中独立运行，并且通常情况下是会在不同的机器上运行。&lt;/p&gt;

&lt;p&gt;状态算子的并行实例组在存储其对应状态时通常是按照键（key）进行分片存储的。每个并行实例算子负责处理一组特定键的事件数据，并且这组键对应的状态会保存在本地。&lt;/p&gt;

&lt;p&gt;如下图的 Flink 作业，其前三个算子的并行度为 2，最后一个 sink 算子的并行度为 1，其中第三个算子是有状态的，并且你可以看到第二个算子和第三个算子之间是全互联的（fully-connected），它们之间通过网络进行数据分发。通常情况下，实现这种类型的 Flink 程序是为了通过某些键对数据流进行分区，以便将需要一起处理的事件进行汇合，然后做统一计算处理。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841508_f8e34c09c2b03a0a26da3f1f3f0e7785&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Flink 应用程序的状态访问都在本地进行，因为这有助于其提高吞吐量和降低延迟。通常情况下 Flink 应用程序都是将状态存储在 JVM 堆上，但如果状态太大，我们也可以选择将其以结构化数据格式存储在高速磁盘中。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841451_6e8e40cc70c35a17443efd9f868d468d&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;窗口&quot;&gt;窗口&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;详情可见👉 &lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/dev/stream/operators/windows.html&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;窗口是处理无限流的核心，因为窗口在无限流上定义了一个有限的元素集合，在这些有限集上执行运算。下面简单介绍 Flink 中涉及的窗口类型及其特性。&lt;/p&gt;

&lt;h3 id=&quot;tumbling-windows&quot;&gt;Tumbling Windows&lt;/h3&gt;

&lt;p&gt;滚动窗口将每个元素分配到一个指定大小的窗口中。通常滚动窗口有一个固定的大小，并且不会出现重叠。例如：如果指定了一个5分钟大小的滚动窗口，无限流的数据会根据时间划分成[0:00 - 0:05)、[0:05, 0:10)、[0:10, 0:15)等窗口，如下图所示。&lt;/p&gt;

&lt;p&gt;默认的话窗口会根据时间对齐，即如果是一小时的滚动窗口，则划分后的窗口为 1:00:00.000 - 1:59:59.999, 2:00:00.000 - 2:59:59.999 等等。Flink 提供了 offset 参数，如果指定了 offset，例如 15min，则将得到1:15:00.000 - 2:14:59.999, 2:15:00.000 - 3:14:59.999 的窗口集合。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841518_d2e7c4cab4df2002a5d0710ab3c7ce54&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;sliding-windows&quot;&gt;Sliding Windows&lt;/h3&gt;

&lt;p&gt;滑动窗口不同于滚动窗口，滑动窗口的窗口可以重叠。&lt;/p&gt;

&lt;p&gt;滑动窗口有两个参数：slide 和 size。slide 为每次滑动的步长，size 为窗口的大小。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;slide &amp;lt; size，则窗口会重叠，每个元素会被分配到多个窗口。&lt;/li&gt;
  &lt;li&gt;slide = size，则等同于滚动窗口。&lt;/li&gt;
  &lt;li&gt;slide &amp;gt; size，则为跳跃窗口，窗口之间不重叠且有间隙。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;通常情况下大部分元素符合第一种情形，窗口是重叠的。因此，滑动窗口在计算移动平均数（moving averages）时很实用。例如，计算过去 5 分钟数据的平均值，每 10 秒钟更新一次，可以设置 slide 为 10秒，size 为 5 分钟。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841500_461707ba22e13f568d7376b167d361ea&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;session-windows&quot;&gt;Session Windows&lt;/h3&gt;

&lt;p&gt;会话窗口根据 session 来对元素进行分组。会话窗口与滚动窗口和滑动窗口相比，没有窗口重叠，没有固定窗口大小。相反，当它在一个固定的时间周期内不再收到元素，即 session 断开时，这个窗口就会关闭。例如，对于用户的鼠标点击流，我们可以根据用户进行区分（group by user_id），分析每个用户每天高频使用鼠标的时间段。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841600_dfe8767245929bc43916e3e14bc2e82c&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;global-windows&quot;&gt;Global Windows&lt;/h3&gt;

&lt;p&gt;全局窗口将所有元素汇集到一个集合。这种窗口通常只与自定义 trigger 配合使用。否则，由于窗口永远不会结束，因此不会触发任何窗口计算。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841393_525324dfadefb2d964a0b1b56b03a52b&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;窗口计算的一些注意点&quot;&gt;窗口计算的一些注意点&lt;/h3&gt;

&lt;p&gt;窗口可以被指定为一个非常长的时间区间，例如天、周、月。不过这意味着维护大量的流状态，通常来说有以下原则来帮助评估其占用的存储空间：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Flink 会对每个窗口的每个元素创建一个副本。对于滚动窗口，每个元素只会唯一创建一个副本（因为每个元素唯一属于一个窗口）。然而，对于滑动窗口来说，每个元素会窗口多个副本。因此，一个步长秒级的天级滑动窗口不是一个好主意。&lt;/li&gt;
  &lt;li&gt;ReduceFunction、AggregateFunction 都能够大大减少存储，因为每个窗口只会存储一个计算后的值，而非每个元素一个值。&lt;/li&gt;
  &lt;li&gt;使用 Evictor 对聚合计算进行预处理，淘汰不必要的元素。&lt;/li&gt;
&lt;/ol&gt;

&lt;h1 id=&quot;应用&quot;&gt;应用&lt;/h1&gt;

&lt;p&gt;Apache Flink 功能强大，支持开发和运行多种不同种类的应用程序。它的主要特性包括：批流一体化、精密的状态管理、事件时间支持以及精确一次的状态一致性保障等。Flink 不仅可以运行在包括 YARN、 Mesos、Kubernetes 在内的多种资源管理框架上，还支持在裸机集群上独立部署。在启用高可用选项的情况下，它不存在单点失效问题。事实证明，Flink 已经可以扩展到数千核心，其状态可以达到 TB 级别，且仍能保持高吞吐、低延迟的特性。世界各地有很多要求严苛的流处理应用都运行在 Flink 之上。&lt;/p&gt;

&lt;h2 id=&quot;事件驱动型应用&quot;&gt;事件驱动型应用&lt;/h2&gt;

&lt;p&gt;事件驱动型应用是一类具有状态的应用，它从一个或多个事件流提取数据，并根据到来的事件触发计算、状态更新或其他外部动作。&lt;/p&gt;

&lt;p&gt;相反，事件驱动型应用是基于状态化流处理来完成。在该设计中，数据和计算不会分离（传统架构中，需要实时请求数据库获取上下文数据），应用只需访问本地（内存或磁盘）即可获取数据。系统容错性的实现依赖于定期向远程持久化存储写入 checkpoint。下图描述了传统应用和事件驱动型应用架构的区别。&lt;/p&gt;

&lt;p&gt;例如：对于用户发文流，应用需要检查某篇文章是否涉嫌抄袭，前面的 pipeline 已经通过 NLP 提取相应的 embedding 向量。那么本应用只需要去查询当前文章的 embedding 是否与 Flink 本地维护的其他元素的 embedding 状态值相近即可。可以理解为，Flink 用内存+磁盘换取数据库调用，并且其状态的维护是精确且可靠的。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841530_4f797732acdb5cecf63f15266f48706d&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;数据分析应用&quot;&gt;数据分析应用&lt;/h2&gt;

&lt;p&gt;数据分析任务需要从原始数据中提取有价值的信息和指标。传统的分析方式通常是利用批查询，或将事件记录下来并基于此有限数据集构建应用来完成。为了得到最新数据的分析结果，必须先将它们加入分析数据集并重新执行查询或运行应用程序，随后将结果写入存储系统或生成报告。&lt;/p&gt;

&lt;p&gt;借助一些先进的流处理引擎，还可以实时地进行数据分析。和传统模式下读取有限数据集不同，流式查询或应用会接入实时事件流（例如 Kafka），并随着事件消费持续产生和更新结果。这些结果数据可能会写入外部数据库系统或以内部状态的形式维护。指标展示看板可以从外部数据库读取数据或直接查询应用的内部状态。&lt;/p&gt;

&lt;p&gt;如下图所示，Apache Flink 同时支持流式及批量分析应用。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841380_768b081f78a78a745240ce4dfeca448b&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;通常来说，流式分析相比于批式分析有几个优势：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;流式分析省掉了周期性的数据导入和查询过程，因此从事件中获取指标的延迟更低&lt;/li&gt;
  &lt;li&gt;批式查询必须处理那些由定期导入和获取数据导致的边界问题，而流式查询则无须考虑该问题。例如前面提到的滚动窗口统计，批式分析需要精确地周期性调度，而调度本身引入了调度时间以及应用冷启动时间，会有一定误差。&lt;/li&gt;
  &lt;li&gt;而容错性方面，Flink 提供了故障恢复机制，而批式计算通常需要由多个独立部件组成，需要周期性地调度提取数据和执行查询、分析。一旦某个组件出错，则会影响后续步骤。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;数据管道应用&quot;&gt;数据管道应用&lt;/h2&gt;

&lt;p&gt;提取-转换-加载（ETL）是一种在存储系统之间进行数据转换和迁移的常用方法。ETL 作业通常会周期性地触发，将数据从事务型数据库拷贝到分析型数据库或数据仓库。&lt;/p&gt;

&lt;p&gt;数据管道和 ETL 作业的用途相似，都可以转换、丰富数据，并将其从某个存储系统移动到另一个。但数据管道是以持续流模式运行，而非周期性触发。因此它支持从一个不断生成数据的源头读取记录，并将它们以低延迟移动到终点。例如：数据管道可以用来监控文件系统目录中的新文件，并将其数据写入事件日志；另一个应用可能会将事件流物化到数据库或增量构建和优化查询索引。&lt;/p&gt;

&lt;p&gt;下图描述了周期性 ETL 作业和持续数据管道的差异。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tech-proxy.bytedance.net/tos/images/1611997841436_2d710bc4ed7157ec72e23a70a7bb8a2c&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Flink 为多种数据存储系统（如：Kafka、Kinesis、Elasticsearch、JDBC数据库系统等）内置了连接器。和周期性 ETL 作业相比，数据管道可以明显降低将端到端数据传输的延迟。此外，由于它能够持续消费和发送数据，因此用途更广，支持用例更多。&lt;/p&gt;

&lt;p&gt;例如，对于用户发文流，经过一系列前置 pipeline 处理，提取了关键词等信息，Flink 作业将数据转化为所需格式 sink 到数据库，并 sink 到另一个事件日志流进行一系列后处理，如送审核、写索引，等等。&lt;/p&gt;

&lt;h1 id=&quot;容错机制&quot;&gt;容错机制&lt;/h1&gt;

&lt;blockquote&gt;
  &lt;p&gt;后续将深入解析 Flink 容错机制的实现，也可以参考👉 &lt;a href=&quot;https://flink.apache.org/features/2018/03/01/end-to-end-exactly-once-apache-flink.html&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;通过状态快照和流重放两种方式的组合，Flink 能够提供可容错的，精确一次计算的语义。这些状态快照在执行时会获取并存储分布式 pipeline 中整体的状态，它会将数据源中消费数据的偏移量记录下来，并将整个 job graph 中算子获取到该数据（记录的偏移量对应的数据）时的状态记录并存储下来。当发生故障时，Flink 作业会恢复上次存储的状态，重置数据源从状态中记录的上次消费的偏移量开始重新进行消费处理。而且状态快照在执行时会异步获取状态并存储，并不会阻塞正在进行的数据处理逻辑。&lt;/p&gt;

&lt;h1 id=&quot;reference&quot;&gt;Reference&lt;/h1&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/learn-flink/&quot;&gt;Flink 概览&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/learn-flink/fault_tolerance.html&quot;&gt;Flink 容错机制&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://ci.apache.org/projects/flink/flink-docs-release-1.12/concepts/flink-architecture.html&quot;&gt;Flink 架构&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;https://ververica.cn/developers-resources&lt;/li&gt;
  &lt;li&gt;https://wuchong.me/categories/Flink/&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Sat, 30 Jan 2021 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2021/01/30/Flink-%E6%A6%82%E8%A7%88/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2021/01/30/Flink-%E6%A6%82%E8%A7%88/</guid>
        
			<category>computation</category>
        
        
		</item>
    
		<item>
			<title>BTree 索引原理及其应用</title>
			<description>&lt;h1 id=&quot;btree-索引原理及其应用&quot;&gt;BTree 索引原理及其应用&lt;/h1&gt;

&lt;p&gt;虽然写 BTree，但其实本章主要讨论其中一个优化的子集，即广泛使用的 B+Tree。&lt;/p&gt;

&lt;h2 id=&quot;1-btree&quot;&gt;1. BTree&lt;/h2&gt;

&lt;h3 id=&quot;11-btree-结构&quot;&gt;1.1. BTree 结构&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;http://blog.codinglabs.org/uploads/pictures/theory-of-mysql-index/4.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;12-btree-索引特性&quot;&gt;1.2. BTree 索引特性&lt;/h3&gt;

&lt;p&gt;例如存在如下的表&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;ID | first_name | last_name    | Class      | Position |  ssn | 
---------------------------------------------------------------
 1 | Teemo      | Shroomer     | Specialist | Top      | 2345 |
 2 | Cecil      | Heimerdinger | Specialist | Mid      | 5461 |
 3 | Annie      | Hastur       | Mage       | Mid      | 8784 |
 4 | Fiora      | Laurent      | Slayer     | Top      | 7867 |
 5 | Garen      | Crownguard   | Fighter    | Top      | 4579 |
 6 | Malcolm    | Graves       | Specialist | ADC      | 4578 |
 7 | Irelia     | Lito         | Figher     | Top      | 5689 |
 8 | Janna      | Windforce    | Controller | Support  | 4580 |
 9 | Jarvan     | Lightshield  | Figher     | Top      | 4579 |
10 | Katarina   | DuCouteau    | Assassin   | Mid      | 5608 |
11 | Kayle      | Hex          | Specialist | Top      | 4794 |
12 | Emilia     | LeBlanc      | Mage       | Mid      | 3468 |
13 | Lee        | Sin          | Fighter    | Jungle   | 8085 |
14 | Lux        | Crownguard   | Mage       | Mid      | 4567 |
15 | Sarah      | Fortune      | Marksman   | ADC      | 6560 |
16 | Morgana    | Hex          | Controller | Support  | 3457 |
17 | Orianna    | Reveck       | Mage       | Mid      | 9282 |
18 | Sona       | Buvelle      | Controller | Support  | 4722 |
19 | Jericho    | Swain        | Mage       | Mid      | 5489 |
20 | Shauna     | Vayne        | Marksman   | ADC      | 2352 |
21 | Xin        | Zhao         | Fighter    | Jungle   | 6902 |
22 | Yorick     | Mori         | Tank       | Top      | 4840 |
23 | Wu         | Kong         | Fighter    | Jungle   | 4933 |
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;创建 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;users.first_name&lt;/code&gt; 的索引，B+Tree 叶子节点组织如下：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;first_name  Primary Key
-----------------------
Annie    -&amp;gt; 3
Cecil    -&amp;gt; 2
Emilia   -&amp;gt; 12
Fiora    -&amp;gt; 4
Garen    -&amp;gt; 5
Irelia   -&amp;gt; 7
Janna    -&amp;gt; 8
Jarvan   -&amp;gt; 9
Jericho  -&amp;gt; 19
Katarina -&amp;gt; 10
Kayle    -&amp;gt; 11
Lee      -&amp;gt; 13
Lux      -&amp;gt; 14
Malcolm  -&amp;gt; 6
Morgana  -&amp;gt; 16
Orianna  -&amp;gt; 17
Sarah    -&amp;gt; 15
Shauna   -&amp;gt; 20
Sona     -&amp;gt; 18
Teemo    -&amp;gt; 1
Wu       -&amp;gt; 23
Xin      -&amp;gt; 21
Yorick   -&amp;gt; 22
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;那么复合索引呢？&lt;/p&gt;

&lt;p&gt;创建 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(class, position)&lt;/code&gt; 的复合索引，B+Tree 叶子节点组织如下：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;class-position       Primary Key
--------------------------------
AssassinMid       -&amp;gt; 10
ControllerSupport -&amp;gt; 16
ControllerSupport -&amp;gt; 18
ControllerSupport -&amp;gt; 8
FigherTop         -&amp;gt; 7
FigherTop         -&amp;gt; 9
FighterJungle     -&amp;gt; 13
FighterJungle     -&amp;gt; 21
FighterJungle     -&amp;gt; 23
FighterTop        -&amp;gt; 5
MageMid           -&amp;gt; 12
MageMid           -&amp;gt; 14
MageMid           -&amp;gt; 17
MageMid           -&amp;gt; 19
MageMid           -&amp;gt; 3
MarksmanADC       -&amp;gt; 15
MarksmanADC       -&amp;gt; 20
SlayerTop         -&amp;gt; 4
SpecialistADC     -&amp;gt; 6
SpecialistMid     -&amp;gt; 2
SpecialistTop     -&amp;gt; 1
SpecialistTop     -&amp;gt; 11
TankTop           -&amp;gt; 22
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;可以看到，叶子节点会首先根据 class 的字典序、其次根据 position 的字典序组织。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;最左前缀：(a, b, c) =&amp;gt; (a), (a, b), (a, b, c), (a, b, c[:k]), (a, b[:k]), (a[:k])&lt;/li&gt;
  &lt;li&gt;第一原则是，如果通过调整顺序，可以少维护一个索引，那么这个顺序往往就是需要优先考虑采用的。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;13-与-btree-的区别&quot;&gt;1.3. 与 BTree 的区别&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;B + 树查询时间复杂度固定是 logn，B 树查询复杂度最好是 O (1)。&lt;/li&gt;
  &lt;li&gt;B 树每个节点即保存数据又保存索引，因此每个节点的字节点指针数量更少，即扇出更少，高度通常比 B+ 树高&lt;/li&gt;
  &lt;li&gt;B + 树相邻接点的指针可以大大增加区间访问性，范围查询效率更高&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;2-btree-in-storage-engine&quot;&gt;2. BTree In Storage Engine&lt;/h2&gt;

&lt;p&gt;存储引擎要做的事情无外乎是将磁盘上的数据读到内存并返回给应用，或者将应用修改的数据由内存写到磁盘上。如何设计一种高效的数据结构和算法是所有存储引擎要考虑的根本问题，目前大多数流行的存储引擎是基于 BTree 或 LSM Tree 这两种数据结构来设计的。&lt;/p&gt;

&lt;h3 id=&quot;21-innodb&quot;&gt;2.1. InnoDB&lt;/h3&gt;

&lt;p&gt;每一个索引在 InnoDB 里面对应一棵 B+ 树。&lt;/p&gt;

&lt;h4 id=&quot;211-数据结构&quot;&gt;2.1.1. 数据结构&lt;/h4&gt;

&lt;h5 id=&quot;聚簇索引&quot;&gt;聚簇索引&lt;/h5&gt;

&lt;p&gt;InnoDB 要求表必须有主键&lt;/p&gt;

&lt;p&gt;InnoDB 的数据文件本身就是索引文件。&lt;/p&gt;

&lt;p&gt;表数据文件本身就是按 B+Tree 组织的一个索引结构，这棵树的叶节点 data 域保存了完整的数据记录。这个索引的 key 是数据表的主键，因此 InnoDB 表数据文件本身就是主索引。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://static001.geekbang.org/resource/image/dc/8d/dcda101051f28502bd5c4402b292e38d.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;为什么要有聚簇索引？磁盘上的组织是如何的？&lt;/p&gt;

&lt;h5 id=&quot;索引维护&quot;&gt;索引维护&lt;/h5&gt;

&lt;p&gt;B+ 树为了维护索引有序性，在插入新值的时候需要做必要的维护。以上面这个图为例，如果插入新的行 ID 值为 700，则只需要在 R5 的记录后面插入一个新记录。如果新插入的 ID 值为 400，就相对麻烦了，需要逻辑上挪动后面的数据，空出位置。页分裂、合并则需要更耗时的操作。&lt;/p&gt;

&lt;p&gt;自增主键的插入数据模式，正符合了我们前面提到的递增插入的场景。每次插入一条新记录，都是追加操作，都不涉及到挪动其他记录，也不会触发叶子节点的分裂。&lt;/p&gt;

&lt;h5 id=&quot;索引列的选择&quot;&gt;索引列的选择&lt;/h5&gt;

&lt;p&gt;基于上面的索引维护过程说明，我们来讨论一个案例：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;你可能在一些建表规范里面见到过类似的描述，要求建表语句里一定要有自增主键。当然事无绝对，我们来分析一下哪些场景下应该使用自增主键，而哪些场景下不应该。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
  &lt;li&gt;时间：自增主键的插入数据模式，正符合了我们前面提到的递增插入的场景。每次插入一条新记录，都是追加操作，都不涉及到挪动其他记录，也不会触发叶子节点的分裂。&lt;/li&gt;
  &lt;li&gt;空间：由于每个非主键索引的叶子节点上都是主键的值，因此主键长度越小，普通索引的叶子节点就越小，普通索引占用的空间也就越小。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以，从性能和存储空间方面考量，自增主键往往是更合理的选择。&lt;/p&gt;

&lt;p&gt;比如，有些业务的场景需求是这样的（典型的 KV 场景）：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;只有一个索引；&lt;/li&gt;
  &lt;li&gt;该索引必须是唯一索引。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这时候，尽量使用主键查询，可以避免每次查询需要搜索两棵树（回表）。&lt;/p&gt;

&lt;h4 id=&quot;212-覆盖索引&quot;&gt;2.1.2. 覆盖索引&lt;/h4&gt;

&lt;p&gt;select * from T where k between 3 and 5，需要执行几次树的搜索操作，会扫描多少行？&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;在 k 索引树上找到 k=3 的记录，取得 ID = 300；&lt;/li&gt;
  &lt;li&gt;再到 ID 索引树查到 ID=300 对应的 R3；&lt;/li&gt;
  &lt;li&gt;在 k 索引树取下一个值 k=5，取得 ID=500；&lt;/li&gt;
  &lt;li&gt;再回到 ID 索引树查到 ID=500 对应的 R4；&lt;/li&gt;
  &lt;li&gt;在 k 索引树取下一个值 k=6，不满足条件，循环结束。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;select ID from T where k between 3 and 5 呢？&lt;/p&gt;

&lt;p&gt;这时只需要查 ID 的值，而 ID 的值已经在 k 索引树上了，因此可以直接提供查询结果，不需要回表。也就是说，在这个查询里面，索引 k 已经 “覆盖了” 我们的查询需求，我们称为覆盖索引。&lt;/p&gt;

&lt;p&gt;字段顺序&lt;/p&gt;

&lt;p&gt;这个最左前缀可以是联合索引的最左 N 个字段，也可以是字符串索引的最左 M 个字符。&lt;/p&gt;

&lt;p&gt;因为可以支持最左前缀，所以当已经有了 (a,b) 这个联合索引后，一般就不需要单独在 a 上建立索引了。因此，第一原则是，如果通过调整顺序，可以少维护一个索引，那么这个顺序往往就是需要优先考虑采用的。&lt;/p&gt;

&lt;p&gt;索引越多，“维护成本” 越大&lt;/p&gt;

&lt;h4 id=&quot;213-索引下推&quot;&gt;2.1.3. 索引下推&lt;/h4&gt;

&lt;p&gt;对于联合索引（name, age）为例。如果现在有一个需求：检索出表中 “名字第一个字是张，而且年龄是 10 岁的所有男孩”。那么，SQL 语句是这么写的：&lt;/p&gt;

&lt;p&gt;select * from tuser where name like ‘ 张 %’ and age=10 and ismale=1;&lt;/p&gt;

&lt;p&gt;索引只能用 “张”&lt;/p&gt;

&lt;p&gt;然后？&lt;/p&gt;

&lt;p&gt;判断其他条件是否满足。&lt;/p&gt;

&lt;p&gt;在 MySQL 5.6 之前，只能从 ID3 开始一个个回表。到主键索引上找出数据行，再对比字段值。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://static001.geekbang.org/resource/image/b3/ac/b32aa8b1f75611e0759e52f5915539ac.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;而 MySQL 5.6 引入的索引下推优化（index condition pushdown)， 可以在索引遍历过程中，对索引中包含的字段先做判断，直接过滤掉不满足条件的记录，减少回表次数。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://static001.geekbang.org/resource/image/76/1b/76e385f3df5a694cc4238c7b65acfe1b.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;22-wiredtiger&quot;&gt;2.2. WiredTiger&lt;/h3&gt;

&lt;p&gt;其实 mongo 除了 wiredtiger 之外，还支持 mongrocks，不过 mongorocks 底层是使用基于 LSM-Tree 的 RocksDB。本文只讨论 BTree，所以 mongorocks 抛到一边。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tosv.byted.org/obj/tech/images/1583208024743_bc9727e772d9408269d13ffd1fb10eed.png&quot; alt=&quot;image.png&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;221-数据结构&quot;&gt;2.2.1. 数据结构&lt;/h4&gt;

&lt;p&gt;wiredtiger 维护索引文件跟数据文件。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://mmbiz.qpic.cn/mmbiz_png/G2NnvykaFvXMpHpUgsV8bic9YWHE0Ob1dzR7iawp6y8SSZaXF7YmqBEDVeBPjk4mUEJIIHxTdeKqcPHtLgicWDXxw/640?wx_fmt=png&amp;amp;tp=webp&amp;amp;wxfrom=5&amp;amp;wx_lazy=1&amp;amp;wx_co=1&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;key 其实是一个 RecordID，每插入一个文档都会插入新的 key/value（RecordID =&amp;gt; RecordPosition）&lt;/p&gt;

&lt;p&gt;mongo 中并不会将 id 索引与行内容存放在一起（即没有聚簇索引的概念）。取而代之的，mongodb 将索引与数据分开存放，通过 RecordId 进行间接引用。&lt;/p&gt;

&lt;p&gt;举例一张包含两个索引（id 和 name）的表，在 wt 层将有三张表与其对应。&lt;/p&gt;

&lt;p&gt;如上图所示，集合包含 {_id: 1}, {name: 1} 2 个索引&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;用户插入文档时，底层引擎将文档内容存储，返回对应的位置信息，即 RecordId1&lt;/li&gt;
  &lt;li&gt;集合包含 2 个索引&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
  &lt;li&gt;插入 {_id: ObjectId1} ⇒ RecordId1 的索引&lt;/li&gt;
  &lt;li&gt;插入 {name: “rose”} ⇒ RecordId1 的索引&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;有了上述的数据，在根据_id 访问时文档时 （根据其他索引字段类似）&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;根据文档的 _id 字段从底层 KV 引擎读取 RecordId&lt;/li&gt;
  &lt;li&gt;根据 RecordId 从底层 KV 引擎读取文档内容&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;http://p1.pstatp.com/origin/33a5c00008229e3945845&quot; alt=&quot;WechatIMG761&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;222-索引实现&quot;&gt;2.2.2. 索引实现&lt;/h4&gt;

&lt;p&gt;其实所有 BTree 索引的实现都是大同小异&lt;/p&gt;

&lt;p&gt;在 MongoDB 中，没有 clustered index，因此，Collection 初始的物理存储跟 doc 插入的顺序有关，MongoDB 按照 doc 插入的顺序，依次将 doc 存储在 disk 上，插入顺序上相邻的 doc 在 disk 的物理位置上也是相邻的；对 doc 的修改可能对 collection 的物理存储发生变化，如果 doc 的修改不会导致 doc 的 size 增加，那么 doc 会继续存储在原来的存储空间中，而不会对 collection 的物理存储有影响，一旦修改操作导致 doc 的 size 增加，导致 doc 发生移动，那么 collection 的物理存储就会发生变化。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;http://p3.pstatp.com/origin/33a35000195dcec379fc8&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;如果插入的集合包含索引（MongoDB 的集合默认会有_id 索引），针对每项索引，还会往 WiredTiger 插入一个新的 key-value，key 是索引的字段内容，value 为插入文档时生成的 RecordId，这样就能快速根据索引找到文档的位置信息。&lt;/p&gt;

&lt;h5 id=&quot;objectid&quot;&gt;ObjectID&lt;/h5&gt;

&lt;p&gt;为什么 ObjectID 是递增的？&lt;/p&gt;

&lt;p&gt;上文说到插入顺序上相邻的 doc 在 disk 的物理位置上也是相邻的。因此默认的 ObjectID 上的索引中，叶子结点的数据也是相邻的（highly clustered）。&lt;/p&gt;

&lt;h5 id=&quot;单字段索引&quot;&gt;单字段索引&lt;/h5&gt;

&lt;p&gt;下述索引会对 age 进行升序排序&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;db.person.createIndex( {age: 1} ) 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;复合索引&quot;&gt;复合索引&lt;/h5&gt;

&lt;p&gt;这个索引要先按 age 字段升序、age 相同的按 name 字段降序&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;db.person.createIndex( {age: 1, name: 1} ) 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;那么下面语句呢？&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;db.person.createIndex( {age: 1, name: -1} ) 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;MongoDB 针对每个索引，会有一个位图来描述索引各个字段的排序方向，如果方向是逆序（如 b: -1），会把 key 的内容里将 b 字段对应的 bit 全部取反。&lt;/p&gt;

&lt;p&gt;InnoDB 的回表查询也适用于此，如果复合索引能够覆盖查询，则不用回表。&lt;/p&gt;

&lt;h5 id=&quot;多-key-索引&quot;&gt;多 key 索引&lt;/h5&gt;

&lt;p&gt;当索引的字段为数组时，创建出的索引称为多 key 索引，多 key 索引会为数组的每个元素建立一条索引&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{&quot;name&quot; : &quot;jack&quot;, &quot;age&quot; : 19, habbit: [&quot;football, runnning&quot;]}
db.person.createIndex( {habbit: 1} )  // 创建多key索引
db.person.find( {habbit: &quot;football&quot;} )
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;底层会创建类似 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;person.habbit_football =&amp;gt; RecordID1&lt;/code&gt; 的索引 key/value&lt;/p&gt;

&lt;h5 id=&quot;全文索引&quot;&gt;全文索引&lt;/h5&gt;

&lt;p&gt;待补充&lt;/p&gt;

&lt;h2 id=&quot;3-application&quot;&gt;3. Application&lt;/h2&gt;

&lt;h3 id=&quot;31-order-by--sort&quot;&gt;3.1. order by / sort&lt;/h3&gt;

&lt;p&gt;MySQL 跟 MongoDB 的实现类似，这里用 MySQL 来举例。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `city` varchar(16) NOT NULL,
  `name` varchar(16) NOT NULL,
  `age` int(11) NOT NULL,
  `addr` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `city` (`city`)
) ENGINE=InnoDB;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;查询城市是 “杭州” 的所有人名字，并且按照姓名排序返回前 1000 个人的姓名、年龄。我们执行&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select city,name,age from t where city=&apos;杭州&apos; order by name limit 1000  ;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;通常情况下，这个语句执行流程如下所示 ：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;初始化 sort_buffer，确定放入 name、city、age 这三个字段；&lt;/li&gt;
  &lt;li&gt;从索引 city 找到第一个满足 city=’ 杭州’条件的主键 id，也就是图中的 ID_X；&lt;/li&gt;
  &lt;li&gt;到主键 id 索引取出整行，取 name、city、age 三个字段的值，存入 sort_buffer 中；&lt;/li&gt;
  &lt;li&gt;从索引 city 取下一个记录的主键 id；&lt;/li&gt;
  &lt;li&gt;重复步骤 3、4 直到 city 的值不满足查询条件为止，对应的主键 id 也就是图中的 ID_Y；&lt;/li&gt;
  &lt;li&gt;对 sort_buffer 中的数据按照字段 name 做快速排序；&lt;/li&gt;
  &lt;li&gt;按照排序结果取前 1000 行返回给客户端。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://static001.geekbang.org/resource/image/6c/72/6c821828cddf46670f9d56e126e3e772.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;如果需要取的字段过多，超多 sort_buffer 的话，则会走到 rowid 排序（多回表一次）&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://static001.geekbang.org/resource/image/dc/6d/dc92b67721171206a302eb679c83e86d.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;需要排序的原因是，原来的数据就是无序的。要解决这个问题，则要利用好索引结构。&lt;/p&gt;

&lt;p&gt;例如，对上述需求，我们新建索引：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;alter table t add index city_user(city, name);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;则对于每个 city，name 都是有序的：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://static001.geekbang.org/resource/image/f9/bf/f980201372b676893647fb17fac4e2bf.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;当然，这个索引对于上述需求还需要回表去取 age，因此建立 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;(city, name, age)&lt;/code&gt; 的覆盖索引可以避免回表。&lt;/p&gt;

&lt;p&gt;如果要取多个城市的呢？&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select * from t where city in (“杭州”,&quot;苏州&quot;) order by name limit 100;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;如果这个需求需要分页呢？&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;select city,name,age from t where city=&apos;杭州&apos; order by name limit 10000,100;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;32-fuzzy-search&quot;&gt;3.2. fuzzy search&lt;/h3&gt;

&lt;p&gt;待补充&lt;/p&gt;

&lt;h2 id=&quot;4-reference&quot;&gt;4. Reference&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;https://mp.weixin.qq.com/s/Wuzh47jsBh5QonBrZxUnjg&lt;/li&gt;
  &lt;li&gt;https://mongoing.com/archives/5367&lt;/li&gt;
  &lt;li&gt;https://dzone.com/articles/learn-mongodb-with-me-part-3?utm_source=dzone&amp;amp;utm_medium=article&amp;amp;utm_campaign=mongodb-cluster&lt;/li&gt;
  &lt;li&gt;https://time.geekbang.org/column/article/73479&lt;/li&gt;
  &lt;li&gt;MySQL 索引实现 http://blog.codinglabs.org/articles/theory-of-mysql-index.html&lt;/li&gt;
  &lt;li&gt;innodb 索引：https://tech.bytedance.net/articles/12571#&lt;/li&gt;
  &lt;li&gt;MongoDB 索引原理 https://yq.aliyun.com/articles/386769&lt;/li&gt;
  &lt;li&gt;MongoDB 索引选择策略 https://dzone.com/articles/effective-mongodb-indexing-part-2&lt;/li&gt;
  &lt;li&gt;复合索引的形象描述 https://medium.com/@User3141592/single-vs-composite-indexes-in-relational-databases-58d0eb045cbe&lt;/li&gt;
&lt;/ul&gt;

</description>
			<pubDate>Wed, 14 Oct 2020 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2020/10/14/BTree%E7%B4%A2%E5%BC%95%E5%8E%9F%E7%90%86%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2020/10/14/BTree%E7%B4%A2%E5%BC%95%E5%8E%9F%E7%90%86%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8/</guid>
        
			<category>database</category>
        
        
		</item>
    
		<item>
			<title>索引结构——LSM-Tree与B-tree</title>
			<description>&lt;h1 id=&quot;索引结构&quot;&gt;索引结构&lt;/h1&gt;

&lt;p&gt;本文介绍目前两种流行的索引结构，作为 DDIA 第三章前半部分的总结。&lt;/p&gt;

&lt;h2 id=&quot;1-b-tree-索引&quot;&gt;1. B-Tree 索引&lt;/h2&gt;

&lt;h3 id=&quot;11-简介&quot;&gt;1.1. 简介&lt;/h3&gt;

&lt;p&gt;B-tree 是目前最广泛使用的一种索引结构（B+ tree 是 B-tree 的一种优化版）。&lt;/p&gt;

&lt;p&gt;可以说，B-tree 是广义上的二分搜索树，只不过每个节点（node）可以有 B 个子节点。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://tikv.org/img/deep-dive/b-tree.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;N 个数据通过 B-tree 组织，可以达到 O(logN) 复杂度的 query 速度。&lt;/p&gt;

&lt;h3 id=&quot;12-存储细节&quot;&gt;1.2. 存储细节&lt;/h3&gt;

&lt;p&gt;B-tree 将数据分成固定大小的页，页是内部读写的最小单元。这种设计接近底层硬件，因为磁盘也是按照固定大小的块排列。&lt;/p&gt;

&lt;p&gt;通常来说，存储引擎会使用 B-tree 组织数据进行存储（使用主键进行排序），即叶子节点的数据会按照顺序存储在磁盘上（叶子节点使用双向链表连接），这称为聚集索引（Clustered Index）。&lt;/p&gt;

&lt;p&gt;如果要更新 B-tree 现有的值，则首先搜索包含该键的叶子页，并将页刷到磁盘。&lt;/p&gt;

&lt;p&gt;如果需要插入新键，则需要找到该范围的页，将值写入页中。如果该页没有足够空间来容纳新键，则将其分裂成两个页，并且更新父页，若父页也满，则触发另一个分裂，以此类推。&lt;/p&gt;

&lt;p&gt;可以看到，如果每次插入新键都可能触发页分裂的话，则写入效率会非常低，因此通常来说，聚集索引会使用 AutoIncrement 的 ID 作为默认主键。&lt;/p&gt;

&lt;h3 id=&quot;13-奔溃恢复&quot;&gt;1.3. 奔溃恢复&lt;/h3&gt;

&lt;p&gt;由于 B-tree 的每次写入都涉及页的更新操作，并且有时候可能涉及多个页的写操作。如果完成部分写，然后数据库奔溃，则会破坏索引（例如某个页成了孤儿页，没有被引用）。&lt;/p&gt;

&lt;p&gt;为了使得数据库能够从奔溃中恢复，通常会实现一个预写日志（write-ahead log, WAL），每次修改 B-tree 之前，需要先更新 WAL，再修改 B-tree 的数据。&lt;/p&gt;

&lt;p&gt;当数据库奔溃后恢复时，使用 WAL 将数据库恢复到最近一致的状态。&lt;/p&gt;

&lt;h3 id=&quot;14-并发控制&quot;&gt;1.4. 并发控制&lt;/h3&gt;

&lt;p&gt;原地更新页的另外一个问题是，并发控制问题，通常会使用锁存器（key 级别的锁）来实现。&lt;/p&gt;

&lt;h3 id=&quot;15-性能优化&quot;&gt;1.5. 性能优化&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;一些数据库（如 LMDB）不实用覆盖页和维护 WAL，而是使用写时复制方案。在新位置写入修改的页，创建新版本的父页，指向该新位置。这种方法也有利于并发控制。&lt;/li&gt;
  &lt;li&gt;键压缩，即保存键的缩略信息，而不是完整的键，以节省页空间，特别是中间层节点，只需要提供 range 信息。&lt;/li&gt;
  &lt;li&gt;页的布局。理论上，页可以放在磁盘上任意位置，但是这样的话范围查询则需要涉及大量的随机I/O，是非常耗时的操作。因此，许多实现将子叶顺序存储，不过，当 B-tree 变大时，维护这个顺序会很困难。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;2-lsm-tree-索引&quot;&gt;2. LSM-Tree 索引&lt;/h2&gt;

&lt;h3 id=&quot;21-简介&quot;&gt;2.1. 简介&lt;/h3&gt;

&lt;p&gt;日志结构合并树（Log-Strutured Merge Tree, LSM-Tree）也是利用键（key）进行排序存储的一种数据结构。&lt;/p&gt;

&lt;p&gt;数据存储在一系列的段文件中，称为排序字符串表（Sorted String Table, SSTable）。每个段文件表对应有一个内存哈希索引（可以是稀疏的），其中存储的值为 sstable 里面的文件偏移。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://pic3.zhimg.com/80/v2-43898b66ed75e8c7ff4e55d290111322_1440w.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;每次查询数据的时候，会从最新的 sstable 开始查找，直到找到所需的 key。&lt;/p&gt;

&lt;h3 id=&quot;22-原理&quot;&gt;2.2. 原理&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;当写入的时候，先往内存的平衡树数据结构写入&lt;/li&gt;
  &lt;li&gt;当上述内存表大小达到某个阈值的时候，则作为 SSTable 文件写入磁盘。新的 SSTable 成为数据库的最新部分。写入 SSTable 的时候，新的写入则添加到新的内存表实例中。&lt;/li&gt;
  &lt;li&gt;处理读请求时，首先尝试在最新的内存表中查找键，其次是最新的磁盘 SSTable，再次是次新的磁盘 SSTable，以此类推。&lt;/li&gt;
  &lt;li&gt;后台进程周期性地执行合并与压缩过程，以合并多个段文件，并丢弃无效的值（被修改或被删除）。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;当然，就像 B-tree 一样，所有存储引擎都需要解决奔溃恢复的问题，LSM-Tree 也可以使用 WAL 日志，每次写入内存表之前，先写入 WAL，。&lt;/p&gt;

&lt;h3 id=&quot;23-性能优化&quot;&gt;2.3. 性能优化&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;当查找某个不存在的键的时候，LSM-Tree 可能会很慢，因为需要从最新的 SSTable 回溯到最老的 SSTable。为此，可以使用布隆过滤器来解决。&lt;/li&gt;
  &lt;li&gt;压缩。压缩是 LSM-Tree 的核心问题。压缩算法包含大小分级（Size-tiered compaction strategy, STCS） 以及分层压缩（Level-based compaction strategy, LBCS）。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://pic1.zhimg.com/80/v2-7a104c0203097084c18c55cf51dffb5e_1440w.jpg&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;大小分级与分层压缩的对比可见上图。&lt;/p&gt;

&lt;p&gt;大小分级中，每个 SSTable 虽然都是 sorted 的，但是当 level0 与 level1 合并 flush 到 level2 中的时候，[1,3,4,7], [2,5,6,8] 并不能组成一个 Run，因为无法直接使用二分搜索。例如，我需要查找 5，那么这两个 SSTable 都在查询范围内。&lt;/p&gt;

&lt;p&gt;而分层压缩则保证了这点，新的层中可能包含交叉的键，但是老的层都是不交叉的键。每次新层文件大小达到阈值的时候，会与较老的层中有交集的文件作合并操作，保证每一层都可以直接使用二分搜索进行查询。&lt;/p&gt;

&lt;h2 id=&quot;3-对比&quot;&gt;3. 对比&lt;/h2&gt;

&lt;h3 id=&quot;31-lsm-tree-的优点&quot;&gt;3.1. LSM-Tree 的优点&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;写入快。B 树索引必须至少两次写入每一段数据：一次写入预先写入日志，一次写入树页面本身（也许再次分页）。即使在该页面中只有几个字节发生了变化，也需要一次编写整个页面的开销。有些存储引擎甚至会覆盖同一个页面两次，以免在电源故障的情况下导致页面部分更新。&lt;/li&gt;
  &lt;li&gt;碎片少。LSM 树可以被压缩得更好，因此经常比 B 树在磁盘上产生更小的文件。 B 树存储引擎会由于页分裂而留下一些未使用的磁盘空间：当页分裂或某行不能放入现有页面时，页面中的某些空间无法被利用。由于 LSM 树不是面向页面的，并且定期重写 SSTables 以去除碎片，所以它们具有较低的存储开销，特别是当使用分层压缩时。&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;32-lsm-tree-的缺点&quot;&gt;3.2. LSM-Tree 的缺点&lt;/h3&gt;

&lt;ol&gt;
  &lt;li&gt;压缩阻塞。因为磁盘的带宽是固定的，因此 LSM-Tree 在压缩过程有时会干扰正在进行的读写操作。&lt;/li&gt;
  &lt;li&gt;压缩速率。当写入吞吐量很大的时候，很可能来不及压缩 SSTable 导致段文件增大到磁盘空间不足。因此需要额外的监控发现这些情况，以及配置合适的压缩间隔。&lt;/li&gt;
  &lt;li&gt;事务。B-tree 中，事务的隔离可以直接通过键范围的锁来实现。而 LSM-Tree 可能需要通过文件范围的锁来解决。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;reference&quot;&gt;Reference&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;leveldb 的实现：https://github.com/google/leveldb/blob/master/doc/impl.md&lt;/li&gt;
  &lt;li&gt;数据结构性能对比：https://tikv.org/deep-dive/key-value-engine/b-tree-vs-lsm/&lt;/li&gt;
  &lt;li&gt;leveldb 架构：https://zhuanlan.zhihu.com/p/38810568&lt;/li&gt;
  &lt;li&gt;DDIA 笔记：https://juejin.im/post/6844904113122050055&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Sat, 08 Aug 2020 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2020/08/08/%E7%B4%A2%E5%BC%95%E7%BB%93%E6%9E%84/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2020/08/08/%E7%B4%A2%E5%BC%95%E7%BB%93%E6%9E%84/</guid>
        
			<category>database</category>
        
        
		</item>
    
		<item>
			<title>Caffeine 的实现原理</title>
			<description>&lt;h1 id=&quot;caffeine-的实现原理&quot;&gt;Caffeine 的实现原理&lt;/h1&gt;

&lt;p&gt;缓存算法其实包含两个部分，准入策略（Admission Policy）以及淘汰策略（Eviction Policy）。一般情况下，对于一个元素，我们先判断是否接受该元素（使用准入策略），若接受的话，则从 Cache 中选择一个替代品（使用淘汰策略），从而把新元素放到 Cache 中。&lt;/p&gt;

&lt;p&gt;本文重点介绍 JAVA 中著名的缓存方案 &lt;a href=&quot;https://github.com/ben-manes/caffeine&quot;&gt;Caffeine&lt;/a&gt; 的底层实现，即 Tiny-LFU 算法，并首先简明地介绍了各个常见的缓存淘汰算法，包括实现原理以及优缺点讨论。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;码字不易，转载请声明出处，否则后果自负。&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;缓存淘汰算法&quot;&gt;缓存淘汰算法&lt;/h2&gt;

&lt;h3 id=&quot;lru&quot;&gt;LRU&lt;/h3&gt;

&lt;p&gt;LRU（Least recently used，最近最少使用）算法根据数据的历史访问记录来进行淘汰数据，其核心思想是 “如果数据最近被访问过，那么将来被访问的几率也更高”。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;通常使用链表实现，靠前的代表最近访问，靠后的代表不经常访问。当 Cache 满的时候，将 Cache 中最不经常访问的元素（链表的尾部节点）驱逐，并全盘接受新来的元素，直接插入到链表头部。&lt;/p&gt;

&lt;p&gt;优缺点&lt;/p&gt;

&lt;p&gt;当存在热点数据时，LRU 的效率很好，但偶发性的、周期性的批量操作会导致 LRU 命中率急剧下降，缓存污染情况比较严重，即会缓存大量长尾数据。&lt;/p&gt;

&lt;h3 id=&quot;lru-k&quot;&gt;LRU-K&lt;/h3&gt;

&lt;p&gt;LRU-K（Least Frequently Used K） 中的 K 代表最近使用的次数，因此 LRU 可以认为是 LRU-1，该算法相当于结合了 LRU 与 LFU 的思想。LRU-K 的主要目的是为了解决 LRU 算法 “缓存污染” 的问题，其核心思想是将 “最近使用过 1 次” 的判断标准扩展为 “最近使用过 K 次”。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;需要维护一个记录表来记录元素的访问次数，当且仅当元素的访问次数大于阈值 K 的时候，才将元素移动到 Cache 中。淘汰策略还是 LRU，只是相当于准入门槛变高而已。&lt;/p&gt;

&lt;p&gt;优缺点&lt;/p&gt;

&lt;p&gt;LRU-K 避免了长尾请求对 LRU 的影响。但是由于 LRU-K 还需要记录那些被访问过、但还没有放入缓存的对象，因此内存消耗会比 LRU 要多；另外，当 K 值的取值需要权衡，当 K 值很大的时候，LRU-K 的适应能力会变差，需要大量数据访问才能将历史访问清除。&lt;/p&gt;

&lt;h3 id=&quot;arc&quot;&gt;ARC&lt;/h3&gt;

&lt;p&gt;ARC（Adaptative Replacement Cache）结合了 LRU 跟 LFU 两个策略，维护了两个 LRU Cache（L1, L2）。L1 缓存只访问过一次的，L2 缓存至少访问过两次的。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;ARC 从 L1、L2 中分别划出两个子列表 T1、T2，其中 T1 代表最近访问，T2 代表最高频访问；并分别维护两个僵尸（Ghost）列表 B1、B2，分别存储从 T1、T2 中被驱逐的元素，B1、B2 都使用 LRU 策略。当两个 Cache 满了的时候，如果 T1 中的数据被驱逐，则该数据被存储到 B1 中，同理，T2 驱逐的数据存到 B2 中。&lt;/p&gt;

&lt;p&gt;T1 与 T2 的长度的动态调整，体现了一种负反馈调节的思想：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;
    &lt;p&gt;如果 B1 中的数据经常被访问，则说明 T1 不够长，所以会拓展 T1 长度，缩短 T2 长度，表现更像 LRU。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;如果 B2 中的数据经常被访问，则说明 T2 不够长，所以会拓展 T2 长度，缩短 T1 长度，表现更像 LFU。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;优缺点&lt;/p&gt;

&lt;p&gt;ARC 解决了 LRU 中 Non-Scan-Resistent 问题，即对于某些长尾请求，会导致 T1 中缓存了大量长尾数据，但是 T2 却不会受很大影响。&lt;/p&gt;

&lt;h3 id=&quot;slru&quot;&gt;SLRU&lt;/h3&gt;

&lt;p&gt;SLRU（Segmented LRU）将 LRU 分成保护段（protected segment）和试用段（probationary segment），其实思想跟 ARC 差不多，只不过没有动态调节段的占比，而且只有 T1 与 T2 之间的交换，舍弃了 B1、B2。保护段缓存访问超过一次的，试用段则全盘接受新来的数据。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;新数据会被存储在试用段，后续如果被访问到，则被提升到保护段。当保护段满的时候，数据会被淘汰至试用段，这时候如果试用段也满了的话，则联动使用 LRU 驱逐。&lt;/p&gt;

&lt;p&gt;局限性&lt;/p&gt;

&lt;p&gt;在该实现中，保护段与试用段的占比是固定的，因此对于分布经常变动的请求是次于 ARC 的。&lt;/p&gt;

&lt;h3 id=&quot;lfu&quot;&gt;LFU&lt;/h3&gt;

&lt;p&gt;LFU（Least Frequently Used）算法根据数据的历史访问频率来淘汰数据，其核心思想是 “如果数据过去被访问多次，那么将来被访问的频率也更高”。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;LFU 记录数据历史访问记录，即对极大部分数据（包括被驱逐的）都维护一个引用计数，当 Cache 满的时候，将 Cache 中频率最低的与新来的（频率高的）进行交换，并更新 Cache 中的排序（通常用最小堆实现）。&lt;/p&gt;

&lt;p&gt;优缺点&lt;/p&gt;

&lt;p&gt;一般情况下，LFU 效率要优于 LRU，且能够避免周期性或者偶发性的操作导致缓存命中率下降的问题。但 LFU 需要记录数据的历史访问记录，一旦数据访问模式改变，LFU 需要更长时间来适用新的访问模式。&lt;/p&gt;

&lt;p&gt;另一方面，经典算法中，维护引用计数需要占用大量内存空间，并且每次更新都需要重新依照访问计数排序，在实际应用中不可能直接这么硬干。&lt;/p&gt;

&lt;h3 id=&quot;lfu-aging&quot;&gt;LFU-Aging&lt;/h3&gt;

&lt;p&gt;基于 LFU 的改进算法，其核心思想是 “除了访问次数外，还要考虑访问时间”。例如，两天前的一个热门视频在今天可能无人问津，但是因为访问频率曾经非常高，导致这个视频依然占坑在缓存中。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;该算法的实现其实有很多种，例如 In-Memory LFU 采用了两个策略：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;限制最高访问计数。即达到阈值则不再增加计数。&lt;/li&gt;
  &lt;li&gt;周期性对引用计数作衰减操作，例如除予某个系数。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;优缺点&lt;/p&gt;

&lt;p&gt;LFU-Aging 因为减少了 LFU 带来的高频旧数据的缓存污染问题，相比于 LFU 能够更快适应新的访问模式。但是依然避免不了 LFU 的两个问题，并且衰减系数也是个参数活，需要慢慢调节。&lt;/p&gt;

&lt;h3 id=&quot;window-lfu&quot;&gt;Window-LFU&lt;/h3&gt;

&lt;p&gt;上述 LFU-Aging 解决了 LFU 其中一个痛点，即不适应访问模式的剧烈变化。而 Window-LFU 旨在解决 LFU 需要维护所有数据的访问历史带来的巨大内存消耗的问题。顾名思义，Window-LFU 维护了一个定长计数窗口，记录最近访问的 W 个元素的访问计数。&lt;/p&gt;

&lt;p&gt;实现&lt;/p&gt;

&lt;p&gt;维护一个长度 W 的 FIFO 计数队列（Window），记录最近 W 个元素，元素的计数以 Window 中的计数为准，当 Cache 满了的时候，用 WIndow 中的计数进行驱逐。&lt;/p&gt;

&lt;h2 id=&quot;tinylfu&quot;&gt;TinyLFU&lt;/h2&gt;

&lt;p&gt;顾名思义，TinyLFU 也是 LFU 的一种变体，TinyLFU 方案可以分为两个重要部分，分别是实现 TinyLFU 的准入策略和 Cache 实现的驱逐策略（可以使用多种置换方案，如 LFU、LRU）。当新来一个元素时，Cache 选择要驱逐的元素（victim），TinyLFU 通过引用计数判断替换为新来的元素是否有收益（即是否会带来更高的命中率）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://segmentfault.com/img/bVbfGh9?w=866&amp;amp;h=428&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;近似引用计数&quot;&gt;近似引用计数&lt;/h3&gt;

&lt;p&gt;近年来有很多针对大数据流的统计研究，期望得到一种压缩的数据结构，例如用来反映数据流中元素的出现频率。比较著名的一类算法是基于布隆过滤器的 Sketch。布隆过滤器用来判断一个给定元素是否存在，而 Sketch 用来判断给定元素出现的频率，具体的做法与布隆过滤器类似，使用一组哈希函数，映射到引用计数矩阵的某几个值，通过对应的几个结果值来反映给定元素的近似出现频率。例如，Minimal Increment Counting Bloom Filter 取的是几个哈希结果的最小值作为频率的近似值，每次新增元素也只对最小计数值进行递增操作。&lt;/p&gt;

&lt;p&gt;但是这类算法有一个问题，现实中的数据流通常带有大量长尾请求，这些请求的出现频率非常低，很容易就把 Sketch 打得饱和，使得计数有很大误差。&lt;/p&gt;

&lt;p&gt;TinyLFU 提出的方案很简单，每次添加一个新元素时，则对一个 counter 进行递增，当计数达到某一个阈值的时候，则对所有计数器作衰减操作（乘上某个衰减系数）。&lt;/p&gt;

&lt;p&gt;由于 counter 都是整型，因此长尾元素的计数大概率会被衰减操作置零；另外一方面，衰减操作也会把高频的旧数据逐渐置零，相当于一个 Aging 操作。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://image-static.segmentfault.com/198/843/1988435212-58d64be00bb9d_articlex&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;准入策略&quot;&gt;准入策略&lt;/h3&gt;

&lt;p&gt;上文提到，TinyLFU 负责准入策略，其实就是把即将淘汰的元素（Cache 中的 victim）与即将到来的元素进行对比，对比的依据是两者在 Sketch 中的计数。&lt;/p&gt;

&lt;p&gt;虽然上述 Sketch 的重置方案能够解决长尾数据带来的缓存污染问题，但是 TinyLFU 还引入了布隆过滤器，后面称为 Doorkeeper，来进一步减少这个问题的影响。具体的做法如下：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;判断新来的元素是否在 doorkeeper 中，没有的话，则插入 doorkeeper，否则插入 Sketch 中&lt;/li&gt;
  &lt;li&gt;当请求一个元素的时候，如果元素在 doorkeeper 中的话，则返回 Sketch 中的计数+1，否则直接返回 Sketch 中的计数。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://i.imgur.com/LhQYje5.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;每次对 Sketch 进行重置的时候，也需要对 doorkeeper 进行清空操作。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;由于前置布隆过滤器 Doorkeeper 的存在，计数为整型的 Sketch 占用的内存可以更小，因为一些长尾请求已经被 Doorkeeper 拒之门外。&lt;/p&gt;

&lt;h3 id=&quot;驱逐策略&quot;&gt;驱逐策略&lt;/h3&gt;

&lt;p&gt;其实驱逐策略很简单，就是一个普通的 LFU 策略，每次需要淘汰的时候，选择频率最小（通常用最小堆实现）的元素进行替换。&lt;/p&gt;

&lt;h3 id=&quot;w-tinylfu&quot;&gt;W-TinyLFU&lt;/h3&gt;

&lt;p&gt;TinyLFU 其实对一些突如其来的高频请求不够友好，因为这些请求很可能在积攒足够频率之前就被淘汰了，通常来说，LRU 对这类请求有着更好的效果。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://image-static.segmentfault.com/274/307/2743073853-58d64be0088fe_articlex&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在该结构中，Cache 使用 SLRU 驱逐策略及 TinyLFU 准入策略，而 Window 则使用 LRU 驱逐策略并全盘接受任何新元素（没有准入门槛）。&lt;/p&gt;

&lt;p&gt;每当一个元素进来时，都会被 Window 接受，被 Window 淘汰的元素有机会通过 Filter（Doorkeeper+Sketch）与 Cache 中的元素竞争。如果竞争成功的话，则相当于 Cache 中的元素被驱逐，否则相当于 Window 中的元素被驱逐。&lt;/p&gt;

&lt;p&gt;可以看到 W-TinyLFU 兼具了 LRU 与 LFU 的优点。该算法也是 JAVA 中著名的缓存 Caffeine 的底层实现方案。&lt;/p&gt;

&lt;h2 id=&quot;reference&quot;&gt;Reference&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;ARC 算法：https://blog.csdn.net/WSKINGS/article/details/46416451&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Caffein 简介：http://highscalability.com/blog/2016/1/25/design-of-a-modern-cache.html&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;Tiny-LFU 论文：https://arxiv.org/pdf/1512.00727.pdf&lt;/li&gt;
  &lt;li&gt;论文简单解读：https://segmentfault.com/a/1190000016091569?utm_source=tag-newest&lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Fri, 24 Jul 2020 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2020/07/24/Tiny-LFU/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2020/07/24/Tiny-LFU/</guid>
        
			<category>cache</category>
        
        
		</item>
    
		<item>
			<title>Go 生态下的 Cache 评测</title>
			<description>&lt;h1 id=&quot;caching-in-go&quot;&gt;Caching in Go&lt;/h1&gt;

&lt;p&gt;本文主要介绍 Go 生态下面比较有名的几个 Cache，剖析这些 Cache 实现的原理，并分析各自存在的不足之处。最后，介绍如何编写 Benchmark 来对比不同的 Cache，方便喜欢造轮子的同学进行测试。&lt;/p&gt;

&lt;p&gt;大家第一次接触 Cache，也许都是因为 LeetCode 上面这么一道 LRUCache 的题目。对 LRU 不太了解的同学可以再来做一下这道题目复习复习。&lt;/p&gt;

&lt;h2 id=&quot;1-lru-cache&quot;&gt;1. LRU Cache&lt;/h2&gt;

&lt;div class=&quot;language-c++ highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cm&quot;&gt;/* 
Design and implement a data structure for Least Recently Used (LRU) cache. It should support the following operations: get and put.
get(key) - Get the value (will always be positive) of the key if the key exists in the cache, otherwise return -1.
put(key, value) - Set or insert the value if the key is not already present. When the cache reached its capacity, it should invalidate the least recently used item before inserting a new item.
Follow up:
Could you do both operations in O(1) time complexity?
Example:
*/&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;LRUCache&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;LRUCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;cm&quot;&gt;/* capacity */&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// returns 1&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;// evicts key 2&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// returns -1 (not found)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;// evicts key 1&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// returns -1 (not found)&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// returns 3&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;    &lt;span class=&quot;c1&quot;&gt;// returns 4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;最简洁的实现&lt;/p&gt;

&lt;div class=&quot;language-py highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;LRUCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;__init__&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;capacity&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;OrderedDict&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;capacity&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;c1&quot;&gt;# move to the end
&lt;/span&gt;    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move_to_end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;put&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cap&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;and&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# pop the front item
&lt;/span&gt;      &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;popitem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;last&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;bp&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;elif&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;move_to_end&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;bp&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lru&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;2-并发支持&quot;&gt;2. 并发支持&lt;/h2&gt;

&lt;p&gt;原生的 LRU Cache 显然不是并发安全的，因为每次 get 或者 set 都涉及到了链表节点移动的操作。要使得 LRU Cache 并发安全，最简单暴力的做法是，直接对整个 LRU Cache  加锁，每次 put、get 都需要 lock 一下，例如 1.8k stars 的 https://github.com/hashicorp/golang-lru，以及基于该库进行二层封装的其他共用库。代码差不多长这样子：&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lockedCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expiresAt&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Lock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expiresAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;l&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lockedCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{})&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Lock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;l&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;f&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这样导致的问题是，当并发 Get 操作很多的时候，Get 操作已经变成串行。也就说对整个 Cache 进行加锁是不明智的。&lt;/p&gt;

&lt;h3 id=&quot;解决1分桶&quot;&gt;解决1：分桶&lt;/h3&gt;

&lt;p&gt;将一个 hashtable 根据 key 拆分成多个 hashtable，每个 hashtable 对应一个锁，锁粒度更细，冲突的概率也就更低了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/6mxr7dS/1.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;解决2延缓提权&quot;&gt;解决2：延缓提权&lt;/h3&gt;

&lt;p&gt;对于 get 操作，原生 LRU Cache 每次都会有一次 move_to_front 的操作，因此每次 get 都会涉及整个 cache 的加锁操作，这会给 cache 的性能大打折扣。&lt;/p&gt;

&lt;p&gt;解决方案是，为每个 Item 都维护一个访问计数 promotions ，当且仅当 promotions 达到阈值的时候，才触发 move_to_front 操作。&lt;/p&gt;

&lt;h2 id=&quot;3-几个实现&quot;&gt;3. 几个实现&lt;/h2&gt;

&lt;h3 id=&quot;31-bigcache&quot;&gt;3.1. BigCache&lt;/h3&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/allegro/bigcache&quot;&gt;BigCache&lt;/a&gt; 根据键值的 hash 将数据分散到多个分片上面进行存储，实现了解决方案一，并根据 Golang 的 GC 做了优化。每个分片的底层存储结构是一个环形缓冲区 entries， 并维护一个 key 到缓冲区 index 的映射。&lt;/p&gt;

&lt;h4 id=&quot;cache-的定义&quot;&gt;Cache 的定义&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;BigCache&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;shards&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cacheShard&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;// 分片&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;lifeWindow&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;     &lt;span class=&quot;c&quot;&gt;// 过期时间&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;clock&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;clock&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;hash&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;Hasher&lt;/span&gt;     &lt;span class=&quot;c&quot;&gt;// 实现了hash算法的interface，所以可以自定义hash&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;config&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;Config&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;shardMask&lt;/span&gt;   &lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;maxShardSize&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;close&lt;/span&gt;     &lt;span class=&quot;k&quot;&gt;chan&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;shard-的定义&quot;&gt;Shard 的定义&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cacheShard&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;hashmap&lt;/span&gt;   &lt;span class=&quot;k&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;// 存储在环形缓冲区 entries 中的元素的 index&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;queue&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;BytesQueue&lt;/span&gt;   &lt;span class=&quot;c&quot;&gt;// 实际存储内容的 byte 数组，是个ring buffer&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;     &lt;span class=&quot;n&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RWMutex&lt;/span&gt;     &lt;span class=&quot;c&quot;&gt;// shard的全局锁&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;entryBuffer&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;        &lt;span class=&quot;c&quot;&gt;// 预分配的一段内存&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;onRemove&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;onRemoveCallback&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;isVerbose&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;bool&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;   &lt;span class=&quot;n&quot;&gt;Logger&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;clock&lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;clock&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;lifeWindow&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;hashmapStats&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;stats&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Stats&lt;/span&gt;           &lt;span class=&quot;c&quot;&gt;// 统计信息&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;每个 shard 的结构如图所示。在 BigCache 的实现中，map 的 key 为原始 key 的哈希值 (uint64)，value 则为一个 int 类型的 index。其中，index 对应底层 ByteQueue 中的某个位置，在读取操作时，BigCache 从 ByteQueue 中取出序列化的 [] byte 片段，从而还原出 value 信息。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/NYKQ5Vq/2.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;set-操作&quot;&gt;Set 操作&lt;/h4&gt;

&lt;p&gt;每当 set 一个新元素的时候，会把 value 序列化后塞进环形缓冲区 entries，然后记录 key 及 value 存储的  index 到 hashmap 中。如果 key 对应的 entry 已经在 entries 中，则之前的 entry 会被 reset。&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cacheShard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;currentTimestamp&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;clock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;epoch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Lock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 1. 查找是否已经存在了对应的缓存对象，如果存在，将它的值置为空&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;previousIndex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hashmap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;];&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;previousIndex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;previousEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;previousIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;));&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;n&quot;&gt;resetKeyFromEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;previousEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 2. 取出最老的缓存对象，判断是否要过期，是的话则淘汰&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldestEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Peek&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;onEvict&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldestEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;currentTimestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;removeOldestEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 将对象放入到一个字节数组中&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;w&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;wrapEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;currentTimestamp&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entryBuffer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 放入到字节队列中&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Push&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;w&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hashmap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
   &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 3. 如果空间不足，移除最老的元素&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;removeOldestEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NoSpace&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Errorf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;entry is bigger than max shard size&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;一个潜在的问题是，上述做法相当于把 map 的 GC 压力转到了 slice (array)，是否真的能带来性能上的提升？事实上，在 golang 中，不存在释放部分数组的情况，即数组的内存管理有两种情况：1. 全部释放；2. 继续保留。因此，对于数组的 GC 时间复杂度可理解为 O (1) 时间。详见 https://golangbot.com/arrays-and-slices/&lt;/p&gt;

&lt;h4 id=&quot;get-操作&quot;&gt;Get 操作&lt;/h4&gt;

&lt;p&gt;get 操作比较简单，根据 hashedKey 获取 itemIndex（环形 buffer 中的 offset），然后根据 itemIndex 获取 entry（即我们存储的序列化的对象）。不过，注意这里有一个对 key 值的二次判断，因为 hash 冲突的存在，实际存储的 key 可能并不一致。&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cacheShard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;([]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RLock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;itemIndex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hashmap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;itemIndex&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RUnlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;miss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ErrEntryNotFound&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 根据 index 取出 entry 的 header 信息&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;wrappedEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entries&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;itemIndex&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RUnlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;miss&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entryKey&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;readKeyFromEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrappedEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entryKey&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// 由于对于冲突的key只会存储最近的一个，所以当hash值一样时还要具体在看key是不是想要的key&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;isVerbose&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Collision detected. Both %q and %q have the same hash %x&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entryKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hashedKey&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RUnlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;collision&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ErrEntryNotFound&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 根据 header 信息取出实际存储的 entry&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;readEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;wrappedEntry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lock&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RUnlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;小结&quot;&gt;小结&lt;/h4&gt;

&lt;h5 id=&quot;缓存策略&quot;&gt;缓存策略&lt;/h5&gt;

&lt;p&gt;不是，其实是 FIFO。每次 set 会把新元素放到后面，如果有冲突，则把先前的元素删除。get 操作仅仅是一个读操作，读到非过期则返回。&lt;/p&gt;

&lt;h5 id=&quot;驱逐策略&quot;&gt;驱逐策略&lt;/h5&gt;

&lt;p&gt;BigCache 的驱逐策略有三个。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;首先，在增加一个元素之前，会检查最老的元素要不要删除。过期则删除&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;其次，在添加一个元素失败后，会清理空间删除最老的元素。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;同时， 还会专门有一个定时的清理 goroutine, 负责移除过期数据。&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;槽点&quot;&gt;槽点&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Set 相同的 key 会导致 BigCache  出现气泡，因为 BigCache 没有尝试重复利用这些空间。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;BigCache 使用的是 FIFO 策略，因此对于一般的 Zipf 分布请求不友好&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;优点&quot;&gt;优点&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Get 请求是无锁的，非常快&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;无 GC 压力&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;32-freecache&quot;&gt;3.2 &lt;strong&gt;FreeCache&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;FreeCache 将 Cache 切成 256 个 Segment，每个 Segment 包含 256 个 slot 以及一个 ringbuffer 来存放具体数据，每个 slot 可以存放多个 entry，指向 ringbuffer 中存放的 item 具体位置。当 add 一个元素时，首先会由 LSB(hash)[:8] 定位到某个 segment，并由 LSB(hash)[8:16] 定位到该 segment 下面的 slot。每个 slot 按 hash 递增的顺序存储多个 entryPtr（不使用 hashMap 应该是基于节约空间以及冲突的考虑）。&lt;/p&gt;

&lt;h4 id=&quot;cache-的定义-1&quot;&gt;Cache 的定义&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;locks&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;segmentCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Mutex&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;segments&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;segmentCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;segment&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;segment-的定义&quot;&gt;Segment 的定义&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// a segment contains 256 slots, a slot is an array of entry pointers ordered by hash16 value&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// the entry can be looked up by hash value of the key.&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;segment&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;       &lt;span class=&quot;n&quot;&gt;RingBuf&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// ring buffer that stores data&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;segId&lt;/span&gt;     &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;       &lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;// ... 一些统计值&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;vacuumLen&lt;/span&gt;   &lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;    &lt;span class=&quot;c&quot;&gt;// up to vacuumLen, new data can be written without overwriting old data.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;slotLens&lt;/span&gt;    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int32&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// The actual length for every slot.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;slotCap&lt;/span&gt;    &lt;span class=&quot;kt&quot;&gt;int32&lt;/span&gt;    &lt;span class=&quot;c&quot;&gt;// max number of entry pointers a slot can hold.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;slotsData&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entryPtr&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// shared by all 256 slots&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// entry pointer struct points to an entry in ring buffer&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entryPtr&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;offset&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;// entry offset in ring buffer&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;hash16&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;uint16&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// entries are ordered by hash16 in a slot.&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;keyLen&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;uint16&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// used to compare a key&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;reserved&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/KXFrGt6/3.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;set-操作-1&quot;&gt;Set 操作&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expireSeconds&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 获取 segID 以及 slotID&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 对 segment 加锁&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;slot&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;getSlot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slotId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// []entryPtr&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lookup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hash16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 如果冲突的话，旧空间足够则直接覆盖，否则标记删除旧空间&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;match&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 如果老的 entry 空间足够容得下新的 entry 的话，则 in-place 修改，return&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;valCap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;valLen&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;// 更新 header 以及 value，key 不变&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;WriteAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdrBuf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;matchedPtr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;offset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;WriteAt&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;matchedPtr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;offset&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ENTRY_HDR_SIZE&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keyLen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 否则，在 ringbuff 中标记删除该 entry，在 slot 中直接把该 entryPtr 删除&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delEntryPtr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slotId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slot&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// ...&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 对该 slot 执行写时驱逐策略，当且仅当剩余空间不够才进行驱逐&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;evacuate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;entryLen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;slotId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 写入 ringbuff 以及 entryPtr&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;insertEntryPtr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slotId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hash16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;End&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;idx&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keyLen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdrBuf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Write&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Skip&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;valCap&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;hdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;valLen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;写时驱逐策略&quot;&gt;写时驱逐策略&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vacuumLen&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entryLen&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// 取出队首&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 1. 如果队首被标记删除&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deleted&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 则将该空间添加到可利用空间&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;continue&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 2. 已经过期&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;expired&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;expireAt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;expireAt&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;now&lt;/span&gt;
 &lt;span class=&quot;c&quot;&gt;// 3. 该 entry 的访问时间小于整个 segment 的平均访问时间（近 LRU 策略）&lt;/span&gt;
 &lt;span class=&quot;n&quot;&gt;leastRecentUsed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;accessTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;atomic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LoadInt64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;totalCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;atomic&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;LoadInt64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;totalTime&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expired&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;leastRecentUsed&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;consecutiveEvacuate&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;5&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 标记删除，并将可利用空间回收&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;delEntryPtrByOffset&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slotId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hash16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldOff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vacuumLen&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldEntryLen&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;c&quot;&gt;// 将队首移到队尾，更新 slot 的 entryPtr 信息，提高命中率&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;newOff&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;rb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Evacuate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldOff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldEntryLen&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;seg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;updateEntryPtr&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slotId&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldHdr&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;hash16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;oldOff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newOff&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
 &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;get-操作-1&quot;&gt;Get 操作&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cache&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;key的哈希值不存在&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ErrNotFound&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;对应的entry已过期&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;delete&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ErrNotFound&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;update&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;entry_header&lt;/span&gt;   
  &lt;span class=&quot;n&quot;&gt;读取entry中的value&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;BTW，FreeCache 还做了内存对齐优化，详见： https://go101.org/article/memory-layout.html，https://ms2008.github.io/2019/08/01/golang-memory-alignment/&lt;/p&gt;

&lt;h4 id=&quot;小结-1&quot;&gt;小结&lt;/h4&gt;

&lt;h5 id=&quot;槽点-1&quot;&gt;槽点&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;没有独立的 goroutine 来进行驱逐，相当于把驱逐的压力都放在了 set 操作的时候（可能是害怕独立的 goroutine 对锁持有时间的不可控制性？）&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;优点-1&quot;&gt;优点&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;近 LRU 策略&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;尝试对老空间的再利用&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;内存对齐优化&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;提权策略&quot;&gt;提权策略&lt;/h5&gt;

&lt;p&gt;提权是通过修改 entry 的过期时间实现的（用于近似 LRU 驱逐）&lt;/p&gt;

&lt;h5 id=&quot;驱逐策略-1&quot;&gt;驱逐策略&lt;/h5&gt;

&lt;p&gt;驱逐策略主要有三个：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;被标记删除的（set 的时候发现相同 key 的旧 entry；get的时候发现 expired 的 entry）&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;已经过期的&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;entry 的访问时间小于整个 segment 的平均访问时间&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;33-ccache&quot;&gt;3.3 CCache&lt;/h3&gt;

&lt;p&gt;主要实现了解决方案 1、2。&lt;/p&gt;

&lt;h4 id=&quot;item-的结构&quot;&gt;Item 的结构&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;     &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;group&lt;/span&gt;    &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;promotions&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int32&lt;/span&gt;    &lt;span class=&quot;c&quot;&gt;// 计数窗口&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;refCount&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;int32&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expires&lt;/span&gt;   &lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;    &lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;    &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;element&lt;/span&gt;   &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Element&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;cache-的定义-2&quot;&gt;Cache 的定义&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Configuration&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;list&lt;/span&gt;     &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;list&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;List&lt;/span&gt; &lt;span class=&quot;c&quot;&gt;// LRU&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;     &lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;buckets&lt;/span&gt;   &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;// entrys&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;bucketMask&lt;/span&gt;  &lt;span class=&quot;kt&quot;&gt;uint32&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;deletables&lt;/span&gt;  &lt;span class=&quot;k&quot;&gt;chan&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;promotables&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;chan&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;control&lt;/span&gt;   &lt;span class=&quot;k&quot;&gt;chan&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;bucket-的结构&quot;&gt;Bucket 的结构&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sync&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RWMutex&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;lookup&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RLock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;defer&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;RUnlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lookup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;b&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;expires&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Add&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UnixNano&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newItem&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;expires&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Lock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lookup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lookup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Unlock&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;set-操作-2&quot;&gt;Set 操作&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;duration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;!=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;// 删除先前含有相同 key 的 item&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deletables&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;existing&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;// 提权&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;promote&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;promote&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;promotables&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/XyB2dmL/6.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;get-操作-2&quot;&gt;Get 操作&lt;/h4&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;// Get an item from the cache. Returns nil if the item wasn&apos;t found.&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// This can return an expired item. Use item.Expired() to see if the item&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// is expired and item.TTL() to see how long until the item expires (which&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;// will be negative for an already expired item).&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Item&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bucket&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;c&quot;&gt;// 没有过期，则提权&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;expires&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;UnixNano&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;promote&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/qRFbnhS/7.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;清道夫协程&quot;&gt;清道夫协程&lt;/h4&gt;

&lt;p&gt;在 new 一个 cache 的时候，会起一个独立的 goroutine 来处理需要提权、删除的 Item：&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;defer&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;dropped&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ok&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;promotables&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ok&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;false&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;goto&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;drain&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doPromote&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;maxSize&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;dropped&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deletables&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doDelete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;control&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;switch&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;control&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getDropped&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;res&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dropped&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;dropped&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;setMaxSize&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;maxSize&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;msg&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;
                &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;maxSize&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;dropped&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;drain&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;select&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deletables&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;doDelete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;nb&quot;&gt;close&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deletables&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;小结-2&quot;&gt;小结&lt;/h4&gt;

&lt;p&gt;CCache 主要使用了延缓提权、分桶策略，来减少并发获取 key 的锁冲突，实现非常简单。&lt;/p&gt;

&lt;p&gt;但是底层仍然使用指针，避免不了 GC 压力。&lt;/p&gt;

&lt;h3 id=&quot;34-benchmark&quot;&gt;3.4 &lt;strong&gt;BenchMark&lt;/strong&gt;&lt;/h3&gt;

&lt;p&gt;这里直接引用 DGraph 的一篇评测结果。该文章对比了 BigCache、FreeCache 以及 GroupCache 在只读、只写、混合读写的性能对比。&lt;/p&gt;

&lt;h4 id=&quot;只读&quot;&gt;只读&lt;/h4&gt;

&lt;p&gt;在只读场景下，BigCache 性能最优，因为在 BigCache 中对分片用了读写锁，所以只读场景下是无锁的。而 FreeCache 以及 GroupCache 在读场景下都需要对分片进行操作，因此加了 mutex，因此性能次于 BigCache。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/fDGQQt1/8.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;只写&quot;&gt;只写&lt;/h4&gt;

&lt;p&gt;在只写场景下，三者性能差别不大，但是 FreeCache 性能更优。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/9g4TCp8/4.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;混合读写-25-writes-75-reads&quot;&gt;混合读写 (25% writes, 75% reads)&lt;/h4&gt;

&lt;p&gt;看起来只有 BigCache 对并发友好。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/XDC7BbX/5.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h4 id=&quot;zipf-分布缓存命中率&quot;&gt;Zipf 分布缓存命中率&lt;/h4&gt;

&lt;p&gt;什么是 Zipf 分布？它可以表述为：在&lt;a href=&quot;https://zh.wikipedia.org/wiki/自然语言&quot;&gt;自然语言&lt;/a&gt;的&lt;a href=&quot;https://zh.wikipedia.org/wiki/語料庫&quot;&gt;語料庫&lt;/a&gt;裡，一个单词出现的频率与它在频率表里的排名成&lt;a href=&quot;https://zh.wikipedia.org/wiki/反比&quot;&gt;反比&lt;/a&gt;，说人话则是，出现频率高的，则更容易被访问，比如搜索结果、淘宝销量排名等等。放在请求的场景下则是，热门内容越容易被请求。&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;CACHE SIZE (# OF ELEM)&lt;/th&gt;
      &lt;th&gt;10000&lt;/th&gt;
      &lt;th&gt;100000&lt;/th&gt;
      &lt;th&gt;1000000&lt;/th&gt;
      &lt;th&gt;10000000&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;BigCache&lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
      &lt;td&gt;37%&lt;/td&gt;
      &lt;td&gt;52%&lt;/td&gt;
      &lt;td&gt;55%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;FreeCache&lt;/td&gt;
      &lt;td&gt;-&lt;/td&gt;
      &lt;td&gt;38%&lt;/td&gt;
      &lt;td&gt;55%&lt;/td&gt;
      &lt;td&gt;90%&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;GroupCache&lt;/td&gt;
      &lt;td&gt;29%&lt;/td&gt;
      &lt;td&gt;40%&lt;/td&gt;
      &lt;td&gt;54%&lt;/td&gt;
      &lt;td&gt;90%&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;FreeCache 和 GroupCache 都作了近 LRU 策略，而 BigCache 相当于 FIFO，因此 BigCache 对于 Zipf 分布的请求不够友好，原因如下：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;BigCache 没有充分利用 buffer 的空间，如果有大量相同的 key 写入的话，会导致 ringbuffer 中存在相同的 key，并且产生气泡。&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;BigCache 没有在 get 的时候对 entry 进行提权，有可能导致最近访问的 key 被驱逐&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;4-编写-benchmark&quot;&gt;4. 编写 Benchmark&lt;/h2&gt;

&lt;p&gt;每当完成一个轮子，我们必须和业界其他轮子进行 PK。如果仅仅弄一个能跑的轮子，那还不简单，重要的是看谁能有更强的 performance，在 PK 中发现自己的不足，吸收别人的优点，才能造出更强的轮子。&lt;/p&gt;

&lt;p&gt;下面的 benchmark 来自 https://github.com/dgraph-io/benchmarks/blob/master/cachebench/cache_bench_test.go，我们只需要实现我们自己的 Cache 的测试接口即可。下面例子是 CCache：&lt;/p&gt;

&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;type&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;CCache&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;c&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ccache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Cache&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;([]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Get&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;errKeyNotFound&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
   &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;r&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;byte&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;r&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;c&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Set&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Second&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;newCCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keysInWindow&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CCache&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;cc&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;ccache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;New&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ccache&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Configure&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;MaxSize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int64&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;keysInWindow&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ItemsToPrune&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;m&quot;&gt;500&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;CCache&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;cc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;下面是结果&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;BenchmarkCaches/CCacheZipfMixed-4         5214476        252 ns/op        28 B/op      2 allocs/op
BenchmarkCaches/FastCacheZipfMixed-4       12887888         89.0 ns/op       6 B/op      0 allocs/op
BenchmarkCaches/FreeCacheZipfMixed-4       21327250         59.1 ns/op       1 B/op      0 allocs/op
BenchmarkCaches/GroupCacheZipfMixed-4       9320209        118 ns/op        13 B/op      0 allocs/op
BenchmarkCaches/CCacheOneKeyMixed-4        2775771        449 ns/op        22 B/op      2 allocs/op
BenchmarkCaches/CCacheZipfRead-4         7524876        143 ns/op        16 B/op      2 allocs/op
BenchmarkCaches/FastCacheZipfRead-4       17824086         64.9 ns/op       7 B/op      0 allocs/op
BenchmarkCaches/FreeCacheZipfRead-4       28711682         60.4 ns/op       1 B/op      0 allocs/op
BenchmarkCaches/GroupCacheZipfRead-4       10563777        126 ns/op        0 B/op      0 allocs/op
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;5-展望&quot;&gt;5. 展望&lt;/h2&gt;

&lt;p&gt;缓存有很多种，本文只介绍了 Golang 生态下几个著名的 Cache，其中 FreeCache 跟 CCache 是 LRU 策略，而 BigCache 是 FIFO 策略。但是仍然可以有更好的淘汰策略值得探索，例如 Java 生态下非常有名的 Caffeine，使用了 Tiny-LFU（作者声称已经接近最优解），但是似乎 Golang 生态下的经过工业级验证过的使用其他更优淘汰策略的 Cache 还是欠缺的。&lt;/p&gt;

&lt;h2 id=&quot;6-reference&quot;&gt;6. Reference&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;https://www.openmymind.net/Shard-Your-Hash-table-to-reduce-write-locks/&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://www.openmymind.net/High-Concurrency-LRU-Caching/&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;http://highscalability.com/blog/2016/1/25/design-of-a-modern-cache.html&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://dgraph.io/blog/post/caching-in-go/&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://go101.org/article/memory-layout.html，https://ms2008.github.io/2019/08/01/golang-memory-alignment/&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://github.com/dgraph-io/benchmarks/tree/master/cachebench&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://blog.golang.org/ismmkeynote&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Sat, 18 Jul 2020 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2020/07/18/Caching-in-go/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2020/07/18/Caching-in-go/</guid>
        
			<category>golang</category>
        
        
		</item>
    
		<item>
			<title>使用 Docker 搭建自己的编程环境</title>
			<description>&lt;h1 id=&quot;使用-docker-搭建自己的编程环境&quot;&gt;使用 Docker 搭建自己的编程环境&lt;/h1&gt;

&lt;h2 id=&quot;1-docker-简介&quot;&gt;1. Docker 简介&lt;/h2&gt;

&lt;h3 id=&quot;11-什么是-docker&quot;&gt;1.1. 什么是 Docker&lt;/h3&gt;

&lt;p&gt;Docker 是什么？&lt;a href=&quot;https://www.zhihu.com/question/28300645/answer/67707287&quot;&gt;如何通俗解释Docker是什么？ - 刘允鹏的回答 - 知乎&lt;/a&gt; 这个回答通俗易懂地从环境依赖、系统依赖、与虚拟机的区别等方面介绍了 Docker 的作用。&lt;/p&gt;

&lt;p&gt;此外，在云时代的今天，Docker 与 K8S 更是无缝结合，为我们开发、测试、部署、交付、维护应用提供了便捷的手段。&lt;/p&gt;

&lt;p&gt;不过 K8S 就是另外一个话题了，我们先从 Docker 入手来窥探 Docker 的使用方法以及基本原理。读了本文，你讲了解到：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Docker client 与 daemon 分别是什么，如何工作&lt;/li&gt;
  &lt;li&gt;Docker 常用命令以及一些注意事项&lt;/li&gt;
  &lt;li&gt;为什么要为非 root 身份用户构建环境，以及如何用该身份运行 Docker&lt;/li&gt;
  &lt;li&gt;一些好用的编程工具及配置方法&lt;/li&gt;
  &lt;li&gt;如何在容器中访问及编辑（读写）你宿主机中的代码&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;至于如何安装 Docker，则可以参考官方文档：http://docs.docker.com/engine/installation/&lt;/em&gt;&lt;/p&gt;

&lt;h3 id=&quot;12-docker-如何工作&quot;&gt;1.2. Docker 如何工作？&lt;/h3&gt;

&lt;p&gt;Docker 会在 Linux 虚拟机（VM）中运行。如果你使用的是 Mac 或者 Windows，Docker 会为你创建一个 VM 并且在其中运行 Docker 守护进程（Docker Daemon），相当于一个服务端程序，而 Docker 会开另外一个客户端进程供用户交互。&lt;/p&gt;

&lt;p&gt;首先，如果从我们编写的 Dockerfile 来编译镜像的话，则执行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker build&lt;/code&gt;。编译过程并不是直接在 client 端运行的。当用户执行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker build&lt;/code&gt; 时，client 会把所有依赖文件上传给 daemon。daemon 与 client 可以不在同一台机器上面，这意味着上传过程可能需要依赖网络传输，因此&lt;strong&gt;尽量不要依赖不必要文件，这会直接影响编译速度&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;在编译时，Docker 会把基础镜像（Base Image）从镜像仓库拉取到本地。&lt;strong&gt;镜像并不是单一的一个二进制文件，而是由多个层组成。&lt;/strong&gt;多个镜像可能共享同一个层，这一特点使得镜像存储、传输都非常轻量级。例如你使用同一个基础镜像创建多个镜像，那么基础镜像只会存储一次。每次从镜像仓库如 Docker Hub 拉取的时候，只会拉取本地没有的层。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/8c323hS/200626-0.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;p&gt;其次是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run&lt;/code&gt; 命令，该命令用来运行某一镜像（image）。当用户执行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run&lt;/code&gt; 命令时，docker 首先会检查镜像是否已经在本地，如果不在本地的话，则从 Docker Hub 拉取镜像到本地，然后开启容器进程运行。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://i.ibb.co/qd3LcMv/200626-1.png&quot; alt=&quot;img&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;13-dockerfile-基本命令及示例&quot;&gt;1.3. Dockerfile 基本命令及示例&lt;/h3&gt;

&lt;h4 id=&quot;run&quot;&gt;RUN&lt;/h4&gt;

&lt;p&gt;每个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN&lt;/code&gt; 命令会在当前镜像的最新的一层执行命令并提交结果，Dockerfile 的下一步执行则会基于上一步提交的镜像进行。&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN&lt;/code&gt; 命令有两种格式：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN &amp;lt;command&amp;gt;&lt;/code&gt; ，或者称为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shell&lt;/code&gt; 格式：默认使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/bin/sh&lt;/code&gt; 来执行&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN [&quot;executable&quot;, &quot;param1&quot;, &quot;param2&quot;]&lt;/code&gt; ，或者称为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exec&lt;/code&gt; 格式&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;注意的是，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN&lt;/code&gt; 命令默认使用的 shell 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/bin/sh&lt;/code&gt; ，这意味着某些 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/bin/sh&lt;/code&gt;  不支持的命令则执行会报错，例如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source &lt;/code&gt;。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;source&lt;/code&gt; 命令默认的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/bin/sh&lt;/code&gt; 是不支持的，因此必须切换为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/bin/bash&lt;/code&gt; 或其他支持的 shell，如：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN /bin/bash -c &apos;source $HOME/.bashrc; echo $HOME&apos;&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;另外， &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exec&lt;/code&gt; 格式是使用 JSON 来解析的，因此命令参数要使用双引号 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN [&quot;x&quot;, &quot;xx&quot;, &quot;xxx&quot;]&lt;/code&gt;；并且，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exec&lt;/code&gt; 格式不会直接调用 shell，因此一些 shell 处理是不会执行的，例如参数替换，命令 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN [ &quot;echo&quot;, &quot;$HOME&quot; ]&lt;/code&gt; 是不会对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$HOME&lt;/code&gt; 进行替换的。若需要 shell 处理，则需要显式调用 shell，例如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN [ &quot;sh&quot;, &quot;-c&quot;, &quot;echo $HOME&quot; ]&lt;/code&gt;，因为指定了 shell 之后，是该 shell 来进行命令解析，而不是 Docker。&lt;/p&gt;

&lt;h4 id=&quot;env&quot;&gt;ENV&lt;/h4&gt;

&lt;p&gt;每一行 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ENV&lt;/code&gt; 都会创建一个中间层，就像 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RUN&lt;/code&gt; 一样，这意味着即使你在未来步骤 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unset&lt;/code&gt; 了该变量，这个变量依然会保留在前面那层，并且最终可以读到。你可以使用下面 Dockerfile 对其进行验证：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;FROM alpine
ENV ADMIN_USER=&quot;mark&quot;
RUN echo $ADMIN_USER &amp;gt; ./mark
RUN unset ADMIN_USER
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;为了避免这样的情况，我们可以在同一层新建、使用、销毁一个环境变量：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;FROM alpine
RUN export ADMIN_USER=&quot;mark&quot; \
    &amp;amp;&amp;amp; echo $ADMIN_USER &amp;gt; ./mark \
    &amp;amp;&amp;amp; unset ADMIN_USER
CMD sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;add&quot;&gt;ADD&lt;/h4&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ADD&lt;/code&gt; 用来拷贝文件、目录、甚至 URLs，并添加到镜像的文件系统中。该命令也有两种形式：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ADD [--chown=&amp;lt;user&amp;gt;:&amp;lt;group&amp;gt;] &amp;lt;src&amp;gt;... &amp;lt;dest&amp;gt;&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ADD [--chown=&amp;lt;user&amp;gt;:&amp;lt;group&amp;gt;] [&quot;&amp;lt;src&amp;gt;&quot;,... &quot;&amp;lt;dest&amp;gt;&quot;]&lt;/code&gt; ，这种格式通常用来拷贝文件名带空格的文件。&lt;/li&gt;
&lt;/ol&gt;

&lt;h2 id=&quot;2-使用非-root-身份运行-docker&quot;&gt;2. 使用非 root 身份运行 Docker&lt;/h2&gt;

&lt;p&gt;为什么需要使用非 root 身份运行 Docker？其实就本文章的目标而言，此举是非必要的，即，完全可以在 root 身份下直接进行，并把环境依赖安装在 root 环境中，这当然是没有问题的。但是，如果在未来某些场景中，如果用户直接以 root 身份访问生产环境的 Docker，那么有时候后果是难以设想的。&lt;/p&gt;

&lt;p&gt;并且，在许多场景中，我们需要为用户提供统一的访问者身份，并部署相应的环境依赖，满足访问者的部分需求。因此，我们有必要学习一下，如何为某一用户部署环境，并使用该用户的身份运行 Docker。&lt;/p&gt;

&lt;p&gt;此小节我们会简明介绍如何以非 root 身份运行 Docker。&lt;/p&gt;

&lt;p&gt;默认的话，Docker 会用 root 身份运行，用户 ID 为 0，这个可以使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;id -u &amp;lt;username&amp;gt;&lt;/code&gt; 来查看。&lt;/p&gt;

&lt;p&gt;我们可以使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--user username:usergroup&lt;/code&gt; 参数来指定用户身份运行。但是，如果直接使用用户名、组名的话，容器是找不到该用户名的，因为没有对应的 ID。因此，我们需要指定用户 ID 及组 ID。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# 添加组 yaoleiqi，并指定 ID
RUN groupadd -f -g 200 yaoleiqi

# 添加用户 yaoleiqi 并指定 home 目录，指定用户 ID、所属的组
RUN useradd -m -d /home/yaoleiqi -u 200 yaoleiqi -g yaoleiqi

# 将 yaoleiqi 添加到 sudo 组
RUN usermod -aG sudo yaoleiqi

# 更改用户 yaoleiqi 的密码
RUN echo &quot;yaoleiqi:yao12345&quot; | chpasswd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;完整语句如下：&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run -it --user 200:200 awesome_dev:latest&lt;/code&gt;&lt;/p&gt;

&lt;h2 id=&quot;3-编写我们的环境&quot;&gt;3. 编写我们的环境&lt;/h2&gt;

&lt;h4 id=&quot;31-构建-base-镜像&quot;&gt;3.1. 构建 base 镜像&lt;/h4&gt;

&lt;p&gt;base 镜像可以是我们其他镜像的基础，构建一些常用并且通用的环境。&lt;/p&gt;

&lt;p&gt;在本示例中，其实只做了两件事：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;安装环境依赖&lt;/li&gt;
  &lt;li&gt;创建用户、组并进行一些 home 目录、密码、权限的配置&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;FROM ubuntu:18.04
MAINTAINER yaoleiqi &quot;yaoleiqi@qq.com&quot;

# build requirements
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    ninja-build \
    gettext \
    libtool \
    libtool-bin \
    autoconf \
    automake \
    cmake \
    g++ \
    pkg-config \
    unzip \
    git \
    sudo \
    build-essential \
    checkinstall \
    wget \
    vim \
  &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

# make the &quot;en_US.UTF-8&quot; locale so myenv will be utf-8 enabled by default
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y locales \
  &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* \
  &amp;amp;&amp;amp; localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8
ENV LANG en_US.utf8

# build python3
# This hack is widely applied to avoid python printing issues in docker containers.
# See: https://github.com/Docker-Hub-frolvlad/docker-alpine-python3/pull/13
ENV PYTHONUNBUFFERED=1

RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    python3-pip \
    python-dev \
    python3-dev \
  &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/* \
  &amp;amp;&amp;amp; cd /usr/local/bin \
  &amp;amp;&amp;amp; sudo ln -s /usr/bin/python3 python \
  &amp;amp;&amp;amp; sudo pip3 install --upgrade pip

# I like python, so I install the latest version of python
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    software-properties-common -y \
  &amp;amp;&amp;amp; add-apt-repository ppa:deadsnakes/ppa -y \
  &amp;amp;&amp;amp; apt-get update &amp;amp;&amp;amp; apt-get install -y \
    python3.8 \
    python3.8-dev \
  &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

# install other dev tools
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y \
    tmux \
    zsh \
    neovim \
    curl \
    silversearcher-ag \
    python3-neovim \
  &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

# add group
RUN groupadd -f -g 200 yaoleiqi

# add user
RUN useradd -m -d /home/yaoleiqi -u 200 yaoleiqi -g yaoleiqi
RUN usermod -aG sudo yaoleiqi
RUN echo &quot;yaoleiqi:yao12345&quot; | chpasswd
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;32-构建-dev-环境&quot;&gt;3.2. 构建 dev 环境&lt;/h4&gt;

&lt;p&gt;dev 环境则进行一些定制化的配置，在本示例中，将构建特定用户的定制化环境依赖，即为用户 yaoleiqi 构建 vim 环境、virtualenv 环境等。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;FROM yaoleiqi/devbase:latest
MAINTAINER yaoleiqi &quot;yaoleiqi@qq.com&quot;

ENV HOME /home/yaoleiqi

# build virtual enviroment
RUN python3.8 -m pip install virtualenv \
  &amp;amp;&amp;amp; virtualenv -p `which python3.8` $HOME/myenv

# install oh-my-zsh
COPY install_zsh.sh /tmp/
RUN chmod +x /tmp/install_zsh.sh \
  &amp;amp;&amp;amp; sh /tmp/install_zsh.sh

# install tmux
RUN cd $HOME \
  &amp;amp;&amp;amp; git clone https://github.com/gpakosz/.tmux.git \
  &amp;amp;&amp;amp; ln -s -f .tmux/.tmux.conf \
  &amp;amp;&amp;amp; cp .tmux/.tmux.conf.local .

# install golang
RUN wget https://dl.google.com/go/go1.14.4.linux-amd64.tar.gz -O /tmp/go.tar.gz \
  &amp;amp;&amp;amp; tar -C /usr/local -xzf /tmp/go.tar.gz \
  &amp;amp;&amp;amp; mkdir $HOME/go \
  &amp;amp;&amp;amp; echo &quot;export PATH=$PATH:/usr/local/go/bin\n\
export GOROOT=/usr/local/go\n\
export GOPATH=$HOME/go\n&quot; &amp;gt;&amp;gt; $HOME/.profile

# install vim plugins
COPY vimrc.sh $HOME/.vimrc

# git config
COPY gitconfig $HOME/.gitconfig

# build vim plugins
# install vundle
RUN mkdir -p $HOME/.config/nvim/bundle \
  &amp;amp;&amp;amp; git clone https://github.com/VundleVim/Vundle.vim.git $HOME/.config/nvim/bundle/Vundle.vim \
  &amp;amp;&amp;amp; mkdir -p $HOME/.vim/bundle \
  &amp;amp;&amp;amp; git clone https://github.com/ycm-core/YouCompleteMe.git $HOME/.vim/bundle/YouCompleteMe \
  &amp;amp;&amp;amp; cd $HOME/.vim/bundle/YouCompleteMe \
  &amp;amp;&amp;amp; git submodule update --init --recursive \
  &amp;amp;&amp;amp; git clone https://github.com/morhetz/gruvbox.git $HOME/.vim/bundle/gruvbox

RUN /bin/bash -c &apos;source $HOME/.profile; \
  $HOME/myenv/bin/python $HOME/.vim/bundle/YouCompleteMe/install.py --go-completer&apos;

RUN runuser -l yaoleiqi -c &apos;vim +PluginInstall +qall &amp;gt; /dev/null&apos; \
  &amp;amp;&amp;amp; runuser -l yaoleiqi -c &apos;vim +GoInstallBinaries +qall &amp;gt; /dev/null&apos;

RUN chown -R yaoleiqi:yaoleiqi $HOME
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;4-运行我们的环境&quot;&gt;4. 运行我们的环境&lt;/h2&gt;

&lt;p&gt;运行环境的前提是，已经安装好 Docker。那么运行以下几个命令即可进入我们的开发环境：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;git clone https://github.com/SarKerson/myenv.git
docker build -t --no-cache yaoleiqi/devbase:latest . &amp;amp;&amp;amp; docker build  -t --no-cache awesome_dev:latest ./awesome_dev/
docker run -it --user 200:200 awesome_dev:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后，就可以放飞自我的开始码代码了！等等！在此之前我们先手动安装一下 vim 环境，由于 vimrc 已经配置好，只需要执行以下两步即可（中间会遇到）：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# 安装插件
runuser -l yaoleiqi -c &apos;vim +PluginInstall +qall &amp;gt; /dev/null&apos;
# optinal：安装 vim-go，如果你需要写 golang 代码，那么推荐使用
runuser -l yaoleiqi -c &apos;vim +GoInstallBinaries +qall &amp;gt; /dev/null
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;5-使用-volume-持久化容器中的文件&quot;&gt;5. 使用 Volume 持久化容器中的文件&lt;/h2&gt;

&lt;p&gt;根据官方文档介绍，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumes&lt;/code&gt; 是推荐的持久化手段，并且由 Docker 进行管理。这意味着数据在 mount 的同时，Docker 可以做一些其他工作，例如文件系统兼容（Linux 与 Windows 互通）、多个容器共享、文件加密等等，具体可以参考 https://docs.docker.com/storage/volumes/。&lt;/p&gt;

&lt;p&gt;并且，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumes&lt;/code&gt; 独立于容器的生命周期，是插拔式的存在，不会增加容器的大小。但是，如果你不需要持久化数据的话，最好还是使用 tmpfs 方式，因为 tmpfs 是直接内存操作，速度会比写入硬盘快得多。因此，我们也可以在容器里直接 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git clone&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;git push&lt;/code&gt; 来编辑我们的代码。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://docs.docker.com/storage/images/types-of-mounts-volume.png&quot; alt=&quot;volumes on the Docker host&quot; /&gt;&lt;/p&gt;

&lt;p&gt;但是，对于一些数据文件，例如运行结果或者配置文件，我们通常还是可以利用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumes&lt;/code&gt; 来持久化、或者多个容器共享的。具体使用方式是，在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run&lt;/code&gt; 时使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-v&lt;/code&gt; 标志，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker run -v /host/directory:/container/directory -other -options image_name command_to_run&lt;/code&gt;，这样则会将宿主机的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/host/directory&lt;/code&gt; 映射到容器中的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/container/directory&lt;/code&gt;，在容器中对该目录的任何修改，都会同步映射到宿主机上。&lt;/p&gt;

&lt;p&gt;另外，我们也可以直接创建 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volumes&lt;/code&gt; ，在任何时候挂载到任何容器上。例如，创建一个名为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my-vol&lt;/code&gt; 的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volume&lt;/code&gt;：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;docker volume create my-vol
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;然后列出所有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volume&lt;/code&gt;：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ docker volume ls
local               my-vol
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;查看一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volume&lt;/code&gt; 的具体信息：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ docker volume inspect my-vol
[
    {
        &quot;Driver&quot;: &quot;local&quot;,
        &quot;Labels&quot;: {},
        &quot;Mountpoint&quot;: &quot;/var/lib/docker/volumes/my-vol/_data&quot;,
        &quot;Name&quot;: &quot;my-vol&quot;,
        &quot;Options&quot;: {},
        &quot;Scope&quot;: &quot;local&quot;
    }
]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们还可以运行一个挂载 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;volume&lt;/code&gt; 的容器。下面命令将 宿主机的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;my-vol&lt;/code&gt; 挂载到容器的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/app&lt;/code&gt;。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$ docker run -d \
  --name devtest \
  --mount source=my-vol,target=/app \
  nginx:latest
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;6-references&quot;&gt;6. References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;https://medium.com/better-programming/running-a-container-with-a-non-root-user-e35830d1f42a&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://medium.com/redbubble/running-a-docker-container-as-a-non-root-user-7d2e00f8ee15&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://www.cyberciti.biz/open-source/command-line-hacks/linux-run-command-as-different-user/&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;https://docs.docker.com/storage/volumes/&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;
</description>
			<pubDate>Fri, 26 Jun 2020 00:00:00 +0000</pubDate>
			<link>https://sarkerson.github.io/2020/06/26/%E4%BD%BF%E7%94%A8-Docker-%E6%90%AD%E5%BB%BA%E8%87%AA%E5%B7%B1%E7%9A%84%E7%BC%96%E7%A8%8B%E7%8E%AF%E5%A2%83/</link>
			<guid isPermaLink="true">https://sarkerson.github.io/2020/06/26/%E4%BD%BF%E7%94%A8-Docker-%E6%90%AD%E5%BB%BA%E8%87%AA%E5%B7%B1%E7%9A%84%E7%BC%96%E7%A8%8B%E7%8E%AF%E5%A2%83/</guid>
        
			<category>docker</category>
        
        
		</item>
    
	</channel>
</rss>
