Wetts's blog

Stay Hungry, Stay Foolish.

0%

基本概念

  • 康威定律
    • 第一定律
      • Communication dictates the design.
        • 组织沟通方式会通过系统设计表达出来
      • 对于复杂的,需要协作完成的系统开发,沟通是必须要持续提升的问题。
      • 每个团队由5-10人组成(沟通成本 = n(n-1)/2),在团队内部进行频繁的、细粒度的沟通。对于团队外部,定义好接口,契约,只进行粗粒度的沟通。这样可以降低沟通成本,同时也符合高内聚,低耦合原则(代码和人员管理有些时候真是相通的)。
    • 第二定律
      • There is never enough time to do something right, but there is always enough time to do it over.
        • 时间再多一件事情也不可能做的完美,但总有时间做完一件事情
      • 这就是我们在用 kanban 管理迭代时几乎都有一列是 BAU(Business As Usual),其中会包括一些日常修复的 Bug Story。敏捷开发中将迭代引入,做到持续交付,快速验证,迅速反馈,持续改进。
    • 第三定律
      • There is a homomorphism from the linear graph of a system to the linear graph of its design organization.
        • 线型系统和线型组织架构间有潜在的异质同态特性
      • 大白话就是,你想要架构成为什么样,就将团队分成怎样的结构。比如前后端分离的团队,架构就是基于前后端分离。在基于微服务设计的团队里,一个很好的理念是自管理。团队内部对于自己所负责的模块高度负责,进行端对端的开发以及运维。
    • 第四定律
      • The structures of large systems tend to disintegrate during development, qualitatively more so than with small systems.
        • 大的系统组织总是比小系统更倾向于分解
      • 合久必分,分久必合,团队以及架构都是在不断优化的。一个团队随着人员的增加,沟通以及管理成本一定会增加。

项目架构

  • 单机架构
    • 特征是整个开发围绕着数据库进行设计和开发。
    • 存在的问题
      • 系统复杂:内部多个模块紧耦合,关联依赖复杂,牵一发而动全身。
      • 运维困难:变更或升级的影响分析困难,任何一个小修改都可能导致单体应用整体运行出现故障。
      • 无法扩展:无法拆分部署,出现性能瓶颈后往往只能够增加服务器或增加集群节点,但是 DB 问题难解决
    • 三层式的集中式架构
      • 采用面向对象的设计方法,业务逻辑分业务层、逻辑层、数据访问层,这种架构很容易某一层或者几层变得臃肿,扩展性较差, 另外摩尔定律失效, 单台机器性能有限。
  • SOA
    • SOA与微服务
    • 提出 MicroService 概念的 Martin Fowler 说过,“我们应该把 SOA 看作微服务的超集”,也就是说微服务是 SOA 的子集。
    • SOA 的探索大概始于 2000 年(概念产生可能更早一些),大家知道当初 ERP、CRM、OA 之类的信息系统都是一套套部署起来的,不同系统往往由不同的供应商分别开发的,技术差别也很大,各个系统孤零零的,企业于是有了应用集成和数据集成的需求,SOA 就出来了,各个系统对外提供粗粒度的服务供外部系统访问,所有的服务都集中在一个 ESB 上,曾经 SOA 和 SOA 治理是信息化领域的热门话题,然而这种集成方式开发代价大、通信效率低,且有单点故障的风险, 实际上在企业中并没有得到大规模应用。
    • ESB
      • ESB 就是一根管道,用来连接各个服务节点。ESB的存在是为了集成基于不同协议的不同服务,ESB 做了消息的转化、解释以及路由的工作,以此来让不同的服务互联互通。
      • 从名称就能知道,它的概念借鉴了计算机组成原理中的通信模型——总线,所有需要和外部系统通信的系统,统统接入 ESB,岂不是完美地兼容了现有的互相隔离的异构系统,可以利用现有的系统构建一个全新的松耦合的异构的分布式系统。
    • 相关问题
      • SOA、ESB、微服务的区别和关系
        • SOA 是一种理念,它的主要特性–面向服务的分布式计算,服务间松散耦合,支持服务的封装,服务注册和自动发现,以服务契约方式定义服务交互方式。但是,SOA 并没有定义出具体的实现方式,目前有两套 SOA 理念的实现方式:中心化和去中心化,这两套架构并没有优劣之分,还是要针对企业的根本诉求。
        • SOA 中心化的实现方式就是 ESB,ESB 的根本诉求是为了解决异构系统之间的连通性,通过协议转换、消息解析、消息路由把服务提供者的数据传送到服务消费者。所以,ESB 是中心化的,很重,有一定的逻辑,但它的确可以解决一些公用逻辑的问题。
        • SOA 去中心化的实现方式的根本诉求是扩展性,实现方式就是微服务。
  • 微服务
    • 实现应用之间的解耦,解决单体应用扩展性的问题。
    • 微服务存在的问题
      • 业务或者微服务的边界界定
    • 业务划分方式
      • 领域驱动建模(DDD)
        • DDD 不是一种架构,而是一种架构方法论,目的就是将复杂问题领域简单化,帮助我们设计出清晰的领域和边界,可以很好的实现技术架构的演进。
        • 战略设计、战术设计
          • 战略设计
            • 在某个领域,核心围绕上下文的设计
            • 主要关注上下文的划分、上下文映射的设计、通用语言的定义
            • 问题空间、解决空间
          • 战术设计
            • 核心关注上下文中的实体建模,定义值对象、实体等,更偏向开发细节
            • 战术设计的术语
              • 实体
                • 实体是指描述了领域中唯一的且可持续变化的抽象模型,通常建模时,名词用于给概念命名,形容词用于描述这些概念,而动词则表示可以完成的操作
              • 值对象
                • 描述了领域中的一件东西,将不同的相关属性组合成了一个概念整体,当度量和描述改变时,可以用另外一个值对象予以替换,属性判等、固定不变
              • 服务
                • 标识的是在领域对象之外的操作与行为,接受用户的请求和执行某些操作
              • 聚合
                • 实体和值对象会形成聚合,每个聚合一般是在一个事物中操作,一般都有持久化操作。聚合中,根实体的生命周期决定了聚合整体的生命周期
              • 工厂(Facotry)和仓库(Repository)
        • 领域服务、应用服务、实体行为
          • 职能、原则
            • 应用服务
              • 编排领域服务
              • 暴露系统的全部功能
              • 安全验证、持久化处理
              • 轻量级、不处理业务逻辑
              • 跨模块协调
              • DTO 转换、AOP、邮件短信、消息通知
            • 实体行为
              • 体现实体业务行为
              • 根实体:公开接口行为、保证不变条件
              • 负责协调实体和值对象按照完成业务逻辑
            • 领域服务
              • 组织业务逻辑(流程、策略、规则、完整性约束等)
              • 协调方案、非必要性
              • 协调领域对象的行为、无状态
              • 某个动作不适合放在聚合对象上时
              • 过度使用领域服务将导致贫血模型
        • 特点
          • 领域模型是对具有某个边界的领域的一个抽象,反映了领域内用户业务需求的本质;领域模型是有边界的,只反应了我们在领域内所关注的部分;
          • 领域模型只反映业务,和任何技术实现无关;领域模型不仅能反映领域中的一些实体概念,如货物,书本,应聘记录,地址,等;还能反映领域中的一些过程概念,如资金转账,等;
          • 领域模型确保了我们的软件的业务逻辑都在一个模型中,都在一个地方;这样对提高软件的可维护性,业务可理解性以及可重用性方面都有很好的帮助;
          • 领域模型能够帮助开发人员相对平滑地将领域知识转化为软件构造;
          • 领域模型贯穿软件分析、设计,以及开发的整个过程;领域专家、设计人员、开发人员通过领域模型进行交流,彼此共享知识与信息;因为大家面向的都是同一个模型,所以可以防止需求走样,可以让软件设计开发人员做出来的软件真正满足需求;
          • 要建立正确的领域模型并不简单,需要领域专家、设计、开发人员积极沟通共同努力,然后才能使大家对领域的认识不断深入,从而不断细化和完善领域模型;
          • 为了让领域模型看的见,我们需要用一些方法来表示它;图是表达领域模型最常用的方式,但不是唯一的表达方式,代码或文字描述也能表达领域模型;
          • 领域模型是整个软件的核心,是软件中最有价值和最具竞争力的部分;设计足够精良且符合业务需求的领域模型能够更快速的响应需求变化;
        • 可以通过三步来确定领域模型和微服务边界
          • DDD
          1. 在事件风暴中梳理业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出领域实体等领域对象。
          2. 根据领域实体之间的业务关联性,将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。在这个图里,聚合之间的边界是第一层边界,它们在同一个微服务实例中运行,这个边界是逻辑边界,所以用虚线表示。
          3. 根据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,形成领域模型。在这个图里,限界上下文之间的边界是第二层边界,这一层边界可能就是未来微服务的边界,不同限界上下文内的领域逻辑被隔离在不同的微服务实例中运行,物理上相互隔离,所以是物理边界,边界之间用实线来表示。
  • IaaS
    • 基础设施即服务(IaaS:Infrastructure as a Service)
    • 把计算基础(服务器、网络技术、存储和数据中心空间)作为一项服务提供给客户。它也包括提供操作系统和虚拟化技术、来管理资源。消费者通过 Internet 可以从完善的计算机基础设施获得服务。
    • 优点:相对其他几种服务,它的自由度、灵活度非常的高。客户可以自行安装自己喜欢的操作系统、方便自己的数据集、需要的软件等。所以,一切东西可以自行部署。我的理解是有点像学生时代去机房上网。
    • 缺点:它的维护成本比较高。使用它会导致 CPU、内存等等计算资源浪费。相关的人力资源和时间资源也会被浪费。相当于把资源分割成一个一个个性化的虚拟的电脑,它们之间互相独立。“土地”就只有这么多,分完了就没有了。而对于用户来说,必须要自行下载操作系统等等繁琐的操作。对于云端和用户来说,各种资源其实都浪费了。
  • Paas
    • 平台即服务(PaaS:Platform as a Service)
    • PaaS 实际上是指将软件研发的平台作为一种服务,供应商提供超过基础设施的服务,一个作为软件开发和运行环境的整套解决方案,即以 SaaS 的模式提交给用户。因此,PaaS 也是 SaaS 模式的一种应用。但是,PaaS 的出现可以加快 SaaS 的发展,尤其是加快 SaaS 应用的开发速度。
    • 优点:减少的搭建各种平台的损耗,为云端和用户节省了资源。
    • 缺点:相对 IaaS 来说,PaaS 的自由度和灵活度比较低,不太适合专业性比较高的 IT 技术从业人员。相当于范围被限定,在特定的范围做一些事情。我的理解有点像 QQ 远程控制自己的电脑处理事情。
  • Saas
    • 软件即服务(SaaS:Software as a Service)
    • 是一种交付模式,其中应用作为一项服务托管,通过 Internet 提供给用户;帮助客户更好地管理它们的 IT 项目和服务、确保它们 IT 应用的质量和性能,监控它们的在线业务。
    • 优点:方便快捷,资源利用可以非常优化。用户使用直接管理这些软件产生的数据就可以了。而使用的时候是模块化的,选择需要功能使用就行。多用户可以并行运行。
    • 缺点:软件多而且杂乱、安装复杂、使用复杂、运维复杂。用户如果不是批量采购的话购买价格昂贵。

      大数据

  • 大数据的特征(4V 特征)
    • 规模性(Volume)
      • 随着信息化技术的高速发展,数据开始爆发性增长。大数据中的数据不再以几个 GB 或几个 TB 为单位来衡量,而是以 PB(1千个T)、EB(1百万个T)或 ZB(10亿个T)为计量单位。
    • 高速性(Velocity)
      • 这是大数据区分于传统数据挖掘最显著的特征。大数据与海量数据的重要区别在两方面:一方面,大数据的数据规模更大;另一方面,大数据对处理数据的响应速度有更严格的要求。实时分析而非批量分析,数据输入、处理与丢弃立刻见效,几乎无延迟。数据的增长速度和处理速度是大数据高速性的重要体现。
    • 多样性(Variety)
      • 多样性主要体现在数据来源多、数据类型多和数据之间关联性强这三个方面。
    • 价值性(Value)
      • 尽管企业拥有大量数据,但是发挥价值的仅是其中非常小的部分。大数据背后潜藏的价值巨大。由于大数据中有价值的数据所占比例很小,而大数据真正的价值体现在从大量不相关的各种类型的数据中。挖掘出对未来趋势与模式预测分析有价值的数据,并通过机器学习方法、人工智能方法或数据挖掘方法深度分析,并运用于农业、金融、医疗等各个领域,以期创造更大的价值。

分层

  • net_1
  • net_2
  • 网络协议分层
  • 应用层
    • 应用层
      • DNS 域名解析协议
        • 域名解析协议是能够来将域名和 IP 地址相互映射,使人更方便地访问互联网的协议。
      • FTP 文件传输协议
        • FTP 协议是基于 TCP 的传输,FTP 采用双 TCP 连接方式,提供一种在服务器和客户机之间上传和下载文件的有效方式,支持授权与认证机制,提供目录列表功能。
      • SMTP 简单邮件传输协议
        • SMTP 简单邮件传输协议是一种提供可靠且有效的电子邮件传输的协议,它控制两个相互通信的 SMTP 进程交换信息。有以下三个阶段,连接建立、邮件传送、连接释放。
      • HTTP 超文本传输协议
        • HTTP 超文本传输协议是用于从万维网服务器传输超文本到本地浏览器的传送协议,它一个无状态的请求/响应协议,是因特网上应用最为广泛的一种网络传输协议,所有的 WWW 文件都必须遵守这个标准,HTTP 超文本传输协议基于 TCP/IP 通信协议来传递数据。
    • 表示层
    • 会话层
  • 传输层
    • 传输层协议为不同主机上运行的应用进程提供逻辑通信
    • 传输层则负责将数据可靠地传送到相应的端口(端到端),传输层提供了主机应用程序进程之间的端到端的服务。传输层利用网络层提供的服务,并通过传输层地址提供给高层用户传输数据的通信端口,使高层用户看到的只是在两个传输实体间的一条端到端的、可由用户控制和设定的、可靠的数据通路。
  • 网络层
    • 网络层协议为不同主机提供逻辑通信。
    • 网络层只是根据网络地址将源结点发出的数据包传送到目的结点(点到点),其主要任务是:通过路由选择算法,为报文或分组通过通信子网选择最适当的路径。该层控制数据链路层与传输层之间的信息转发,建立、维持和终止网络的连接。具体地说,数据链路层的数据在这一层被转换为数据包,然后通过路径选择、分段组合、顺序、进/出路由等控制,将信息从一个网络设备传送到另一个网络设备。
    • 路由器的主要功能
      • 路由选择、分组转发,掌握原理
    • 动态路由算法
      • 距离向量路由算法、链路状态路由算法
    • IP 地址
      • IP 地址是 IP 协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异
    • MAC 地址
      • MAC 是地址物理地址,用来定义网络设备的位置,在 OSI 模型中,第三层网络层负责 IP 地址,第二层数据链路层则负责 MAC 地址。
  • 网络接口层
    • 数据链路层
      • 可靠传输机制
        • 序列号、校验和、确认应答机制、超时重传、连接管理(三次握手四次挥手)、流量控制、拥塞控制
    • 物理层
      • 物理层的几种复用
        • 频分复用、时分复用、波分复用、码分复用

网络层协议

IP

  • IP 协议用于屏蔽下层物理网络的差异,为上层提供统一的 IP 数据报。
    • IP协议
  • IP 协议提供无连接的、不可靠的、尽力的数据报投递服务
    • 无连接的投递服务
      • 发送端可于任何时候自由发送数据,而接收端永远不知道自己会在何时从哪里接收到数据。每个 IP 数据报独立处理和传输,一台主机发出的数据报序列,可能会走不同的路径,甚至有可能其中的一部分数据报会在传输过程中丢失
    • 不可靠的投递服务
      • IP 协议本身不保证 IP 数据报投递的结果。在传输的过程中,IP 数据报可能会丢失、重复、延迟和乱序等,IP 协议不对内容作任何检测,也不将这些结果通知收发双方
        • IP 数据报的丢失,通过路由器发 ICMP 报文 告知;必要时,由高层实体(如 TCP)负责差错恢复动作
    • 尽力投递服务
      • 每个数据链路上会规定一个最大传输单元 MTU,如果 IP 数据报的长度超过 MTU,那么网络层就会把这些报文分割成一个一个的小组(分组)进行传送,以适应具体的传输网络
  • 报文
    • IP 数据报中含有收/发方的 IP 地址

ICMP

  • ICMP(Internet Control Message Protocol)Internet 控制报文协议。它是 TCP/IP 协议簇的一个子协议,用于在 IP 主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。
  • ICMP 使用 IP 的基本支持,就像它是一个更高级别的协议,但是,ICMP 实际上是 IP 的一个组成部分,必须由每个 IP 模块实现。
  • PING 命令是利用 ICMP 协议

SSL/TSL

  • SSL
    • SSL(Secure Socket Layer 安全套接层)以及其继承者 TSL(Transport Layer Security 传输层安全)是为了网络通信安全,提供安全及数据完整性的一种安全协议。TLS 与 SSL 在传输层对网络连接进行加密。
    • SSL 协议位于 TCP/IP 协议与各种应用层协议之间,为数据通讯提供安全支持。
    • SSL 协议可分为两层:
      • SSL 记录协议(SSL Record Protocol)
        • 它建立在可靠的传输协议(如 TCP)之上,为高层协议提供数据封装、压缩、加密等基本功能的支持。
      • SSL 握手协议(SSL Handshake Protocol)
        • 它建立在 SSL 记录协议之上,用于在实际的数据传输开始前,通讯双方进行身份认证、协商加密算法、交换加密密钥等。
    • SSL_应用
  • TSL
    • TLS(Transport Layer Security)传输层安全是 IETF 在 SSL3.0 基础上设计的协议,实际上相当于 SSL 的后续版本。
    • 结构
      • TSL_结构
      • TLS主要分为两层
        • 底层的是 TLS 记录协议,主要负责使用对称密码对消息进行加密。
        • 上层的是 TLS 握手协议,主要分为握手协议,密码规格变更协议和应用数据协议 4 个部分。
          • 握手协议负责在客户端和服务器端商定密码算法和共享密钥,包括证书认证,是 4 个协议中最最复杂的部分。
          • 密码规格变更协议负责向通信对象传达变更密码方式的信号
          • 警告协议负责在发生错误的时候将错误传达给对方
          • 应用数据协议负责将 TLS 承载的应用数据传达给通信对象的协议。

传输层协议

TCP

  • 报文
    • 报文头部
      • TCP报文头部
  • 流程
    • TCP
    • 在 TCP 中,有个 FLAGS 字段,这个字段有以下几个标识
      • SYN
        • 表示建立连接
      • FIN
        • 表示关闭连接
      • ACK
        • 表示响应
        • ACK 是可能与 SYN,FIN 等同时使用的
      • PSH
        • 表示有 DATA 数据传输
      • RST
        • 表示连接重置
      • URG
    • TCP 状态表
      • TCP状态转换图
      • CLOSED
        • 关闭状态,没有连接活动或正在进行
      • LISTEN
        • 监听状态,服务器正在等待连接进入
      • SYN RCVD
        • 收到一个连接请求,尚未确认
      • SYN SENT
        • 已经发出连接请求,等待确认
      • ESTABLISHED
        • 连接建立,正常数据传输状态
      • FIN WAIT 1
        • (主动关闭)已经发送关闭请求,等待确认
      • FIN WAIT 2
        • (主动关闭)收到对方关闭确认,等待对方关闭请求
      • TIME WAIT
        • 完成双向关闭,等待所有分组死掉
      • CLOSING
        • 双方同时尝试关闭,等待对方确认
      • CLOSE WAIT
        • (被动关闭)收到对方关闭请求,已经确认
      • LAST ACK
        • (被动关闭)等待最后一个关闭确认,并等待所有分组死掉
  • 客户端端口+服务端端口+客户端IP+服务端IP+传输协议组成的五元组可以明确的标识一条连接
  • 使用 TCP 的协议:FTP(文件传输协议)、Telnet(远程登录协议)、SMTP(简单邮件传输协议)、POP3(和 SMTP 相对,用于接收邮件)、HTTP 协议等。
  • 安全性
    • 初始化序列号 ISN(Initial Sequence Number)
      • 三次握手过程是建立 TCP 连接的第一步,所以这里的序列号叫初始序列号 ISN。在后续通信中的序列号都是基于 ISN 计算出来的。所以 ISN 是后续通信的基础,如果在后续的报文中检查序列号不匹配,这个报文将被认为是非法报文,做丢弃处理。
      • ISN 生成基本规则
        1. 递增,直到超过最大值,再从较小的值开始。ISN 如果不是递增的,就可能因为网络延迟导致 ISN 重复,引起后续通信错乱,连接失败。
        2. 随机,ISN 必须是不可预测的随机数,如果 ISN 可以预测,将会引起很多安全问题。
      • TCP_ISN
  • 拆包、封包
    • TCP 是个”流”协议,所谓流,就是没有界限的一串数据。大家可以想想河里的流水,是连成一片的,其间是没有分界线的。但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包。
    • 由于 TCP “流”的特性以及网络状况,在进行数据传输时会出现以下几种情况
      1. 先接收到 data1,然后接收到 data2
      2. 先接收到 data1 的部分数据,然后接收到 data2 余下的部分以及 data2 的全部
      3. 先接收到了 data1 的全部数据和 data2 的部分数据,然后接收到了 data2 的余下的数据
      4. 一次性接收到了 data1 和 data2 的全部数据
      • 2、3、4 的情况就是大家经常说的”粘包”,就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包。为了拆包就必须在发送端进行封包。
        • 封包
          • 封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了。
          • 包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义。根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。
  • 保活机制(keepAlive)
    • 保活机制是由一个保活计时器实现的。当计时器被激发,连接端将发送一个保活探测报文,另一端接收报文的同时会发送一个 ACK 作为响应。
    • 相关配置
      • 保活时间:默认 7200 秒(2 小时)
      • 保活时间间隔:默认 75 秒
      • 保活探测数:默认 9 次
    • 过程描述
      • 连接中启动保活功能的一端,在保活时间内连接处于非活动状态,则向对方发送一个保活探测报文,如果收到响应,则重置保活计时器,如果没有收到响应报文,则经过一个保活时间间隔后再次向对方发送一个保活探测报文,如果还没有收到响应报文,则继续,直到发送次数到达保活探测数,此时,对方主机将确认为不可到达,连接被中断。
    • TCP 保活功能工作过程中,开启该功能的一端会发现对方处于以下四种状态之一:
      • 对方主机仍在工作,并且可以到达。此时请求端将保活计时器重置。如果在计时器超时之前应用程序通过该连接传输数据,计时器再次被设定为保活时间值。
      • 对方主机已经崩溃,包括已经关闭或者正在重新启动。这时对方的 TCP 将不会响应。请求端不会接收到响应报文,并在经过保活时间间隔指定的时间后超时。超时前,请求端会持续发送探测报文,一共发送保活探测数指定次数的探测报文,如果请求端没有收到任何探测报文的响应,那么它将认为对方主机已经关闭,连接也将被断开。
      • 客户主机崩溃并且已重启。在这种情况下,请求端会收到一个对其保活探测报文的响应,但这个响应是一个重置报文段 RST,请求端将会断开连接。
      • 对方主机仍在工作,但是由于某些原因不能到达请求端(例如网络无法传输,而且可能使用 ICMP 通知也可能不通知对方这一事实)。这种情况与状态 2 相同,因为 TCP 不能区分状态 2 与状态 4,结果是都没有收到探测报文的响应。
    • 弊端
      • 在出现短暂的网络错误的时候,保活机制会使一个好的连接断开;
      • 保活机制会占用不必要的带宽;
    • 保活功能在默认情况下是关闭的。没有经过应用层的请求,Linux 系统不会提供保活功能。
    • 相关问题
      • TCP 连接时,一方如何知道另一方【异常断开连接】?
        • TCP 不是轮询的协议,否则 TCP 将占用大量网络带宽。可以说 TCP 属于事件触发的协议,对等方的异常断链只能在应用层通过 send() 函数来判断,所以业界通常的做法是定时 send HEARTBEAD。TCP 还有个套接字 Option,设置后每隔 2 小时如果没有数据交互的话协议会自动检测。
  • 确保可靠性的方式:
    • 校验和
      • 计算方式
        • 在数据传输的过程中,将发送的数据段都当做一个 16 位的整数。将这些整数加起来。并且前面的进位不能丢弃,补在后面,最后取反,得到校验和。
      • 发送方:在发送数据之前计算检验和,并进行校验和的填充。
      • 接收方:收到数据后,对数据以同样的方式进行计算,求出校验和,与发送方的进行比对。
      • TCP校验和
      • 如果接收方比对校验和与发送方不一致,那么数据一定传输有误。但是如果接收方比对校验和与发送方一致,数据不一定传输成功
    • 序列号、确认应答
      • 序列号
        • TCP 传输时将每个字节的数据都进行了编号,这就是序列号。
      • 确认应答
        • TCP 传输的过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送 ACK 报文。这个 ACK 报文当中带有对应的确认序列号,告诉发送方,接收到了哪些数据,下一次的数据从哪里发。
      • TCP确认应答与序列号
      • 序列号的作用不仅仅是应答的作用,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据。这也是TCP传输可靠性的保证之一。
    • 超时重传
      • 发送方没有介绍到响应的ACK报文原因可能有两点:
        • 数据在传输过程中由于网络原因等直接全体丢包,接收方根本没有接收到。
        • 接收方接收到了响应的数据,但是发送的ACK报文响应却由于网络原因丢包了。
      • 重传机制就是发送方在发送完数据后等待一个时间,时间到达没有接收到 ACK 报文,那么对刚才发送的数据进行重新发送。如果是刚才第一个原因,接收方收到二次重发的数据后,便进行 ACK 应答。如果是第二个原因,接收方发现接收的数据已存在(判断存在的根据就是序列号,所以上面说序列号还有去除重复数据的作用),那么直接丢弃,仍旧发送ACK应答。
        • 最大超时时间(也就是等待的时间)是动态计算的
          • 在 Linux 中(BSD Unix 和 Windows 下也是这样)超时以 500ms 为一个单位进行控制,每次判定超时重发的超时时间都是 500ms 的整数倍。重发一次后,仍未响应,那么等待 2*500ms 的时间后,再次重传。等待 4*500ms 的时间继续重传。以一个指数的形式增长。累计到一定的重传次数,TCP 就认为网络或者对端出现异常,强制关闭连接。
    • 连接管理
      • 握手、挥手
        • 三次握手
          • 过程描述
            • 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c)。此时客户端处于 SYN_SEND 状态。
              • 首部的同步位 SYN=1,初始序号 seq=x,SYN=1 的报文段不能携带数据,但要消耗掉一个序号。
            • 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。
              • 在确认报文段中 SYN=1,ACK=1,确认号 ack=x+1,初始序号 seq=y。
              • SYN-ACK 重传次数
                • 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。
                • 注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s…
            • 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
              • 确认报文段 ACK=1,确认号 ack=y+1,序号 seq=x+1(初始为 seq=x,第二个报文段所以要 +1),ACK 报文段可以携带数据,不携带数据则不消耗序号。
            • 发送第一个 SYN 的一端将执行主动打开(active open),接收这个 SYN 并发回下一个 SYN 的另一端执行被动打开(passive open)。
            • TCP_三次握手
        • 四次挥手
          • 过程描述
            • 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
              • 即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭 TCP 连接,进入 FIN_WAIT1(终止等待 1)状态,等待服务端的确认。
            • 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
              • 即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号 ack=u+1,序号 seq=v),服务端进入 CLOSE_WAIT(关闭等待)状态,此时的 TCP 处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入 FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
            • 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
              • 即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号 ack=u+1),服务端进入 LAST_ACK(最后确认)状态,等待客户端的确认。
            • 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
              • 即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入 TIME_WAIT(时间等待)状态。此时 TCP 未释放掉,需要经过时间等待计时器设置的时间 2MSL 后,客户端才进入 CLOSED 状态。
            • 收到一个 FIN 只意味着在这一方向上没有数据流动。客户端执行主动关闭并进入 TIME_WAIT 是正常的,服务端通常执行被动关闭,不会进入 TIME_WAIT 状态。
            • TCP_四次挥手
        • 相关问题
          • 为什么要三次握手?
            • 防止失效的连接请求报文段被服务端接收,从而产生错误。
              • 失效的连接请求:若客户端向服务端发送的连接请求丢失,客户端等待应答超时后就会再次发送连接请求,此时,上一个连接请求就是『失效的』。
            • 若建立连接只需两次握手,客户端并没有太大的变化,仍然需要获得服务端的应答后才进入 ESTABLISHED 状态,而服务端在收到连接请求后就进入 ESTABLISHED 状态。此时如果网络拥塞,客户端发送的连接请求迟迟到不了服务端,客户端便超时重发请求,如果服务端正确接收并确认应答,双方便开始通信,通信结束后释放连接。此时,如果那个失效的连接请求抵达了服务端,由于只有两次握手,服务端收到请求就会进入 ESTABLISHED 状态,等待发送数据或主动发送数据。但此时的客户端早已进入 CLOSED 状态,服务端将会一直等待下去,这样浪费服务端连接资源。
          • 什么是半连接队列?
            • 服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。
            • 当然还有一个全连接队列,就是已经完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
          • 为什么要四次挥手?
            • 试想一下,假如现在你是客户端你想断开跟 Server 的所有连接该怎么做?第一步,你自己先停止向 Server 端发送数据,并等待 Server 的回复。但事情还没有完,虽然你自身不往 Server 发送数据了,但是因为你们之前已经建立好平等的连接了,所以此时他也有主动权向你发送数据;故 Server 端还得终止主动向你发送数据,并等待你的确认。其实,说白了就是保证双方的一个合约的完整执行!
          • 为什么 TIME_WAIT 状态需要经过 2MSL(最大报文段生存时间)才能返回到 CLOSE 状态?
            • 为了保证 Server 能收到 Client 的确认应答。
            • 若 Client 发完确认应答后直接进入 CLOSED 状态,那么如果该应答丢失,Server 等待超时后就会重新发送连接释放请求,但此时 Client 已经关闭了,不会作出任何响应,因此 Server 永远无法正常关闭。
    • 流量控制与拥塞控制
      • 流量控制
        • 接收端在接收到数据后,对其进行处理。如果发送端的发送速度太快,导致接收端的结束缓冲区很快的填充满了。此时如果发送端仍旧发送数据,那么接下来发送的数据都会丢包,继而导致丢包的一系列连锁反应,超时重传呀什么的。而 TCP 根据接收端对数据的处理能力,决定发送端的发送速度,这个机制就是流量控制。
        • 在 TCP 协议的报头信息当中,有一个 16 位字段的窗口大小。在介绍这个窗口大小时我们知道,窗口大小的内容实际上是接收端接收数据缓冲区的剩余大小。这个数字越大,证明接收端接收缓冲区的剩余空间越大,网络的吞吐量越大。接收端会在确认应答发送 ACK 报文时,将自己的即时窗口大小填入,并跟随 ACK 报文一起发送过去。而发送方根据 ACK 报文里的窗口大小的值的改变进而改变自己的发送速度。如果接收到窗口大小的值为 0,那么发送方将停止发送数据。并定期的向接收端发送窗口探测数据段,让接收端把窗口大小告诉发送端。
        • TCP流量控制
        • 16 位的窗口大小最大能表示 65535 个字节(64K),但是 TCP 的窗口大小最大并不是 64K。在 TCP 首部中 40 个字节的选项中还包含了一个窗口扩大因子 M,实际的窗口大小就是 16 为窗口字段的值左移 M 位。每移一位,扩大一倍。
      • 拥塞控制
        • TCP 传输的过程中,发送端开始发送数据的时候,如果刚开始就发送大量的数据,那么就可能造成一些问题。网络可能在开始的时候就很拥堵,如果给网络中在扔出大量数据,那么这个拥堵就会加剧。拥堵的加剧就会产生大量的丢包,就对大量的超时重传,严重影响传输。
        • TCP 引入了慢启动的机制,在开始发送数据时,先发送少量的数据探路。探清当前的网络状态如何,再决定多大的速度进行传输。这时候就引入一个叫做拥塞窗口的概念。发送刚开始定义拥塞窗口为 1,每次收到 ACK 应答,拥塞窗口加 1。在发送数据之前,首先将拥塞窗口与接收端反馈的窗口大小比对,取较小的值作为实际发送的窗口。
        • 拥塞窗口的增长是指数级别的。慢启动的机制只是说明在开始的时候发送的少,发送的慢,但是增长的速度是非常快的。为了控制拥塞窗口的增长,不能使拥塞窗口单纯的加倍,设置一个拥塞窗口的阈值,当拥塞窗口大小超过阈值时,不能再按照指数来增长,而是线性的增长。在慢启动开始的时候,慢启动的阈值等于窗口的最大值,一旦造成网络拥塞,发生超时重传时,慢启动的阈值会为原来的一半(这里的原来指的是发生网络拥塞时拥塞窗口的大小),同时拥塞窗口重置为 1。
        • TCP拥塞控制
        • 操作步骤
          • 慢开始:最开始发送方的拥塞窗口为 1,由小到大逐渐增大发送窗口和拥塞窗口。每经过一个传输轮次,拥塞窗口 cwnd 加倍。当 cwnd 超过慢开始门限,则使用拥塞避免算法,避免 cwnd 增长过大。
          • 拥塞避免:每经过一个往返时间 RTT,cwnd 就增长1。另外在慢开始和拥塞避免的过程中,一旦发现网络拥塞,就把慢开始门限设为当前值的一半,并且重新设置 cwnd 为 1,重新慢启动。(乘法减小,加法增大)
          • 快重传:接收方每次收到一个失序的报文段后就立即发出重复确认,发送方只要连续收到三个重复确认就立即重传(尽早重传未被确认的报文段)。
          • 快恢复:当发送方连续收到了三个重复确认,就乘法减半(慢开始门限减半),将当前的拥塞窗口设置为慢开始门限,并且采用拥塞避免算法(连续收到了三个重复请求,说明当前网络可能没有拥塞)。
          • 采用慢开始和拥塞避免算法的时候
            • 一旦 cwnd > 慢开始门限,就采用拥塞避免算法,减慢增长速度
            • 一旦出现丢包的情况,就重新进行慢开始,减慢增长速度
          • 采用快恢复和快重传算法的时候
            • 一旦 cwnd > 慢开始门限,就采用拥塞避免算法,减慢增长速度
            • 一旦发送方连续收到了三个重复确认,就采用拥塞避免算法,减慢增长速度
      • 发送端实际可用的窗口:接收端通知窗口(流量控制中的发送窗口)和拥塞窗口中的较小者。
  • 相关问题
    • 滑动窗口的作用
      • 流量控制
        • 接收端窗口大小,代表接收端缓冲区还有多少大小,从而控制发送端发送大小,达到流量控制的目的。
      • 拥塞控制
        • 拥塞控制也就是考虑当前的网络环境,动态调整窗口大小,没有发生拥塞情况,则窗口增大,拥塞了窗口减小,如此往复,最终应该接近与接收端的窗口大小。
      • 提高传输效率
        • 在确认应答机制中,对每一个发送的数据段,都要给一个 ACK 确认应答,收到 ACK 后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差。而有了滑动窗口,通信双方就不用发送一个报文后,收到此报文的确认后再发送下一个报文,而是可以连续发送多个报文,只要别超过窗口大小限制。
          • 粘包
            • 发送端为了将多个发往接收端的包,更加高效的的发给接收端,于是采用了优化算法(Nagle算法),将多次间隔较小、数据量较小的数据,合并成一个数据量大的数据块,然后进行封包。
            • 产生原因
              • 发送方原因
              • 接收方原因
                • TCP 接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上,TCP 将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果 TCP 接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
            • TCP 本来就是基于字节流而不是消息包的协议,按长度解析包就行。

UDP

  • UDP 是无连接的,即发送数据之前不需要建立连接,减少了开销和发送数据之前的时延。UDP 使用尽最大努力交付,即不保证可靠交付,主机不需要维持复杂的连接状态表。UDP 面向报文,发送方的 UDP 对应用程序交下来的报文,在添加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。
  • Quic
    • Quic 全称 quick udp internet connection,“快速 UDP 互联网连接”,(和英文 quick 谐音,简称“快”)是由 Google 提出的使用 udp 进行多路并发传输的协议。
    • Quic 相比现在广泛应用的 http2+tcp+tls 协议有如下优势:
      • 减少了 TCP 三次握手及 TLS 握手时间;
      • 改进的拥塞控制;
      • 避免队头阻塞的多路复用;
      • 连接迁移;
      • 前向冗余纠错。
    • UDP_QUIC_网络层对比图
    • UDP_QUIC_通讯时间对比图
    • 需要 QUIC 的原因
      • 问题描述
        • 协议历史悠久导致中间设备僵化;
        • 依赖于操作系统的实现导致协议本身僵化;
        • 建立连接的握手延迟大;
        • 队头阻塞。

应用层协议

HTTP

  • HTTP(HyperText Transfer Protocol): 超文本传输协议。是互联网上应用最广泛的一种网络协议。所有 www 文件都必须遵守的一个标准,是以 ASCII 码传输,建立在 TCP/IP 协议之上的应用层规范。简单点说就是一种固定的通讯规则。
  • HTTP 状态码
    • 已定义范围 分类
      1XX 100-101 信息提示
      2XX 200-206 成功
      3XX 300-305 重定向
      4XX 400-415 客户端错误
      5XX 500-505 服务器错误
    • 常见状态码:
      • 200 OK。服务器成功处理了请求(这个是我们见到最多的)
      • 301/302 Moved Permanently(重定向)。请求的 URL 已移走。Response 中应该包含一个 Location URL, 说明资源现在所处的位置
      • 400 Bad Request(坏请求)。告诉客户端,它发送了一个错误的请求。
      • 404 Not Found。未找到资源
      • 500 Internal Server Error(内部服务器错误)。服务器遇到一个错误,使其无法为请求提供服务
  • 生命周期
    • HTTP 的生命周期通过 Request 来界定,也就是一个 Request 一个 Response,那么在 HTTP1.0 中,这次 HTTP 请求就结束了。
    • 在 HTTP1.1 中进行了改进,使得有一个 keep-alive,也就是说,在一个 HTTP 连接中,可以发送多个 Request,接收多个 Response。
  • 特点
    • 被动性:只能由客户端发起请求
  • 请求方式
    • 分类
      • POST
        • 浏览器先发送 header,服务器响应 100 continue,浏览器再发送 data,服务器响应 200 ok(返回数据)
      • GET
        • 请求过程
          • 浏览器会把 http header 和 data 一并发送出去,服务器响应 200(返回数据)
      • PUT
      • DELETE
    • GET 和 POST 的区别
      • get 参数通过 url 传递,post 放在 request body 中。
      • get 请求在 url 中传递的参数是有长度限制的,而 post 没有。
      • get 比 post 更不安全,因为参数直接暴露在 url 中,所以不能用来传递敏感信息。
      • get 请求只能进行 url 编码,而 post 支持多种编码方式。
      • get 请求会浏览器主动 cache。
      • get 请求参数会被完整保留在浏览历史记录里,而 post 中的参数不会被保留。
      • GET 和 POST 本质上就是 TCP 链接,并无差别。但是由于 HTTP 的规定和浏览器/服务器的限制,导致他们在应用过程中体现出一些不同。
      • GET 产生一个 TCP 数据包;POST 产生两个 TCP 数据包。
  • 发展历史
    • HTTP发展史
    • HTTP/0.9 版本
      • 这是最早定稿的HTTP版本,这个版本中它的内容非常地简单。
        • 首先它只有一个命令 GET。对应到现在的 GET 请求和 POST 请求,这些叫做 HTTP 的命令或者方法。
        • 它没有 HEADER 等描述数据的信息。因为这个时候的请求非常简单,它需要达到的目的也非常简单,没有那么多数据格式。
        • 服务器发送完内容之后,就关闭 TCP 连接。这里需要注意一点,这里的 TCP 连接和 http 请求是不一样的。http 请求和 TCP 连接不是一个概念。一个 http 请求通过 TCP 连接发送,而一个 TCP 连接里面可以发送很多个 http 请求(HTTP/0.9 不能这么做,但是 HTTP/1.1 可以这么做,而且在 HTTP/2 这方面会更大程度地优化,来提高 HTTP 协议传输的效率以及服务器的性能),所以一个 TCP 连接对应的是多个 http 请求,一个 http 请求肯定是在某一个 TCP 连接里面进行发送的。
    • HTTP/1.0 版本
      • 这个版本和 HTTP/1.1 差不多,在 HTTP/0.9 版本基础上进行了改进。
        • 增加了很多命令。比如:POST、PUT、HEADER 这些命令。
        • 增加了 status code 和 header 相关的内容。
          • status code 是用来描述服务器端处理某一个请求之后的状态的;
          • header 主要包含:请求和发送数据的描述以及对这部分数据进行操作的方法。
        • 增加了多字符集支持、多部分发送、权限、缓存等相关的内容。这些内容有利于更好地使用 http 请求去实现 WEB 服务。
    • HTTP/1.1 版本
      • 这个版本是在 HTTP/1.0 的基础上增加了一些功能来优化网络连接的过程。
        • 在这个版本支持了持久连接。在 HTTP/1.0 版本里面,一个 http 请求要发送就要先在客户端和服务器端之间创建一个 TCP 连接,创建完这个 TCP 连接之后,等服务器端返回完数据之后,这个 TCP 连接就关闭了。
        • 增加了 pipeline。可以在同一个 TCP 连接里面发送多个 http 请求,就是上面说的那样。但是在 HTTP/1.1 里面,虽然是可以在同一个 TCP 连接里面发送多个 http 请求,但是服务器端对于进来的请求,是要按照顺序进行数据返回的。
          • 因此,如果前一个请求等待时间非常长,而后一个请求处理得比较快。这个时候后一个请求不能先发送,而是要等第一个请求数据全部发送完成之后,才能进行发送,即是串行的。等待的这部分时间就体现出了与并行传输性能之间的差距 【这个在HTTP/2里面得到了优化。】
        • 增加了 HTTP 的头 host 和其他一些命令。其中比较重要的就是 host,有了 host 之后就可以在同一台服务器(物理服务器)上同时跑多个 web 服务。比如说一个 Node.js 的 web 服务,一个 Java 的 web 服务。
    • HTTP/2 版本
      • 所有数据都是以二进制进行传输的。在 HTTP/1.1 里面大部分的数据传输是通过字符串,所以数据的分片方式是不太一样的。在 HTTP/2 里面所有的数据都是以帧进行传输的。
      • 多路复用。同一个连接里面发送多个请求时,服务器端不再需要按照顺序来返回处理后的数据了。而是可以在返回第一个请求里面数据的时候,同时返回第二个请求里面的数据。这样的并行传输能够更大限度地提高 web 应用的传输效率。
      • 新增头信息压缩以及推送等功能,提高了传输效率。HTTP/2 其实主要就是改善了 HTTP/1.1 里面造成性能低下的一些问题。
        • 头信息的压缩
          • 在 HTTP/1.1 里面每一次发送请求和返回请求,很多 http 头都是必须要进行完整的发送和返回的,但是这一部分头信息里面有很多的内容比如说:Headers 字段、Content-Type、accept 等字段是以字符串的形式保存的。
          • 所以占用较大的带宽量。所以 HTTP/2 里面对头信息进行了压缩,可以有效地减少带宽使用
        • 推送的功能
          • 指的是 HTTP/2 之前,只能由客户端发送数据,服务器端返回数据。客户端是主动方,服务器端永远是被动方。在 HTTP/2 里面有了”推送”的概念,也就是说服务器端可以主动向客户端发起一些数据传输。
          • 例子
            • 一个 web 页面加载时会要求一些 html、css、js 等文件,css 和 js文件是以链接的形式在 html 文本里面显示的,只有通过浏览器解析了 html 里面的内容之后,才能根据链接里面包含的 URL 地址去请求对应的 css 和 js 文件。
            • 在 HTTP/2 之前,这个传输过程会包含顺序问题,需要先请求到 html 的文件,通过浏览器运行解析这个 html 文件之后,才能去发送 css 的请求和 js 的请求。
            • HTTP/2 中有了推送功能之后,在请求 html 的同时,服务器端可以主动把 html 里面所引用到的 css 和 js 文件推送到客户端,这样 html、css 和 js 的发送就是并行的而不是串行的,整体的传输效率和性能就提高了不少。
    • HTTP/3
      • 之前协议的问题
        • 虽然 HTTP/2 解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题,虽然这个问题并不是它本身造成的,而是底层支撑的 TCP 协议的问题。
          • 因为 HTTP/2 使用了多路复用,一般来说同一域名下只需要使用一个 TCP 连接。当这个连接中出现了丢包的情况,那就会导致 HTTP/2 的表现情况反倒不如 HTTP/1 了。
          • 因为在出现丢包的情况下,整个 TCP 都要开始等待重传,也就导致了后面的所有数据都被阻塞了。但是对于 HTTP/1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。
          • 那么可能就会有人考虑到去修改 TCP 协议,其实这已经是一件不可能完成的任务了。因为 TCP 存在的时间实在太长,已经充斥在各种设备中,并且这个协议是由操作系统实现的,更新起来不大现实。
          • 基于这个原因,Google 就更起炉灶搞了一个基于 UDP 协议的 QUIC 协议,并且使用在了 HTTP/3 上,当然 HTTP/3 之前名为 HTTP-over-QUIC,从这个名字中我们也可以发现,HTTP/3 最大的改造就是使用了 QUIC。
      • HTTP3 核心新功能
        • QUIC
          • QUIC 是基于 UDP 实现的,UDP 协议虽然效率很高,但是并不是那么的可靠。QUIC 虽然基于 UDP,但是在原本的基础上新增了很多功能,比如多路复用、0-RTT、使用 TLS1.3 加密、流量控制、有序交付、重传等等功能。
            • 多路复用
              • 虽然 HTTP/2 支持了多路复用,但是 TCP 协议终究是没有这个功能的。QUIC 原生就实现了这个功能,并且传输的单个数据流可以保证有序交付且不会影响其他的数据流,这样的技术就解决了之前 TCP 存在的问题。
              • 并且 QUIC 在移动端的表现也会比 TCP 好。因为 TCP 是基于 IP 和端口去识别连接的,这种方式在多变的移动端网络环境下是很脆弱的。但是 QUIC 是通过 ID 的方式去识别一个连接,不管你网络环境如何变化,只要 ID 不变,就能迅速重连上。
            • 0-RTT
              • 通过使用类似 TCP 快速打开的技术,缓存当前会话的上下文,在下次恢复会话的时候,只需要将之前的缓存传递给服务端验证通过就可以进行传输了。
            • 纠错机制
              • 假如说这次我要发送三个包,那么协议会算出这三个包的异或值并单独发出一个校验包,也就是总共发出了四个包。
              • 当出现其中的非校验包丢包的情况时,可以通过另外三个包计算出丢失的数据包的内容。
              • 当然这种技术只能使用在丢失一个包的情况下,如果出现丢失多个包就不能使用纠错机制了,只能使用重传的方式了。

HTTPS

  • 原理
    • 非对称加密+对称加密
    • 流程
      1. 某网站有用于非对称加密的公钥 A、私钥 A’。
      2. 浏览器向网站服务器请求,服务器把公钥 A 明文给传输浏览器。
      3. 浏览器随机生成一个用于对称加密的密钥 X,用公钥 A 加密后传给服务器。
      4. 服务器拿到后用私钥 A’ 解密得到密钥 X。
      5. 这样双方就都拥有密钥 X 了,且别人无法知道它。之后双方所有数据都通过密钥X加密解密即可。
    • 相关问题
      • 如何证明浏览器收到的公钥一定是该网站的公钥?
        • 利用数字证书
      • 为什么不用两组公钥私钥非对称加密方案?
        • 流程
          1. 某网站服务器拥有公钥 A 与对应的私钥 A’;浏览器拥有公钥 B 与对应的私钥 B’。
          2. 浏览器把公钥 B 明文传输给服务器。
          3. 服务器把公钥 A 明文给传输浏览器。
          4. 之后浏览器向服务器传输的内容都用公钥 A 加密,服务器收到后用私钥 A’ 解密。由于只有服务器拥有私钥 A’,所以能保证这条数据的安全。
          5. 同理,服务器向浏览器传输的内容都用公钥 B 加密,浏览器收到后用私钥 B’ 解密。同上也可以保证这条数据的安全。
        • 原因
          • 很重要的原因是非对称加密算法非常耗时,而对称加密快很多
          • 会有中间人攻击
            • 中间人攻击
              • 流程
                1. 某网站有用于非对称加密的公钥 A、私钥 A’。
                2. 浏览器向网站服务器请求,服务器把公钥 A 明文给传输浏览器。
                3. 中间人劫持到公钥 A,保存下来,把数据包中的公钥A替换成自己伪造的公钥 B(它当然也拥有公钥 B 对应的私钥 B’)。
                4. 浏览器生成一个用于对称加密的密钥 X,用公钥 B(浏览器无法得知公钥被替换了)加密后传给服务器。
                5. 中间人劫持后用私钥 B’ 解密得到密钥X,再用公钥 A 加密后传给服务器。
                6. 服务器拿到后用私钥 A’ 解密得到密钥 X。
  • 数字证书
    • 网站在使用 HTTPS 前,需要向 CA 机构申领一份数字证书,数字证书里含有证书持有者信息、公钥信息等。服务器把证书传输给浏览器,浏览器从证书里获取公钥就行了,证书就如身份证,证明“该公钥对应该网站”。
    • 数字签名
      • HTTPS_数字签名
      • 数字签名的制作过程
        1. CA 机构拥有非对称加密的私钥和公钥
        2. CA 机构对证书明文数据 T 进行 hash
        3. 对 hash 后的值用私钥加密,得到数字签名 S
      • 浏览器验证过程
        1. 拿到证书,得到明文 T,签名 S
        2. 用 CA 机构的公钥对 S 解密(由于是浏览器信任的机构,所以浏览器保有它的公钥),得到 S’
        3. 用证书里指明的 hash 算法对明文 T 进行 hash 得到 T’
        4. 显然通过以上步骤,T’ 应当等于 S‘,除非明文或签名被篡改。所以此时比较 S’ 是否等于 T’,等于则表明证书可信
      • 111
    • 相关问题
      • 如何放防止数字证书被篡改?
        • 我们把证书原本的内容生成一份“签名”,比对证书内容和签名是否一致就能判别是否被篡改。
      • 为什么制作数字签名时需要 hash 一次?
        • 最显然的是性能问题,前面我们已经说了非对称加密效率较差,证书信息一般较长,比较耗时。而 hash 后得到的是固定长度的信息(比如用 md5 算法 hash 后可以得到固定的 128 位的值),这样加解密就快很多。
      • 怎么证明 CA 机构的公钥是可信的?
        • 操作系统、浏览器本身会预装一些它们信任的根证书,如果其中会有 CA 机构的根证书,这样就可以拿到它对应的可信公钥了。
        • 实际上证书之间的认证也可以不止一层,可以 A 信任 B,B 信任 C,以此类推,我们把它叫做信任链或数字证书链。也就是一连串的数字证书,由根证书为起点,透过层层信任,使终端实体证书的持有者可以获得转授的信任,以证明身份。
        • 另外,不知你们是否遇到过网站访问不了、提示需安装证书的情况?这里安装的就是根证书。说明浏览器不认给这个网站颁发证书的机构,那么你就得手动下载安装该机构的根证书(风险自己承担)。安装后,你就有了它的公钥,就可以用它验证服务器发来的证书是否可信了。
      • 每次进行 HTTPS 请求时都必须在 SSL/TLS 层进行握手传输密钥吗?
        • 服务器会为每个浏览器(或客户端软件)维护一个 session ID,在 TLS 握手阶段传给浏览器,浏览器生成好密钥传给服务器后,服务器会把该密钥存到相应的 session ID 下,之后浏览器每次请求都会携带 session ID,服务器会根据 session ID 找到相应的密钥并进行解密加密操作,这样就不必要每次重新制作、传输密钥了!
  • HTTPS_握手

WebSocket

  • 特点
    • 建立在 TCP 协议之上,服务器端的实现比较容易。
    • 与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
    • 数据格式比较轻量,性能开销小,通信高效。
    • 可以发送文本,也可以发送二进制数据。
    • 没有同源限制,客户端可以与任意服务器通信。
    • 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL。
  • 对比技术
    • ajax 轮询
      • 让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。
    • long poll
      • 原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回 Response 给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

请求过程

  • 网页请求过程
    1. 对网址进行 DNS 域名解析,得到对应的 IP 地址
      • DNS 域名解析采用的是递归查询的方式,过程是,先去找 DNS 缓存->缓存找不到就去找根域名服务器->根域名又会去找下一级,这样递归查找之后,找到了,给我们的 web 浏览器
    2. 根据这个 IP,找到对应的服务器,发起 TCP 的三次握手
    3. 建立 TCP 连接后发起 HTTP 请求
    4. 服务器响应 HTTP 请求,浏览器得到 HTML 代码
    5. 关闭TCP连接
      • 一般情况下,一旦 Web 服务器向浏览器发送了请求的数据,它就要关闭 TCP 连接,但是如果浏览器或者服务器在其头信息加入了这行代码:Connection:keep-alive。TCP 连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。
    6. 浏览器解析 HTML 代码,并请求 HTML 代码中的资源(如 js、css、图片等)(先得到 HTML 代码,才能去找这些资源)
    7. 浏览器对页面进行渲染呈现给用户

DNS

  • DNS域名解析

网络编程

网络编程模型

  • Acceptor-Connector 模式
    • 这种模式是面向连接的 TCP/IP 协议。
    • 模式思想
      • 此模式只负责连接的建立,不管有多少连接上来,这个模式都能应对。
      • 至于连接建立之后如何通信,那是通信处理器的事情,与此模式不再有任何关系。
      • 资源的管理总是通过调用函数的返回值来做约定的处理。不用类型如果有特殊的资源管理需求,均可以覆盖父类的方法。
  • Asynchronous Completion Token 模式
    • ACT 就是应对应用程序异步调用服务操作,并处理相应的服务完成事件。
    • 例子
      • 比如,通常应用程序会有调用第三方服务的需求,一般是业务线程请求都到,需要第三方资源的时候,去同步的发起第三方请求,而为了提升应用性能,需要异步的方式发起请求,但异步请求的话,等数据到达之后,此时的我方应用程序的语境以及上下文信息已经发生了变化,你没办法去处理。
      • ACT 解决的就是这个问题,采用了一个 token 的方式记录异步发送前的信息,发送给接受方,接受方回复的时候再带上这个 token,此时就能恢复业务的调用场景。
  • Proactor 模式
    • Proactor 模型运用于异步 I/O 操作。
  • Reactor 模式
    • Reactor 模型用于同步 I/O。Reactor 模式是一种典型的事件驱动的编程模型。
    • Reactor 模型中定义的三种角色:
      • Reactor
        • 负责监听和分配事件,将 I/O 事件分派给对应的 Handler。新的事件包含连接建立就绪、读就绪、写就绪等。
      • Acceptor
        • 处理客户端新连接,并分派请求到处理器链中。
      • Handler
        • 将自身与事件绑定,执行非阻塞读/写任务,完成 channel 的读入,完成处理业务逻辑后,负责将结果写出 channel。可用资源池来管理。
    • 流程
      • Reactor 处理请求的流程:
        • 读取操作:
          1. 应用程序注册读就绪事件和相关联的事件处理器
          2. 事件分离器等待事件的发生
          3. 当发生读就绪事件的时候,事件分离器调用第一步注册的事件处理器

网络攻击

  • 常见攻击
  • 入侵检测
    • 通过对行为、安全日志或审计数据或其它网络上可以获得的信息进行操作,检测到对系统的闯入或闯入的企图
    • 分类
      • 基于主机的入侵检测系统
        • 在主机上装一个 Agent,然后通过 Agent 监视系统的状态和各种进程
      • 基于网络的入侵检测系统
        • 通过被动地监听网络上传输的原始流量,对获取的网络数据进行处理,从中提取有用的信息,再通过与已知攻击特征相匹配或与正常网络行为原型相比较来识别攻击事件。此类检测系统不依赖操作系统作为检测资源,可应用于不同的操作系统平台;配置简单,不需要任何特殊的审计和登录机制;可检测协议攻击、特定环境的攻击等多种攻击。但它只能监视经过本网段的活动,无法得到主机系统的实时状态,精确度较差
      • 分布式 IDS
    • 按引擎检测机制分类
      • 基于签名检测的 IDS
        • 根据已知的签名进行检测,这种方法能有效识别签名库中已有的攻击,但无法识别未知攻击和已知攻击的变种。
      • 基于异常行为检测的 IDS
        • 通过学习网络流量行为来对流量进行分类,可以检测未知的攻击。
        • 再分类
          • 基于机器学习的入侵检测技术
            • 数理统计(Statistical):通过检查用户或系统的正常行为和异常行为来创建统计模型,统计模型可以用来识别新的攻击。常用的统计方法有主成分分析、卡方分布、高斯混合分布。
            • 支持向量机(Support Vector Machine,SVM):支持向量机是一种在数据样本有限的情况下检测入侵事件的有效方法。向量机的目标是以最合适的方式用一个特征向量来区分两种类型数据。它们有很多应用领域,如人物识别、声音识别等,是机器学习中的经典模型。
            • 数据挖掘(Data Mining):数据挖掘是从采集的海量数据中提取大量的信息,通过分析用户与数据之间的关联关系来提取关键规则,是用户行为分析的常用方法。
            • 基于规则集(Rule-Based):由安全研究人员分析网络中的攻击流量,提取关键规则,从而在这基础上降低数据维度后再对入侵行为进行检测。该方法在一定程度上可以减少检测计算量,提高检测效率。
            • 人工神经网络(Artificial Neural Network,ANN):人工神经网络是一种智能的信息处理模型,它模拟人类大脑对信息进行加工、存储和处理。神经网络通过学习获得知识,并将学到的知识存储在连接点的权重中。该模型具有学习性和自适应性,并且可以识别未知入侵。
          • 基于深度学习的入侵检测技术
    • 工具
      • IDS-snort
    • 数据集
      • KDD Cup99
      • NSL-KDD
      • UNSW-NB15
      • CIC-IDS 2017
  • 态势感知
    • 网络安全态势感知系统通常是集合了防病毒软件、防火墙、入侵监测系统、安全审计系统等多个数据信息系统,将这些系统整合起来,对目前的整个网络情况进行评估,以及预测未来的变化趋势。
    • 网络安全态势感知系统主要分为四个部分
      • 数据采集
        • 就是对当前整个网络状态进行数据提取,包括网站安全日志、漏洞数据库、恶意代码数据库等多个数据进行统筹整理,一般各个厂家都会有自己对应的信息数据库。
      • 特征提取
        • 通过第一步收集了大量的数据之后,从这些数据中提取有用的数据进行相应的预处理工作,为后面接下来的工作做好数据准备。数据采集和特征提取都是整个网络安全态势感知系统的最底层,数据准备工作。
      • 态势评估
        • 态势评估主要是通过对关联事件进行数据融合处理,从时间、空间、协议等多个方面进行关联识别。简单来说,就是结合数据信息、对当前的时间进行危险评估、判断危险等级。
      • 安全预警
        • 在通过了上面的几个步骤提取了大量的网络状态数据之后,系统就会根据指定的标准对目前的网络状态以及未来的网络状态进行评估和预测,进而给出相应的分析报告和安全状态预警处理。

其他

  • 路由器和交换机
    • 路由器
      • DHCP
        • DHCP 是动态主机设置协议的简称
        • 主要有两个用途
          • 用于内部网或网络服务供应商自动分配IP地址;
          • 给用户用于内部网管理员作为对所有计算机作中央管理的手段。
    • 区别
      • 工作层次不同
        • 交换机主要工作在数据链路层(第二层)
        • 路由器工作在网络层(第三层)
      • 转发依据不同
        • 交换机转发所依据的对象时:MAC 地址。(物理地址)
        • 路由转发所依据的对象是:IP 地址。(网络地址)
      • 主要功能不同
        • 交换机主要用于组建局域网。
        • 路由主要功能是将由交换机组好的局域网相互连接起来,或者接入Interne。
        • 交换机能做的,路由都能做。
        • 交换机不能分割广播域,路由可以。
        • 路由还可以提供防火墙的功能。
        • 路由配置比交换机复杂。
  • 网闸和防火墙
    • 网闸
      • 网闸(GAP)全称安全隔离网闸。安全隔离网闸是一种由带有多种控制功能专用硬件在电路上切断网络之间的链路层连接,并能够在网络间进行安全适度的应用数据交换的网络安全设备。
      • 基本原理
        • 切断网络之间的通用协议连接;将数据包进行分解或重组为静态数据;对静态数据进行安全审查,包括网络协议检查和代码扫描等;确认后的安全数据流入内部单元;内部用户通过严格的身份认证机制获取所需数据。
      • 安全隔离与信息交换系统 SGAP 一般由三部分构成
        • 内网处理单元
        • 外网处理单元
        • 专用隔离硬件交换单元
    • 防火墙
      • 防火墙是由一些软、硬件组合而成的网络访问控制器,它根据一定的安全规则来控制流过防火墙的网络包,如禁止或转发、能够屏蔽被保护网络内部的信息、拓扑结构和运行状况,从而起到网络安全屏障的作用。
      • 防火墙安全策略
        • 白名单策略:只允许符合安全规则的包通过防火墙
        • 黑名单策略:禁止与安全规则相冲突的包通过防火墙
      • 主要功能
        • 过滤非安全网络访问
        • 限制网络访问
        • 网络访问审计
        • 网络带宽控制
        • 协同防御
      • 防火墙类型与实现技术
        • 包过滤
          • 是在 IP 层实现的防火墙技术,根据包的源 IP 地址、目的 IP 地址、源端口、目的端口及包传递方向等包头信息判断是否允许包通过,对用户透明。基于包过滤技术的防火墙简称为包过滤型防火墙(Packet Filter)
          • 优点:低负载、高通过率、对用户透明
          • 缺点:不能在用户级别过滤,不能识别地址伪造,容易被绕过
        • 状态检查技术
          • 利用 TCP 会话和 UDP “伪”会话的状态信息进行网络访问控制。建立并维护一张会话表,当有符合已定义安全策略的 TCP 连接或 UDP 流时,防火墙会创建会话项,依据状态表项检查,与会话相关联的包才能通过
          • 主要步骤
            1. 接收到的数据包
            2. 检查数据包有效性,若无效,则丢弃并审计
            3. 查找会话表,若找到,进一步检查数据包的序列号和会话状态,如有效,则进行地址转换和路由,转发该数据包;否则,丢弃并审计
            4. 当会话表中没有新到的数据包信息时,查找策略表,若符合,则增加会话条目,进行地址转换和路由,转发数据包了否则,丢弃并审计
        • 应用服务代理
          • 代替受保护网络的主机向外部网发送服务请求,并将外部服务请求响应的结果返回给受保护网络的主机。由代理服务程序和身份验证服务程序构成,能够提供应用级别网络安全访问控制,如 FTP 代理、Telnet 代理、HTTP 代理
          • 优点
            • 不允许外部主机直接访问内部主机
            • 支持多种用户认证方案
            • 可以分析数据包内部的应用命令
            • 可以提供详细的审计记录
          • 缺点
            • 速度比包过滤慢
            • 对用户不透明
            • 与特定的应用协议相关联
        • 网络地址转换技术
          • NAT(Network Address Translation):本质:解决 IPv4 公开地址不足问题。安全方面:能透明地对所有内部地址做转换,使外部网络无法了解内部网络的内部结构,从而提高内部网络的安全性。
          • 实现方式
            • 静态 NAT(static NAT):内部网络中的每个主机都被永久映射成外部网络中的某个合法的地址
            • NAT 池(pooled NAT):在外部网络中配置合法的地址集,采用动态分配的方法映射到内部网络
            • 端口 NAT(PAT):内部地址映射到外部网络的一个 IP 地址的不同端口上
        • WAF
          • 用于保护 Web 服务器和 Web 应用的网络安全机制
          • 常见功能
            • 允许/禁止 HTTP 请求类型
            • HTTP 协议头各个字段长度限制
            • 后缀名过滤
            • URL 内容关键字过滤
            • Web 服务器返回内容过滤
          • 抵御典型攻击
            • SQL 注入
            • XSS 跨站脚本攻击
            • Web 应用扫描
            • Webshell
            • Cookie 注入攻击
            • CSRF 攻击
          • 开源框架
            • ModSecurity
            • WebKnight
            • Shadow Daemon
        • 数据库防火墙技术
          • 一种用于保护数据库服务器的网络安全机制
          • 技术原理
            • 协议深度分析:“源地址、目标地址、源端口、目标端口、SQL 语句”
            • 虚拟补丁:创建安全屏障层,监控所有数据库活动
        • 工控防火墙技术
          • 用于保护工业设备及系统的网络安全机制
          • 技术原理
            • 工控协议深度分析(Modbus TCP、IEC 61850、OPC、Ethernet/IP、DNP3)
        • 下一代防火墙技术(NGFW)
          • 应用识别和管控。不依赖端口,通过数据包深度内容分析,实现应用层协议与应用的识别和控制。
          • 入侵防护(IPS)。
          • 数据防泄漏。对传输的文件和内容进行识别和过滤
          • 恶意代码防护。基于信誉的恶意检测技术。
          • URL 分类与过滤。构建 URL 分类库
          • 带宽管理与 QoS 优化。
          • 加密通信分析。对 SSL、SSH 等加密的网络流量进行监控分析
    • 区别
      • 功能项 防火墙 安全隔离网闸
        产品定位 在保障互联互通的前提下,尽可能保障安全。 在保证高度安全的前提下,尽可能实现互联互通。
        设计思想 网络访问控制设备。防火墙部署于网络边界,在保证双方网络访问连接的同时,根据策略对数据报文进行访问控制,并在不影响设备性能的前提下进行内容过滤。 网络隔离交换设备。模拟人工拷盘实现数据信息在两个网络之间交换。对于网络间通过网闸实现数据同步的应用,网闸主动监控并读取所隔离的两个网络中的服务器数据从而实现数据同步,而非服务器主动发起连接请求;对于网络间的客户端和服务器之间通过网闸访问控制的应用,网闸的一个主机系统中断访问连接,另一主机系统重新建立连接,两主机系统中在应用层进行数据报文重组,重在进行数据内容的检查。
        硬件结构设计 防火墙为单主机结构,报文在同一个主机系统上经过安全检测后,根据策略转发。 安全隔离网闸基于“2+1”的体系结构,即由两个主机系统和一个隔离交换硬件组成,隔离交换硬件为专有硬件,不受主机系统控制。数据信息流经网闸时是串行流经三个系统的。
        操作系统设计 防火墙一般采用单一的专用操作系统。 安全隔离网闸的两个主机系统各自有专用操作系统,相互独立。
        协议处理程度 不同类型的防火墙,可能分别或综合采用分组包过滤、状态包过滤、NAT、应用层内容检查等安全技术,工作在OSI协议栈的第三至七层,通过匹配安全策略规则,依据IP头、TCP头信息、应用层明文信息,对进出防火墙的会话进行过滤。 所有到达安全隔离网闸外网的会话都被中断原有的TCP/IP连接,隔离设备将所有的协议剥离,将原始的数据写入存储介质。根据不同的应用,可能有必要对数据进行完整性和安全性检查,如防病毒和恶意代码等。对所有数据在应用层进行协议还原的基础上,以专有协议格式进行数据摆渡,综合了访问控制、内容过滤、病毒查杀等技术,具备全面的安全防护功能。
        安全机制 采用包过滤、代理服务等安全机制。 在GAP技术的基础上,综合了访问控制、内容过滤、病毒查杀等技术,具有全面的安全防护功能。
        抵御基于操作系统漏洞攻击行为 防火墙通过防止对内扫描等设置,可以部分防止黑客发现主机的操作系统漏洞。但无法阻止黑客通过防火墙允许的策略利用漏洞进行攻击。 安全隔离网闸的双主机之间是物理阻断的,无连接的,因此,黑客不可能扫描内部网络的所有主机的操作系统漏洞,无法攻击内部,包括安全隔离网闸的内部主机系统。
        抵御基于TCP/IP漏洞的攻击 防火墙需要制定严格的访问控制策略对连接进行检查以抵御TCP/IP漏洞攻击,只能对大部分已知TCP/IP攻击实施阻断。 由于安全隔离网闸的主机系统把TCP/IP协议头全部剥离,以原始数据方式在两主机系统间进行“摆渡”,网闸的接受请求的主机系统与请求主机之间建立会话,因此,对于目前所有的如源地址欺骗、伪造TCP序列号、SYN攻击等TCP/IP漏洞攻击是完全阻断的。
        抵御木马将数据外泄 防火墙部署时,一般对于内部网络向外部的访问控制是全部开放的,因此内部主机上的木马会很容易将数据外泄。并且黑客也容易通过木马主动建立的对外连接实现对内主机的远程控制。 安全隔离网闸对于每个应用都是在应用层进行处理,并且策略需按照应用逐个下达,同时对于目的地址也要唯一性指定,因此内部主机上的木马是无法实现将数据外泄的。并且木马主动发起的对外连接也将直接被隔离设备切断。
        抵御基于文件的病毒传播 防火墙可以根据应用层访问控制策略对经过防火墙的文件进行检查,或根据对文件类型的控制,只允许低级文件格式,如无病毒的文本格式内容穿过防火墙。等方式来抵御病毒传播。 安全隔离网闸在理论上是完全可以防止基于文件的攻击,如病毒等。病毒一般依附在高级文件格式上,低级文件格式则不会有病毒,因此进行文件“摆渡”的时候,可以限制文件的类型,如只有文本文件才可以通过“摆渡”,这样就不会有病毒。另外一种方式,是剥离重组方式。剥离高级格式,就消除了病毒的载体,重组后的文件,不会再有病毒。这种方式会导致效率的下降,一些潜在的危险的格式可能会被禁止。
        抵御DoS/DDoS攻击 防火墙通过SYN代理或SYN网关等技术,可以较好的抵御现有的各种DoS攻击类型。但对于大规模DDoS攻击方式,还没有有效的防护手段。 安全隔离网闸自身特有的无连接特性,能够很好的防止DoS或DDoS攻击穿过隔离设备攻击服务器。但也不能抵御针对安全隔离设备本身的DDoS攻击。
        管理安全性 通过网络接口远程管理。但如攻击者获得了管理权限,可以通过远程调整防火墙的安全策略,从而达到攻击目的。 内外网主机系统分别有独立于网络接口的专用管理接口,同时对于运行的安全策略需要在两个系统分别下达,并通过统一的任务号进行对应。以此达到高安全。
        可管理性 管理配置有一定复杂性。 个人认为管理配置不比防火墙简单。
        遭攻击后果 被攻破的防火墙只是个简单的路由器,将危及内网安全。 即使系统的外网处理单元瘫痪,网络攻击也无法触及内网处理单元。
        与其它安全设备联动性 目前防火墙基本都可以与IDS设备联动。 可结合防火墙、IDS、VPN等安全设备运行,形成综合网络安全防护平台

分布式

基础理论

  • ACID
    • 原子性(Atomicity)
      • 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
    • 一致性(Consistency)
      • 一致性是指事务执行前后,数据处于一种合法的状态,这种状态是语义上的而不是语法上的。
        • 那什么是合法的数据状态呢?
          • 这个状态是满足预定的约束就叫做合法的状态,再通俗一点,这状态是由你自己来定义的。满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!
    • 隔离性(Isolation)
      • 事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
    • 持久性(Durability)
      • 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
  • CAP
    • CAP 原则又称 CAP 定理,指的是在一个分布式系统中,Consistency(一致性)、Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
      • 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
      • 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
      • 分区容忍性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在 C 和 A 之间做出选择。
    • “三选二”定律
      • CAP 要解释成,当 P 发生的时候,A 和 C 只能而选一。
      • 例子
        • A 服务器 B 服务器同步数据,现在 A、B 之间网络断掉了,那么现在发来 A 一个写入请求,但是 B 却没有相关的请求,显然,如果 A 不写,保持一致性,那么我们就失去了 A 的服务,但是如果 A 写了,跟 B 的数据就不一致了,我们自然就丧失了一致性。
  • BASE 理论
    • BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent(最终一致性)三个短语的简写,BASE 是对 CAP 中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于 CAP 定理逐步演化而来的,其核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。
      • 基本可用
        • 基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。
        • 但请注意,这绝不等价于系统不可用,以下两个就是“基本可用”的典型例子。
          • 响应时间上的损失:正常情况下,一个在线搜索引擎需要 0.5 秒内返回给用户相应的查询结果,但由于出现异常(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了 1~2 秒。
          • 功能上的损失:正常情况下,在一个电子商务网站上进行购物,消费者几乎能够顺利地完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
      • 软状态
        • 弱状态也称为软状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据听不的过程存在延时。
      • 最终一致性
        • 最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
        • 在实际工程实践中,最终一致性分为 5 种:
          • 因果一致性(Causal consistency)
            • 如果节点 A 在更新完某个数据后通知了节点 B,那么节点 B 之后对该数据的访问和修改都是基于 A 更新后的值。于此同时,和节点 A 无因果关系的节点 C 的数据访问则没有这样的限制。
          • 读己之所写(Read your writes)
            • 节点 A 更新一个数据后,它自身总是能访问到自身更新过的最新值,而不会看到旧值。其实也算一种因果一致性。
          • 会话一致性(Session consistency)
            • 会话一致性将对系统数据的访问过程框定在了一个会话当中:系统能保证在同一个有效的会话中实现“读己之所写”的一致性,也就是说,执行更新操作之后,客户端能够在同一个会话中始终读取到该数据项的最新值。
          • 单调读一致性(Monotonic read consistency)
            • 如果一个节点从系统中读取出一个数据项的某个值后,那么系统对于该节点后续的任何数据访问都不应该返回更旧的值。
          • 单调写一致性(Monotonic write consistency)
            • 一个系统要能够保证来自同一个节点的写操作被顺序的执行。

分布式相关问题及算法

  • 问题
    • 拜占庭将军问题
      • 拜占庭将军问题是 Leslie Lamport(2013 年的图灵讲得住)用来为描述分布式系统一致性问题(Distributed Consensus)在论文中抽象出来一个著名的例子。
      • 例子
        • 拜占庭帝国想要进攻一个强大的敌人,为此派出了 10 支军队去包围这个敌人。这个敌人虽不比拜占庭帝国,但也足以抵御 5 支常规拜占庭军队的同时袭击。这10支军队在分开的包围状态下同时攻击。他们任一支军队单独进攻都毫无胜算,除非有至少 6 支军队(一半以上)同时袭击才能攻下敌国。他们分散在敌国的四周,依靠通信兵骑马相互通信来协商进攻意向及进攻时间。困扰这些将军的问题是,他们不确定他们中是否有叛徒,叛徒可能擅自变更进攻意向或者进攻时间。在这种状态下,拜占庭将军们才能保证有多于6支军队在同一时间一起发起进攻,从而赢取战斗?
        • 拜占庭将军问题中并不去考虑通信兵是否会被截获或无法传达信息等问题,即消息传递的信道绝无问题。Lamport 已经证明了在消息可能丢失的不可靠信道上试图通过消息传递的方式达到一致性是不可能的。所以,在研究拜占庭将军问题的时候,已经假定了信道是没有问题的。

        • 问题分析
          • 先看在没有叛徒情况下,假如一个将军 A 提一个进攻提议(如:明日下午 1 点进攻,你愿意加入吗?)由通信兵通信分别告诉其他的将军,如果幸运中的幸运,他收到了其他6位将军以上的同意,发起进攻。如果不幸,其他的将军也在此时发出不同的进攻提议(如:明日下午 2 点、3 点进攻,你愿意加入吗?),由于时间上的差异,不同的将军收到(并认可)的进攻提议可能是不一样的,这是可能出现 A 提议有 3 个支持者,B 提议有 4 个支持者,C 提议有 2 个支持者等等。
          • 再加一点复杂性,在有叛徒情况下,一个叛徒会向不同的将军发出不同的进攻提议(通知 A 明日下午 1 点进攻, 通知B明日下午 2 点进攻等等),一个叛徒也会可能同意多个进攻提议(即同意下午 1 点进攻又同意下午 2 点进攻)。
            • 叛徒发送前后不一致的进攻提议,被称为“拜占庭错误”,而能够处理拜占庭错误的这种容错性称为「Byzantine fault tolerance」,简称为 BFT。
  • 算法
    • 一致性算法
      • 一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。
      • 具体算法
        • 强一致性
          • 说明:保证系统改变提交以后立即改变集群的状态。
          • 模型:
            • Paxos
              • 概念介绍
                • Proposal
                  • 提案,即分布式系统的修改请求,可以表示为[提案编号N,提案内容value]
                • Client
                  • 用户,类似社会民众,负责提出建议
                • Proposer
                  • 议员,类似基层人大代表,负责帮 Client 上交提案
                • Acceptor
                  • 投票者,类似全国人大代表,负责为提案投票,不同意比自己以前接收过的提案编号要小的提案,其他提案都同意,例如 A 以前给 N 号提案表决过,那么再收到小于等于 N 号的提案时就直接拒绝了
                • Learner
                  • 提案接受者,类似记录被通过提案的记录员,负责记录提案
              • Basic Paxos 算法
                • 步骤
                  1. Proposer 准备一个 N 号提案
                  2. Proposer 询问 Acceptor 中的多数派是否接收过 N 号的提案,如果都没有进入下一步,否则本提案不被考虑
                  3. Acceptor 开始表决,Acceptor 无条件同意从未接收过的 N 号提案,达到多数派同意后,进入下一步
                  4. Learner 记录提案
                  • BasicPaxos算法
                • 节点故障
                  • 若 Proposer 故障,没关系,再从集群中选出 Proposer 即可
                  • 若 Acceptor 故障,表决时能达到多数派也没问题
                • 潜在问题
                  • 活锁
                    • 假设系统有多个 Proposer,他们不断向 Acceptor 发出提案,还没等到上一个提案达到多数派下一个提案又来了,就会导致 Acceptor 放弃当前提案转向处理下一个提案,于是所有提案都别想通过了。
              • Multi Paxos 算法
                • 根据 Basic Paxos 的改进:整个系统只有一个 Proposer,称之为 Leader。
                • 步骤
                  1. 若集群中没有 Leader,则在集群中选出一个节点并声明它为第 M 任 Leader。
                  2. 集群的 Acceptor 只表决最新的 Leader 发出的最新的提案
                  3. 其他步骤和 Basic Paxos 相同
                  • MultiPaxos算法
                • 算法优化
                  • Multi Paxos 角色过多,对于计算机集群而言,可以将 Proposer、Acceptor 和 Learner 三者身份集中在一个节点上,此时只需要从集群中选出 Proposer,其他节点都是 Acceptor 和 Learner,这就是接下来要讨论的 Raft 算法。
            • Raft(muti-paxos)
              • 说明:Paxos 算法不容易实现,Raft 算法是对 Paxos 算法的简化和改进
              • 概念介绍
                • Leader
                  • 总统节点,负责发出提案
                • Follower
                  • 追随者节点,负责同意 Leader 发出的提案
                • Candidate
                  • 候选人,负责争夺 Leader
                • Raft_概念介绍
              • 步骤:Raft 算法将一致性问题分解为两个的子问题,Leader 选举和状态复制
                • Leader 选举
                  • 时间被分为很多连续的随机长度的 term,term 有唯一的 id。每个 term 一开始就进行选主
                  • 流程
                    1. 每个 Follower 都持有一个定时器
                      • Raft_Leader选举_1
                    2. 当定时器时间到了而集群中仍然没有 Leader,【批注:将自己维护的 current_term_id 加 1】Follower 将声明自己是 Candidate 并参与 Leader 选举,同时将消息发给其他节点来争取他们的投票 【批注:发送 RequestVoteRPC 消息(带上 current_term_id)】,若其他节点长时间没有响应 Candidate 将重新发送选举信息
                      • Raft_Leader选举_2
                    3. 集群中其他节点将给 Candidate 投票
                      • Raft_Leader选举_3
                    4. 获得多数派支持的 Candidate 将成为第 M 任 Leader(M 任是最新的任期)
                      • Raft_Leader选举_4
                    5. 在任期内的 Leader 会不断发送心跳给其他节点证明自己还活着,其他节点受到心跳以后就清空自己的计时器并回复 Leader 的心跳。这个机制保证其他节点不会在 Leader 任期内参加 Leader 选举。
                      • Raft_Leader选举_5
                    6. 当 Leader 节点出现故障而导致 Leader 失联,没有接收到心跳的 Follower 节点将准备成为 Candidate 进入下一轮 Leader 选举
                      • Raft_Leader选举_6
                    7. 若出现两个 Candidate 同时选举并获得了相同的票数,那么这两个 Candidate 将随机推迟一段时间后再向其他节点发出投票请求,这保证了再次发送投票请求以后不冲突
                      • Raft_Leader选举_7
                  • 过程会有三种结果
                    • 自己被选成了主
                      • 当收到了 majority 的投票后,状态切成 Leader,并且定期给其它的所有 server 发心跳消息(不带 log 的 AppendEntriesRPC)以告诉对方自己是 current_term_id 所标识的 term 的 leader。每个 term 最多只有一个 leader,term id 作为 logical clock,在每个 RPC 消息中都会带上,用于检测过期的消息。当一个 server 收到的 RPC 消息中的 rpc_term_id 比本地的 current_term_id 更大时,就更新 current_term_id 为 rpc_term_id,并且如果当前 state 为 leader 或者 candidate 时,将自己的状态切成 follower。如果 rpc_term_id 比本地的 current_term_id 更小,则拒绝这个 RPC 消息。
                    • 别人成为了主
                      • 当 Candidator 在等待投票的过程中,收到了大于或者等于本地的 current_term_id 的声明对方是 leader 的 AppendEntriesRPC 时,则将自己的 state 切成 follower,并且更新本地的 current_term_id。
                    • 没有选出主
                      • 当投票被瓜分,没有任何一个 candidate 收到了 majority 的 vote 时,没有 leader 被选出。这种情况下,每个 candidate 等待的投票的过程就超时了,接着 candidates 都会将本地的 current_term_id 再加 1,发起 RequestVoteRPC 进行新一轮的 leader election。
                  • 投票策略
                    • 每个节点只会给每个 term 投一票,具体的是否同意和后续的 Safety 有关。
                      • Safety
                        • 哪些 follower 有资格成为 leader?
                          • Raft 保证被选为新 leader 的节点拥有所有已提交的 log entry,这与 ViewStamped Replication 不同,后者不需要这个保证,而是通过其他机制从 follower 拉取自己没有的提交的日志记录

                          • 这个保证是在 RequestVoteRPC 阶段做的,candidate 在发送 RequestVoteRPC 时,会带上自己的最后一条日志记录的 term_id 和 index,其他节点收到消息时,如果发现自己的日志比 RPC 请求中携带的更新,拒绝投票。日志比较的原则是,如果本地的最后一条 log entry 的 term id 更大,则更新,如果 term id 一样大,则日志更多的更大(index 更大)。
                        • 哪些日志记录被认为是 commited?
                          • leader 正在 replicate 当前 term(即 term 2)的日志记录给其它 Follower,一旦 leader 确认了这条 log entry 被 majority 写盘了,这条 log entry 就被认为是 committed。
                          • leader 正在 replicate 更早的 term 的 log entry 给其它 follower。
                    • 当投票被瓜分后,所有的 candidate 同时超时,然后有可能进入新一轮的票数被瓜分,为了避免这个问题,Raft 采用一种很简单的方法:每个 Candidate 的 election timeout 从 150ms-300ms 之间随机取,那么第一个超时的 Candidate 就可以发起新一轮的 leader election,带着最大的 term_id 给其它所有 server 发送 RequestVoteRPC 消息,从而自己成为 leader,然后给他们发送心跳消息以告诉他们自己是主。
                • 状态复制
                  1. Leader 负责接收来自 Client 的提案请求(红色提案表示未确认)
                    • Raft_状态复制_1
                  2. 提案内容将包含在 Leader 发出的下一个心跳中
                    • Raft_状态复制_2
                  3. Follower 接收到心跳以后回复 Leader 的心跳
                    • Raft_状态复制_3
                  4. Leader 接收到多数派 Follower 的回复以后确认提案并写入自己的存储空间中并回复 Client
                    • Raft_状态复制_4
                  5. Leader 通知 Follower 节点确认提案并写入自己的存储空间,随后所有的节点都拥有相同的数据
                    • Raft_状态复制_5
                  6. 若集群中出现网络异常,导致集群被分割,将出现多个 Leader
                    • Raft_状态复制_6
                  7. 被分割出的非多数派集群将无法达到共识,即脑裂,如图中的 A、B 节点将无法确认提案
                    • Raft_状态复制_7
                    • Raft_状态复制_8
                  8. 当集群再次连通时,将只听从最新任期 Leader 的指挥,旧 Leader 将退化为 Follower,如图中 B 节点的 Leader(任期 1)需要听从 D 节点的 Leader(任期 2)的指挥,此时集群重新达到一致性状态
                    • Raft_状态复制_9
                    • Raft_状态复制_10
            • ZAB(muti-paxos)
              • 说明:ZAB 也是对 Multi Paxos 算法的改进,大部分和 Raft 相同
              • 和 Raft 算法的主要区别:
                • 对于 Leader 的任期,Raft 叫做 term,而 ZAB 叫做 epoch
                • 在状态复制的过程中,Raft 的心跳从 Leader 向 Follower 发送,而 ZAB 则相反。
        • 弱一致性
          • 说明:也叫最终一致性,系统不保证改变提交以后立即改变集群的状态,但是随着时间的推移最终状态是一致的。
          • 模型:
            • DNS 系统
            • Gossip 协议
              • 说明:Gossip 算法每个节点都是对等的,即没有角色之分。Gossip 算法中的每个节点都会将数据改动告诉其他节点(类似传八卦)。有话说得好:”最多通过六个人你就能认识全世界任何一个陌生人”,因此数据改动的消息很快就会传遍整个集群。
              • 步骤:
                1. 集群启动,如下图所示(这里设置集群有20个节点)
                  • Gossip算法_步骤_1
                2. 某节点收到数据改动,并将改动传播给其他 4 个节点,传播路径表示为较粗的 4 条线
                  • Gossip算法_步骤_2
                  • Gossip算法_步骤_3
                3. 收到数据改动的节点重复上面的过程直到所有的节点都被感染
      • 应用举例
        • Google 的 Chubby 分布式锁服务,采用了 Paxos 算法
        • etcd 分布式键值数据库,采用了 Raft 算法
        • ZooKeeper 分布式应用协调服务,Chubby 的开源实现,采用 ZAB 算法

分布式 ID

  • 雪花算法
    • 组成部分(64bit)
      1. 第一位 占用 1bit,其值始终是 0,没有实际作用。
      2. 时间戳 占用 41bit,精确到毫秒,总共可以容纳约 69 年的时间。
      3. 工作机器 id 占用 10bit,其中高位 5bit 是数据中心 ID,低位 5bit 是工作节点 ID,做多可以容纳 1024 个节点。
      4. 序列号占用 12bit,每个节点每毫秒 0 开始不断累加,最多可以累加到 4095,一共可以产生 4096 个 ID。
    • 可能出现的问题
      • 时钟回拨
        • 如果时间回拨时间较短,比如配置 5ms 以内,那么可以直接等待一定的时间,让机器的时间追上来。
        • 如果时间的回拨时间较长,我们不能接受这么长的阻塞等待,那么又有两个策略:
          1. 直接拒绝,抛出异常,打日志,通知 RD 时钟回滚。
          2. 利用扩展位,上面我们讨论过不同业务场景位数可能用不到那么多,那么我们可以把扩展位数利用起来了,比如当这个时间回拨比较长的时候,我们可以不需要等待,直接在扩展位加 1、2 位的扩展位允许我们有 3 次大的时钟回拨,一般来说就够了,如果其超过三次我们还是选择抛出异常,打日志。

分布式锁

  • 基于 MySQL 的分布式锁
  • 基于 Redis 的分布式锁
  • 基于 Zookeeper 的分布式锁

分布式事务

  • 2PC
    • 2PC(Two-phase commit protocol),中文叫二阶段提交。 二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段。
    • 具体流程
      1. 准备阶段协调者会给各参与者发送准备命令,你可以把准备命令理解成除了提交事务之外啥事都做完了。
      2. 同步等待所有资源的响应之后就进入第二阶段即提交阶段(注意提交阶段不一定是提交事务,也可能是回滚事务)。
        1. 假如在第一阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交事务命令,然后等待所有事务都提交成功之后,返回事务执行成功。
          • 分布式事务_2PC_成功
        2. 假如在第一阶段有一个参与者返回失败,那么协调者就会向所有参与者发送回滚事务的请求,即分布式事务执行失败。
          • 分布式事务_2PC_失败
      3. 如果第二阶段提交失败
        1. 第一种是第二阶段执行的是回滚事务操作,那么答案是不断重试,直到所有参与者都回滚了,不然那些在第一阶段准备成功的参与者会一直阻塞着。
        2. 第二种是第二阶段执行的是提交事务操作,那么答案也是不断重试,因为有可能一些参与者的事务已经提交成功了,这个时候只有一条路,就是头铁往前冲,不断的重试,直到提交成功,到最后真的不行只能人工介入处理。
    • 首先 2PC 是一个同步阻塞协议,
      • 第一阶段协调者会等待所有参与者响应才会进行下一步操作,当然第一阶段的协调者有超时机制,假设因为网络原因没有收到某参与者的响应或某参与者挂了,那么超时后就会判断事务失败,向所有参与者发送回滚命令。
      • 在第二阶段协调者的没法超时,因为按照我们上面分析只能不断重试!
    • 协调者故障分析
      • 协调者是一个单点,存在单点故障问题。
        • 假设协调者在发送准备命令之前挂了,还行等于事务还没开始。
        • 假设协调者在发送准备命令之后挂了,这就不太行了,有些参与者等于都执行了处于事务资源锁定的状态。不仅事务执行不下去,还会因为锁定了一些公共资源而阻塞系统其它操作。
        • 假设协调者在发送回滚事务命令之前挂了,那么事务也是执行不下去,且在第一阶段那些准备成功参与者都阻塞着。
        • 假设协调者在发送回滚事务命令之后挂了,这个还行,至少命令发出去了,很大的概率都会回滚成功,资源都会释放。但是如果出现网络分区问题,某些参与者将因为收不到命令而阻塞着。
        • 假设协调者在发送提交事务命令之前挂了,这下是所有资源都阻塞着。
        • 假设协调者在发送提交事务命令之后挂了,这个还行,也是至少命令发出去了,很大概率都会提交成功,然后释放资源,但是如果出现网络分区问题某些参与者将因为收不到命令而阻塞着。
      • 协调者故障,通过选举得到新协调者
        • 如果处于第一阶段,其实影响不大都回滚好了,在第一阶段事务肯定还没提交。
        • 如果处于第二阶段,假设参与者都没挂,此时新协调者可以向所有参与者确认它们自身情况来推断下一步的操作。
        • 假设有个别参与者挂了!比如协调者发送了回滚命令,此时第一个参与者收到了并执行,然后协调者和第一个参与者都挂了。
          • 此时其他参与者都没收到请求,然后新协调者来了,它询问其他参与者都说OK,但它不知道挂了的那个参与者到底O不OK,所以它傻了。
          • 问题其实就出在每个参与者自身的状态只有自己和协调者知道,因此新协调者无法通过在场的参与者的状态推断出挂了的参与者是什么情况。
        • 虽然协议上没说,不过在实现的时候我们可以灵活的让协调者将自己发过的请求在哪个地方记一下,也就是日志记录
        • 但是就算协调者知道自己该发提交请求,那么在参与者也一起挂了的情况下没用,因为你不知道参与者在挂之前有没有提交事务。
          • 如果参与者在挂之前事务提交成功,新协调者确定存活着的参与者都没问题,那肯定得向其他参与者发送提交事务命令才能保证数据一致。
          • 如果参与者在挂之前事务还未提交成功,参与者恢复了之后数据是回滚的,此时协调者必须是向其他参与者发送回滚事务命令才能保持事务的一致。
        • 所以说极端情况下还是无法避免数据不一致问题。
  • 3PC
    • 3PC 的出现是为了解决 2PC 的一些问题,相比于 2PC 它在参与者中也引入了超时机制,并且新增了一个阶段使得参与者可以利用这一个阶段统一各自的状态。
    • 3PC 包含了三个阶段,分别是准备阶段、预提交阶段和提交阶段,对应的英文就是:CanCommit、PreCommit 和 DoCommit。
      • 看起来是把 2PC 的提交阶段变成了预提交阶段和提交阶段,但是 3PC 的准备阶段协调者只是询问参与者的自身状况。
      • 分布式事务_3PC_流程图
      • 不管哪一个阶段有参与者返回失败都会宣布事务失败,这和 2PC 是一样的(当然到最后的提交阶段和 2PC 一样只要是提交请求就只能不断重试)。
    • 3PC 的阶段相对 2PC 的变更有什么影响
      • 首先准备阶段的变更成不会直接执行事务,而是会先去询问此时的参与者是否有条件接这个事务,因此不会一来就干活直接锁资源,使得在某些资源不可用的情况下所有参与者都阻塞着。
      • 而预提交阶段的引入起到了一个统一状态的作用,它像一道栅栏,表明在预提交阶段前所有参与者其实还未都回应,在预处理阶段表明所有参与者都已经回应了。
        • 假如你是一位参与者,你知道自己进入了预提交状态那你就可以推断出来其他参与者也都进入了预提交状态。
      • 但是多引入一个阶段也多一个交互,因此性能会差一些,而且绝大部分的情况下资源应该都是可用的,这样等于每次明知可用执行还得询问一次。
    • 超时机制
      • 如果是等待提交命令超时,那么参与者就会提交事务了,因为都到了这一阶段了大概率是提交的,如果是等待预提交命令超时,那该干啥就干啥了,反正本来啥也没干。
      • 数据不一致
        • 比如在等待提交命令时候超时了,参与者默认执行的是提交事务操作,但是有可能执行的是回滚操作,这样一来数据就不一致了。
    • 2PC 和 3PC 都不能保证数据100%一致,因此一般都需要有定时扫描补偿机制。
  • TCC
    • 2PC 和 3PC 都是数据库层面的,而 TCC 是业务层面的分布式事务,分布式事务不仅仅包括数据库的操作,还包括发送短信等,这时候 TCC 就派上用场了!
    • TCC 指的是Try - Confirm - Cancel。
      • Try 指的是预留,即资源的预留和锁定,注意是预留。
      • Confirm 指的是确认操作,这一步其实就是真正的执行了。
      • Cancel 指的是撤销操作,可以理解为把预留阶段的动作撤销了。
    • TCC 模型还有个事务管理者的角色,用来记录 TCC 全局事务状态并提交或者回滚事务。
    • 流程
      • 分布式事务_TCC_流程图
    • 特点
      • TCC 对业务的侵入较大和业务紧耦合,需要根据特定的场景和业务逻辑来设计相应的操作。
      • 撤销和确认操作的执行可能需要重试,因此还需要保证操作的幂等。
  • 本地消息表
    • 例子
      • 分布式事务_本地消息表

分布式调度

  • 发展史
    • 分布式调度_发展史
    1. 第一阶段
      • 单线程调度,在 Java1.5 之前,基于线程的等待(sleep 或 wait)机制定时执行,需要开发者实现调度逻辑,单个线程(Thread)处理单个任务有些浪费,但是一个线程(Timer)处理多个任务容易因为某个任务繁忙导致其他任务阻塞。
    2. 第二阶段
      • 线程池调度,在 Java1.5 开始提供 ScheduledExecutorService 调度线程池,调度线程池支持固定的延时和固定间隔模式,对于需要在某天或者某月的时间点执行就不大方便,需要计算时间间隔,转换成启动延时和固定间隔,处理起来比较麻烦。
    3. 第三阶段
      • Spring 任务调度,Spring 简化了任务调度,通过 @Scheduled 注解支持将某个 Bean 的方法定时执行,除了支持固定延时和固定间隔模式外,还支持 cron 表达式,使得定时任务的开发变得极其简单。
    4. 第四阶段
      • Quartz 任务调度,在任务服务集群部署下,Quartz 通过数据库锁,实现任务的调度并发控制,避免同一个任务同时执行的情况。Quartz 通过 Scheduler 提供了任务调度 API,开发可以基于此开发自己的任务调度管理平台。
    5. 第五阶段
      • 分布式任务平台,提供一个统一的平台,无需再去做和调度相关的开发,业务系统只需要实现具体的任务逻辑,自动注册到任务调度平台,在上面进行相关的配置就完成了定时任务的开发。

接口设计

  • 遇到的问题

    • 高并发下如何保证接口的幂等性?

      • 幂等性定义
        • 幂等性就是同一个操作执行多次,产生的效果一样。如 http 的 get 请求、数据库的 select 请求就是幂等的。
        • 如提交订单、扣款等接口都要保证幂等性,不然会造成重复创建订单、重复扣款
      • 解决方法
        • 前端保证幂等性的方法
          • 按钮只能点击一次
            • 用户点击按钮后将按钮置灰,或者显示 loading 状态
          • RPG 模式
            • 即 Post-Redirect-Get,当客户提交表单后,去执行一个客户端的重定向,转到提交成功页面。避免用户按 F5 刷新导致的重复提交,也能消除按浏览器后退键导致的重复提交问题。
        • 后端保证幂等性的方法
          • 使用唯一索引
            • 对业务唯一的字段加上唯一索引,这样当数据重复时,插入数据库会抛异常
            • 接口幂等性_唯一索引
          • 状态机幂等
            • 如果业务上需要修改订单状态,例如订单状态有待支付,支付中,支付成功,支付失败。设计时最好只支持状态的单向改变。这样在更新的时候就可以加上条件,多次调用也只会执行一次。例如想把订单状态更新为支持成功,则之前的状态必须为支付中。
            • 接口幂等性_状态机
          • 悲观锁
            • 接口幂等性_悲观锁
          • 乐观锁
            • 步骤
              1. 查询数据获得版本号
              2. 通过版本号去更新,版本号匹配则更新,版本号不匹配则不更新
            • 接口幂等性_乐观锁
          • 防重表
            • 增加一个防重表,业务唯一的 id 作为唯一索引,如订单号,当想针对订单做一系列操作时,可以向防重表中插入一条记录,插入成功,执行后续操作,插入失败,则不执行后续操作。本质上可以看成是基于 MySQL 实现的分布式锁。根据业务场景决定执行成功后,是否删除防重表中对应的数据。
            • 接口幂等性_防重表
          • select+insert
            • 先查询一下有没有符合要求的数据,如果没有再执行插入。没有并发的系统中可以保证幂等性,高并发下不要用这种方法,也会造成数据的重复插入。我一般做消息幂等的时候就是先 select,有数据直接返回,没有数据加分布式锁进行 insert 操作。
          • 分布式锁
            • 执行方法时,先根据业务唯一的 id 获取分布式锁,获取成功,则执行,失败则不执行。分布式锁可以基于 Redis、zookeeper、MySQL 来实现。
            • 接口幂等性_分布式锁
          • 全局唯一号
            • 通过 source(来源)+ seq(序列号)来判断请求是否重复,重复则直接返回请求重复提交,否则执行。如当多个三方系统调用服务的时候,就可以采用这种方式。
          • 获取 token
            • 该方案跟之前的所有方案都有点不一样,需要两次请求才能完成一次业务操作。
            • 步骤
              1. 第一次请求获取 token
                • 接口幂等性_获取token_1
              2. 第二次请求带着这个 token,完成业务操作。
                • 接口幂等性_获取token_2
    • 多版本共存

      • 解决方案

        • Header 版本控制

          • 此方法需要客户端将指示资源版本的自定义 Header 添加到请求中,如果省略了此 Header,按默认值(一般是最新版)处理。

          • ```

            Request

            GET http://adventure-works.com/customers/3
            Custom-Header: api-version=1

            Response

            HTTP/1.1 200 OK
            Content-Type: application/json; charset=utf-8

            {“id”:3,”name”:”Contoso LLC”,”address”:”1 Microsoft Way Redmond WA 98053”}

            1
            2
            3
            4
            5
            6
            7
            8
              - 优点:纯粹的版本控制机制,符合 RESTful API「每个资源使用唯一的URI定位」的原则
            - 缺点:不直观,无法支持表单直接调用(众所周知 HTML 表单是不能添加自定义 Header 的),缓存不友好(不能认为同一 URI/查询字符串指向的是相同数据,因此难以缓存)
            - 媒体类型版本控制
            - 通常,Accept 标头的用途是由客户端指定响应的正文是 XML、JSON或其他格式。但是,可以定义包括以下信息的自定义媒体类型:该信息使客户端应用程序可以指示它所需的资源版本。
            - ```
            # Request
            GET http://adventure-works.com/customers/3 HTTP/1.1
            Accept: application/vnd.adventure-works.v1+json
          • 服务端负责处理 Accept 标头并尽可能采用该值(可以在 Accept 标头中指定多种格式,在这种情况下,服务端在其中选择最适合的格式用于响应正文)。返回结果中的 Content-Type 标头确认响应正文中的数据格式:

          • ```

            Response

            Content-Type: application/vnd.company.myapp-v3+xml

            1
            2
            3
            4
            - URI 版本控制
            - ```
            # Request
            GET http://adventure-works.com/v2/customers/3
          • 优点:直观,缓存友好,支持表单直接调用

          • 缺点:不符合「每个资源使用唯一的URI定位」的原则

        • 查询字符串版本控制

          • # Request
            GET http://adventure-works.com/customers/3?version=2
            
          • 优点:直观,缓存友好,支持表单直接调用,符合「每个资源使用唯一的URI定位」的原则
          • 缺点:某些较旧的 Web 浏览器和代理不会缓存在 URI 中包含字符串的请求的响应,这会对性能产生影响
    • 如何保证 API 接口数据安全?

      1. 登陆验证
      2. 权限验证
      3. HTTPS
      4. 接口签名
        • 签名流程
          • 接口安全性_签名流程
        • 签名规则
          • 线下分配 appid 和 appsecret,针对不同的调用方分配不同的 appid 和 appsecret
            • appSecret 的作用主要是区分不同客户端 app。并且利用获取到的 appSecret 参与到 sign 签名,保证了客户端的请求签名是由我们后台控制的,我们可以为不同的客户端颁发不同的 appSecret。
          • 加入 timestamp(时间戳),5 分钟内数据有效
          • 加入临时流水号 nonce(防止重复提交),至少为 10 位。针对查询接口,流水号只用于日志落地,便于后期日志核查。针对办理类接口需校验流水号在有效期内的唯一性,以避免重复请求。
          • 加入签名字段 signature,所有数据的签名信息。
        • 客户端签名生成
          • 所有动态参数 = 请求头部分 + 请求 URL 地址 + 请求 Request 参数 + 请求 Body
            • 上面的动态参数以 key-value 的格式存储,并以 key 值正序排序,进行拼接
          • 最后拼接的字符串再拼接 appSecret
          • 拼接成一个字符串,然后做md5不可逆加密
            • signature = DigestUtils.md5DigestAsHex(sortParamsMap + appSecret)
        • 服务端签名验证
          • 验证流程
            1. 验证必须的头部参数
            2. 获取头部参数,request 参数,Url 请求路径,请求体 Body,把这些值放入 SortMap 中进行排序
            3. 对 SortMap 里面的值进行拼接
            4. 对拼接的值进行加密,生成 sign
            5. 把生成的 sign 和前端传入的 sign 进行比较,如果不相同就返回错误
          • 附加功能
            • 可以通过对请求的 timestamp 进行时间验证,如果大于 10 分钟表示此链接已经超时,防止别人来到这个链接去请求。
            • 利用 nonce 参数,防止重复提交

限流

  • 计数器(固定窗口)算法
    • 计数器算法是使用计数器在周期内累加访问次数,当达到设定的限流值时,触发限流策略。下一个周期开始时,进行清零,重新计数。
    • 限流算法_计数器(固定窗口)算法_1
    • 这个算法通常用于QPS限流和统计总访问量,对于秒级以上的时间周期来说,会存在一个非常严重的问题,那就是临界问题。
      • 假设 1min 内服务器的负载能力为 100,因此一个周期的访问量限制在 100,然而在第一个周期的最后 5 秒和下一个周期的开始 5 秒时间段内,分别涌入 100 的访问量,虽然没有超过每个周期的限制量,但是整体上 10 秒内已达到 200 的访问量,已远远超过服务器的负载能力,由此可见,计数器算法方式限流对于周期比较长的限流,存在很大的弊端。
      • 限流算法_计数器(固定窗口)算法_2
  • 滑动窗口算法
    • 滑动窗口算法是将时间周期分为 N 个小周期,分别记录每个小周期内访问次数,并且根据时间滑动删除过期的小周期。
      • 假设时间周期为 1min,将 1min 再分为 2 个小周期,统计每个小周期的访问数量,则第一个时间周期内,访问数量为 75,第二个时间周期内,访问数量为 100,超过 100 的访问则被限流掉了。
      • 限流算法_滑动窗口算法
    • 当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
    • 此算法可以很好的解决固定窗口算法的临界问题。
  • 漏桶算法
    • 漏桶算法是访问请求到达时直接放入漏桶,如当前容量已达到上限(限流值),则进行丢弃(触发限流策略)。漏桶以固定的速率进行释放访问请求(即请求通过),直到漏桶为空。
    • 限流算法_漏桶算法
  • 令牌桶算法
    • 令牌桶算法是程序以 r(r=时间周期/限流值)的速度向令牌桶中增加令牌,直到令牌桶满,请求到达时向令牌桶请求令牌,如获取到令牌则通过请求,否则触发限流策略。
    • 限流算法_令牌桶算法
    • Google 开源工具包 Guava 提供了限流工具类 RateLimiter,该类基于令牌桶算法来完成限流,非常易于使用。

工程及解决方案

面向对象

  • SOLID 原则
    • The Single Responsibility Principle
      • 单一责任原则
      • 当需要修改某个类的时候原因有且只有一个(THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE)。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。
    • The Open Closed Principle
      • 开放封闭原则
      • 一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭
    • The Liskov Substitution Principle
      • 里氏替换原则
      • 所有引用基类的地方必须能透明地使用其子类的对象
    • The Dependency Inversion Principle
      • 依赖倒置原则
      • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
      • 抽象不应该依赖于细节,细节应该依赖于抽象
    • The Interface Segregation Principle
      • 接口分离原则
      • 不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。

解决方案

  • 防止超卖
    • 解决方案
      • 使用 MySQL 的事务加排他锁来解决
      • 乐观锁
        • select version from goods WHERE id = 1001
        • update goods set num = num - 1, version = version + 1 WHERE id= 1001 AND num > 0 AND version = @version(上面查到的version);
      • 使用文件锁实现
        • 当用户抢到一件促销商品后先触发文件锁,防止其他用户进入,该用户抢到促销品后再解开文件锁,放其他用户进行操作。这样可以解决超卖的问题,但是会导致文件得 I/O 开销很大。
      • 使用了 Redis 的队列来实现
        • 将要促销的商品数量以队列的方式存入 Redis 中,每当用户抢到一件促销商品则从队列中删除一个数据,确保商品不会超卖。这个操作起来很方便,而且效率极高,最终我们采取这种方式来实现。
  • 流量削峰
    • 由来
      • 春节火车票抢购,大量的用户需要同一时间去抢购;以及大家熟知的阿里双 11 秒杀, 短时间上亿的用户涌入,瞬间流量巨大(高并发),比如:200 万人准备在凌晨 12:00 准备抢购一件商品,但是商品的数量缺是有限的 100-500 件左右。
      • 服务器的处理资源是有限的,所以出现峰值的时候,很容易导致服务器宕机,用户无法访问的情况出现。
    • 解决方案
      • 消息队列解决削峰
        • 消息队列解决削峰
      • 流量削峰漏斗:层层削峰
        • 对请求进行分层过滤,从而过滤掉一些无效的请求。
        • 流量削峰漏斗
        • 核心思想
          • 通过在不同的层次尽可能地过滤掉无效请求。
          • 通过 CDN 过滤掉大量的图片,静态资源的请求。
          • 再通过类似 Redis 这样的分布式缓存,过滤请求等就是典型的在上游拦截读请求。
        • 基本原则
          • 对写数据进行基于时间的合理分片,过滤掉过期的失效请求。
          • 对写请求做限流保护,将超出系统承载能力的请求过滤掉。
          • 涉及到的读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题。
          • 对写数据进行强一致性校验,只保留最后有效的数据。
  • 密码存储
    • 在注册时,根据用户设置的登录密码,生成其消息认证码,然后存储用户名和消息认证码,不存储原始密码。每次用户登录时,根据登录密码,生成消息认证码,与数据库中存储的消息认证码进行比对,以确认是否为有效用户,这样即使网站被脱库,用户的原始密码也不会泄露,不会为用户使用的其他网站带来账号风险。
    • 当然,使用的消息认证码算法其哈希碰撞的概率应该极低才行,目前一般在 HMAC 算法中使用 SHA256。对于这种方式需要注意一点:防止用户使用弱密码,否则也可能会被暴力破解。现在的网站一般要求用户密码 6 个字符以上,并且同时有数字和大小写字母,甚至要求有特殊字符。
    • 另外,也可以使用加入随机 salt 的哈希算法来存储校验用户密码。
  • 登陆
    • JWT
      • JWT_登陆流程
    • Token
      • 生存 UUID 作为 Token

性能评估

  • TPS、QPS
    • TPS
      • (Transactions Per Second),即每秒执行的事务总数。
      • 首先一个事务包括三个动作,即客户端请求服务端,服务端内部进行处理,服务端对客户端进行响应。
      • 将这三个动作看成一个整体,并将之称为一个事务,若在一秒内,服务端可以完成 N 个事务,则这个服务端的 TPS 为 N。
      • 一般来说,评价系统的性能主要看系统的 TPS,系统的整体性能取决于性能最低模块的 TPS 值。
    • QPS
      • (Queries Per Second),及每秒执行的查询总数(每秒有多少的请求响应)
      • 客户端请求一个地址时,比如百度首页,其实会产生很多的请求,比如 js、css、png等,像这样的每个单个请求都可以算作查询次数。
      • 若在一秒内,客户端请求服务端的首页,服务端返回了 N 个内部链接(js、css、png、html 等),那么服务端的 QPS 就为 N。
      • QPS 反映系统的吞吐能力,更偏向于读取文件,查询数据。
    • 举例
      • 若在一秒内,用户请求了百度首页并看到了首页全貌,这样就形成了一个 TPS,但却形成了多个 QPS。
      • 若在一秒内,我们请求一个单调的网页,此网页只有一个 html,不包含任何其他内部链接,此时 TPS=QPS。
  • 独立访客、综合浏览量
    • 网站流量是指网站的访问量,用来描述访问网站的用户数量以及用户所浏览的网页数量等指标,常用的统计指标包括网站的独立用户数量、总用户数量(含重复访问者)、网页浏览数量、每个用户的页面浏览数量、用户在网站的平均停留时间等。
    • 网站访问量的常用衡量标准:独立访客(UV)和 综合浏览量(PV),一般以日为单位来衡量和计算。
      • 独立访客(UV):指一定时间范围内相同访客多次访问网站,只计算为 1 个独立访客。
      • 综合浏览量(PV):指一定时间范围内页面浏览量或点击量,用户每次刷新即被计算一次。
  • PV 计算带宽
    • 计算带宽大小需要关注两个指标:峰值流量和页面的平均大小。
    • 举个例子:
      • 假设网站的平均日 PV:10w 的访问量,页面平均大小 0.4 M 。

      • 网站带宽 = 10w / (24 * 60 * 60)* 0.4M * 8 =3.7 Mbps

      • 具体的计算公式是:网站带宽 = PV / 统计时间(换算到s)* 平均页面大小(单位KB)* 8

    • 在实际的网站运行过程中,我们的网站必须要在峰值流量时保持正常的访问,假设,峰值流量是平均流量的 5 倍,按照这个计算,实际需要的带宽大约在 3.7 Mbps * 5=18.5 Mbps
    • PS:
      1. 字节的单位是 Byte,而带宽的单位是 bit,1Byte=8bit,所以转换为带宽的时候,要乘以 8。
      2. 在实际运行中,由于缓存、CDN、白天夜里访问量不同等原因,这个是绝对情况下的算法。
  • PV 与并发
    • 具体的计算公式是:并发连接数 = PV / 统计时间 * 页面衍生连接次数 * http响应时间 * 因数 / web服务器数量
    • 解释:
      • 页面衍生连接次数:一个页面请求,会有好几次 http 连接,如外部的 css、js、图片等,这个根据实际情况而定。
      • http 响应时间:平均一个 http 请求的响应时间,可以使用 1 秒或更少。
      • 因数:峰值流量和平均流量的倍数,一般使用 5,最好根据实际情况计算后得出。
    • 例子:
      • 10w PV 的并发连接数:(100000PV / 86400秒 * 50个派生连接数 * 1秒内响应 * 5倍峰值) / 1台Web服务器 = 289 并发连接数
    • 所以,如果我们能够测试出单机的并发连接数和日 pv 数,那么我们同样也能估算出需要 web 的服务器数量。
    • 还有一套通过单机 QPS 计算 pv 和 需要的 web 服务器数量的方法,目前一些公司采用这种计算方法,但是其实计算的原理都是差不多的。
  • QPS、PV 和需要部署机器数量计算公式
    • QPS = req/sec = 请求数/秒
    • QPS统计方式:一般使用 http_load 进行统计
    • QPS = 总请求数 / (进程总数 * 请求时间)
    • QPS:单个进程每秒请求服务器的成功次数
    • QPS 计算 PV 和机器的方式
      • 单台服务器每天 PV 计算
        • 公式 1:每天总 PV = QPS * 3600 * 6
        • 公式 2:每天总 PV = QPS * 3600 * 8
      • 服务器计算
        • 服务器数量 = (每天总PV / 单台服务器每天总PV)
      • 峰值 QPS 和机器计算公式
        • 原理:每天 80% 的访问集中在 20% 的时间里,这 20% 时间叫做峰值时间
        • 公式:(总 PV 数 * 80%) / (每天秒数 * 20%) = 峰值时间每秒请求数(QPS)
        • 机器:峰值时间每秒 QPS / 单台机器的 QPS = 需要的机器
      • 例子:每天 300w PV 的在单台机器上,这台机器需要多少 QPS?
        • (3000000 * 0.8) / (86400 *0.2) = 139 (QPS)
      • 例子:如果一台机器的 QPS 是 58,需要几台机器来支持?
        • 139 / 58 = 3

测试

  • 常见场景
    • 新系统上线
    • 升级重构
    • 容量规划
    • 性能探测
    • 系统稳定性
  • 常见的测试
    • 基准测试
      • 通过单用户循环调用接口,持续 10 分钟, 统计响应时间的平均值,和 TP90、TP99,了解接口在系统无压力情况下的性能数据。
        • 相关概念
          • TP90,top percent 90,即 90% 的数据都满足某一条件;
          • TP95,top percent 95,即 95% 的数据都满足某一条件;
          • TP99,top percent 99,即 99% 的数据都满足某一条件;
    • 负载测试
      • 从 0 开始逐步增加系统压力,知道系统吞吐量或者系统资源消耗达到预估的标准,了解系统在安全运行下的极限。
    • 压力测试
      • 从负载测试探测到的系统吞吐量开始继续加压,直到系统错误率超标甚至不可以用,了解系统可服务的极限。
    • 缓存穿透
      • 了解系统缓存不可以下的吞吐量。
    • 扩展性测试
      • 扩展应用服务器数量,了解系统扩展能力。
  • 测试工具
    • LoadRunner
    • Apache JMeter
    • Siege
      • 常用参数
        • -c
          • 并发数量
        • -r
          • 重复的次数
        • -t
          • 测试时间
        • -i
          • 随机访问 -f 指定的 url.txt 中的 url 列表项,以此模拟真实的访问情况(随机性)
        • -b
          • –benchmark:基准测试,请求之间没有延迟。

负载均衡

  • 一般负载分为软件负载和硬件负载,比如软件中使用 nginx 等工具实现负载均衡,而 F5 负载均衡器就是硬件网络性能优化设备。
  • 硬件
    • F5 负载均衡器
      • 通俗的讲是将客户端请求量通过 F5 负载到各个服务器,增加吞吐量,从而降低服务器的压力,他不同于交换机、路由器这些网络基础设备,而是建立在现有网络结构上用来增加网络带宽和吞吐量的的硬件设备
      • 工作原理
        1. 客户发出服务请求到 VIP
        2. BIGIP 接收到请求,将数据包中目的 IP 地址改为选中的后台服务器 IP 地址,然后将数据包发出到后台选定的服务器
        3. 后台服务器收到后,将应答包按照其路由发回到 BIGIP
        4. BIGIP 收到应答包后将其中的源地址改回成 VIP 的地址,发回客户端,由此就完成了一个标准的服务器负载平衡的流程

协议许可证

  • 协议许可证

基础

  • MongoDB 是一个文档数据库,提供好的性能,领先的非关系型数据库。采用 BSON 存储文档数据。
  • MongoDB的优势有哪些
    • 面向文档的存储:以 JSON 格式的文档保存数据。
    • 任何属性都可以建立索引。
    • 复制以及高可扩展性。
    • 自动分片。
    • 丰富的查询功能。
    • 快速的即时更新。
    • 来自 MongoDB 的专业支持。
  • 基础概念
    • 模型层面
      • database 数据库,与 SQL 的数据库(database)概念相同,一个数据库包含多个集合(表)
      • collection 集合,相当于 SQL 中的表(table),一个集合可以存放多个文档(行)。不同之处就在于集合的结构(schema)是动态的,不需要预先声明一个严格的表结构。更重要的是,默认情况下 MongoDB 并不会对写入的数据做任何 schema 的校验。
      • document 文档,相当于 SQL 中的行(row),一个文档由多个字段(列)组成,并采用 bson(json)格式表示。
      • field 字段,相当于 SQL 中的列(column),相比普通 column 的差别在于 field 的类型可以更加灵活,比如支持嵌套的文档、数组。
      • 此外,MongoDB 中字段的类型是固定的、区分大小写、并且文档中的字段也是有序的。
      • MongoDB 和关系型数据库术语对比
        • MongoDB和关系型数据库术语对比图
    • SQL 层面
      • _id 主键,MongoDB 默认使用一个_id 字段来保证文档的唯一性。
      • reference 引用,勉强可以对应于外键(foreign key)的概念,之所以是勉强是因为 reference 并没有实现任何外键的约束,而只是由客户端(driver)自动进行关联查询、转换的一个特殊类型。
      • view 视图,MongoDB 3.4 开始支持视图,和 SQL 的视图没有什么差异,视图是基于表/集合之上进行动态查询的一层对象,可以是虚拟的,也可以是物理的(物化视图)。
      • index 索引,与 SQL 的索引相同。
      • $lookup,这是一个聚合操作符,可以用于实现类似 SQL-join 连接的功能
      • transaction 事务,从 MongoDB 4.0 版本开始,提供了对于事务的支持
      • aggregation 聚合,MongoDB 提供了强大的聚合计算框架,group by 是其中的一类聚合操作。
      • MongoDB_SQL概念映射对比图
  • 分布式 ID
    • MongoDB 采用 ObjectId 来表示主键的类型,数据库中每个文档都拥有一个_id 字段表示主键。
    • _id 的生成规则如下:
      • MongoDB_ID生成规则
      • 4-byte Unix 时间戳
      • 3-byte 机器 ID
      • 2-byte 进程 ID
      • 3-byte 计数器(初始化随机)
    • 值得一提的是 _id 的生成实质上是由客户端(Driver)生成的,这样可以获得更好的随机性,同时降低服务端的负载。
    • 当然服务端也会检测写入的文档是否包含 _id 字段,如果没有就生成一个。

索引

  • MongoDB 支持非常丰富的索引类型。
  • 索引的技术实现依赖于底层的存储引擎,在当前的版本中 MongoDB 使用 wiredTiger 作为默认的引擎。
  • 在索引的实现上使用了 B+树的结构。
  • 大部分基于SQL数据库的一些索引调优技巧在 MongoDB 上仍然是可行的。
  • 索引特性
    • unique=true,表示一个唯一性索引
    • expireAfterSeconds=3600,表示这是一个TTL索引,并且数据将在1小时后老化
    • sparse=true,表示稀疏的索引,仅索引非空(non-null)字段的文档
    • partialFilterExpression: { rating: { $gt: 5 },条件式索引,即满足计算条件的文档才进行索引
  • 索引分类
    • 哈希(HASH)索引,哈希是另一种快速检索的数据结构,MongoDB 的 HASH 类型分片键会使用哈希索引。
    • 地理空间索引,用于支持快速的地理空间查询,如寻找附近1公里的商家。
    • 文本索引,用于支持快速的全文检索
    • 模糊索引(Wildcard Index),一种基于匹配规则的灵活式索引,在4.2版本开始引入。
  • 索引评估、调优
    • 使用 explain() 命令可以用于查询计划分析,进一步评估索引的效果。

集群

  • 一个典型的 MongoDB 集群架构会同时采用分片+副本集的方式
    • MongoDB_集群实例
    • 架构说明
      • 数据分片(Shards)
        • 分片用于存储真正的集群数据,可以是一个单独的 Mongod 实例,也可以是一个副本集。生产环境下 Shard 一般是一个 Replica Set,以防止该数据片的单点故障。
        • 对于分片集合(sharded collection)来说,每个分片上都存储了集合的一部分数据(按照分片键切分),如果集合没有分片,那么该集合的数据都存储在数据库的 Primary Shard中。
      • 配置服务器(Config Servers)
        • 保存集群的元数据(metadata),包含各个 Shard 的路由规则,配置服务器由一个副本集(ReplicaSet)组成。
      • 查询路由(Query Routers)
        • Mongos 是 Sharded Cluster 的访问入口,其本身并不持久化数据。Mongos 启动后,会从 Config Server 加载元数据,开始提供服务,并将用户的请求正确路由到对应的Shard。
        • Sharding 集群可以部署多个 Mongos 以分担客户端请求的压力。
  • 分片机制
    • 数据如何切分
      • 首先,基于分片切分后的数据块称为 chunk,一个分片后的集合会包含多个 chunk,每个 chunk 位于哪个分片(Shard)则记录在 Config Server(配置服务器)上。
      • Mongos 在操作分片集合时,会自动根据分片键找到对应的 chunk,并向该 chunk 所在的分片发起操作请求。
      • 数据是根据分片策略来进行切分的,而分片策略则由分片键(ShardKey)+分片算法(ShardStrategy)组成。
        • MongoDB 支持两种分片算法:
          • 范围分片
            • MongoDB_分片算法_范围分片
          • 哈希分片
            • MongoDB_分片算法_哈希分片
    • 如何保证均衡
      • 真实的场景中,会存在下面两种情况:
        • 全预分配,chunk 的数量和 shard 都是预先定义好的,比如 10 个 shard,存储 1000 个 chunk,那么每个 shard 分别拥有 100 个 chunk。
        • 非预分配,这种情况则比较复杂,一般当一个 chunk 太大时会产生分裂(split),不断分裂的结果会导致不均衡;或者动态扩容增加分片时,也会出现不均衡的状态。 这种不均衡的状态由集群均衡器进行检测,一旦发现了不均衡则执行 chunk 数据的搬迁达到均衡。
      • MongoDB 的数据均衡器运行于 Primary Config Server(配置服务器的主节点)上,而该节点也同时会控制 Chunk 数据的搬迁流程。
        • MongoDB_分片机制_数据自动均衡
        • 对于数据的不均衡是根据两个分片上的 Chunk 个数差异来判定的,阈值对应表如下:
          • MongoDB_分片机制_数据自动均衡规则
      • MongoDB 的数据迁移对集群性能存在一定影响,这点无法避免,目前的规避手段只能是将均衡窗口对齐到业务闲时段。
    • 应用高可用
      • 应用节点可以通过同时连接多个 Mongos 来实现高可用
        • MongoDB_Mongos_连接
  • 副本集
    • 副本集可以作为 Shard Cluster 中的一个Shard(片)之外,对于规模较小的业务来说,也可以使用一个单副本集的方式进行部署。
    • MongoDB 的副本集采取了一主多从的结构,即一个 Primary Node + N* Secondary Node 的方式,数据从主节点写入,并复制到多个备节点。
    • 架构
      • MongoDB_副本架构
    • 利用副本集,我们可以实现:
      • 数据库高可用,主节点宕机后,由备节点自动选举成为新的主节点;
      • 读写分离,读请求可以分流到备节点,减轻主节点的单点压力。
        • 请注意,读写分离只能增加集群”读”的能力,对于写负载非常高的情况却无能为力。
        • 对此需求,使用分片集群并增加分片,或者提升数据库节点的磁盘IO、CPU能力可以取得一定效果。
    • 选举
      • MongoDB 副本集通过 Raft 算法来完成主节点的选举,这个环节在初始化的时候会自动完成
    • 心跳
      • 副本集中的每个节点上都会定时向其他节点发送心跳,以此来感知其他节点的变化,比如是否失效、或者角色发生了变化。
      • 利用心跳,MongoDB 副本集实现了自动故障转移的功能
      • 流程
        • 默认情况下,节点会每 2 秒向其他节点发出心跳,这其中包括了主节点。如果备节点在 10 秒内没有收到主节点的响应就会主动发起选举。
        • 此时新一轮选举开始,新的主节点会产生并接管原来主节点的业务。整个过程对于上层是透明的,应用并不需要感知,因为 Mongos 会自动发现这些变化。
        • 如果应用仅仅使用了单个副本集,那么就会由 Driver 层来自动完成处理。
    • 复制
      • 主节点和备节点的数据是通过日志(oplog)复制来实现的,这很类似于 mysql 的 binlog。
      • 流程
        • 在每一个副本集的节点中,都会存在一个名为 local.oplog.rs 的特殊集合。当 Primary 上的写操作完成后,会向该集合中写入一条 oplog,而 Secondary 则持续从 Primary 拉取新的 oplog 并在本地进行回放以达到同步的目的。
      • MongoDB 对于 oplog 的设计是比较仔细的,比如:
        • oplog 必须保证有序,通过 optime 来保证。
        • oplog 必须包含能够进行数据回放的完整信息。
        • oplog 必须是幂等的,即多次回放同一条日志产生的结果相同。
        • oplog 集合是固定大小的,为了避免对空间占用太大,旧的 oplog 记录会被滚动式的清理。

事务与一致性

  • 实质上,MongoDB 很早就有事务的概念,但是这个事务只能是针对单文档的,即单个文档的操作是有原子性保证的。
  • 在 4.0 版本之后,MongoDB 开始支持多文档的事务:
    • 4.0 版本支持副本集范围的多文档事务。
    • 4.2 版本支持跨分片的多文档事务(基于两阶段提交)。
  • 在事务的隔离性上,MongoDB 支持快照(snapshot)的隔离级别,可以避免脏读、不可重复读和幻读。
  • 一致性
    • 在分布式架构的 CAP 理论以及许多延续的观点中提到,由于网络分区的存在,要求系统在一致性和可用性之间做出选择,而不能两者兼得。
      • CAP理论
    • 在 MongoDB 中,这个选择是可以由开发者来定的。MongoDB 允许客户端为其操作设定一定的级别或者偏好,包括:
      • read preference
        • 读取偏好,可指定读主节点、读备节点,或者是优先读主、优先读备、取最近的节点
      • write concern
        • 写关注,指定写入结果达到什么状态时才返回,可以为无应答(none)、应答(ack),或者是大多数节点完成了数据复制等等
      • read concern
        • 读关注,指定读取的数据版本处于怎样的状态,可以为读本地、读大多数节点写入,或者是线性读(linearizable)等等。
    • 使用不同的设定将会产生对于C(一致性)、A(可用性)的不同的抉择,比如:
      • 将读偏好设置为 primary,此时读写都在主节点上。这保证了数据的一致性,但一旦主节点宕机会导致失败(可用性降低)
      • 将读偏好设置为 secondaryPrefered,此时写主,优先读备,可用性提高了,但数据存在延迟(出现不一致)
      • 将读写关注都设置为 majority(大多数),一致性提升了,但可用性也同时降低了(节点失效会导致大多数写失败)

数据结构

基本

  • 字符串
    • 一个 key 对应一个 value
    • 原理
      • 如果一个字符串对象保存的是整数值,并且这个整数值可以用 long 类型标识,那么字符串对象会讲整数值保存在 ptr 属性中,并将 encoding 设置为 int。
      • 如果字符串对象保存的是一个字符串值,Redis 的字符串底层数据结构是 sds(simple dynamic string),即简单动态字符串。
        • 具体由 embstr 编码方式和 raw 编码方式实现
          • embstr
            • Redis_string_embstr编码
          • raw
            • Redis_string_raw编码
          • 对比
            • embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次。
            • 释放 embstr 编码的字符串对象只需要调用一次内存释放函数,而释放 raw 编码的字符串对象需要调用两次内存释放函数。
            • 因为 embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起 raw ,编码的字符串对象能够更好地利用缓存带来的优势。
          • sdshdr
            • 结构
              • ```
                struct sdshdr{
                int len;//已使用保存的字符串长度
                int free;//未使用字符串长度
                char buf[];保存字符串的数组
                
                }
                1
                2
                3
                4
                5
                6
                7
                8
                9
                10
                11
                12
                13
                14
                15
                16
                17
                18
                19
                20
                21
                22
                23
                24
                25
                26
                27
                28
                29
                30
                31
                32
                33
                34
                35
                36
                37
                38
                39
                40
                41
                42
                43
                44
                45
                46
                47
                48
                49
                50
                51
                52
                53
                54
                55
                56
                57
                58
                59
                60
                61
                62
                63
                64
                65
                66
                67
                68
                69
                70
                71
                72
                73
                74
                75
                76
                77
                78
                79
                80
                81
                82
                83
                84
                85
                86
                87
                88
                89
                90
                91
                92
                93
                94
                95
                96
                97
                98
                99
                100
                101
                102
                103
                104
                105
                106
                107
                108
                109
                110
                111
                112
                113
                114
                115
                116
                117
                118
                119
                120
                121
                          - 版本区别
                - Redis3.2 之前,统一使用一个版本的 sdshdr。
                - Redis3.2 开始,对数据结构做出了修改,针对不同的长度范围定义了不同的结构。
                - sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用于存储不同的长度的字符串,分别代表 $2^5=32B$,$2^8=256B$,$2^16=64KB$,$2^32=4GB$,$2^64$ 约等于无穷大,但实际官方字符串 value 最大值为 512M。
                - 版本区别
                - Redis 的 embstr 编码方式和 raw 编码方式在 3.0 版本之前是以 39 字节为分界的
                - 在 3.2 版本之后,则变成了 44 字节为分界
                - 在 44 字节以内,使用 embstr 实现
                - 超过了 44 字节,使用 raw 存储
                - 相关问题
                - **Redis 字符串与 C 语言中的字符串区别?**
                - 复杂度问题
                - SDS 由于存储了 len 属性,所以获取字符长度的时间复杂度为 $O(1)$,而 C 字符串并不记录本身长度,故获取字符串长度需要遍历整个字符串,直到遇到空字符,时间复杂度为 $O(N)$。
                - 内存分配释放策略
                - SDS 是预分配+惰性释放
                - SDS 的内存分配策略:
                - 如果对 SDS 字符串修改后,len 的值小于 1MB,那么程序会分配和 len 同样大小的空间,此时 len 和 free 的值是相同的,例如,如果 SDS 的字符串长度修改为 15 字节,那么会分配 15 字节空间给 free,SDS 的 buf 属性长度为 15(len)+15(free)+1(空字符)= 31 字节。
                - 如果SDS字符串修改后,len 大于等于 1MB,那么程序会分配 1MB 的空间给 free,例如,SDS 字符串长度修改为 50MB 那么程序会分配 1MB 的未使用空间给 free,SDS 的 buf 属性长度为 50MB(len)+1MB(free)+1byte(空字符)。
                - SDS 的内存释放策略:
                - 当需要缩短 SDS 字符串时,程序并不立刻将内存释放,而是使用 free 属性将这些空间记录下来,以备将来使用。
                - 缓冲区溢出问题
                - SDS 的字符串的内存预分配策略能有效避免缓冲区溢出问题
                - C 字符串每次操作增加长度时,都要分配足够长度的内存空间,否则就会产生缓冲区溢出(buffer overflow)。
                - 二进制安全问题
                - SDS 字符串 API 都是以处理二进制的方式处理 buf 数组里的数据,程序不会对其中的数据进行过滤、操作等,所以 SDS 是二进制数据安全的。
                - C 字符串的字符则必须符合某种编码(ASCII),并且字符串的中间不能包含空字符,否则字符串就会被截断,所以 C 字符串智能保存文本数据,而不能保存图片、音视频等数据类型。
                - **为什么 Redis 的 embstr 与 raw 编码方式不再以 39 字节为界?**
                - embstr 是一块连续的内存区域,由 redisObject 和 sdshdr 组成。其中 redisObject 占 16 个字节,当 buf 内的字符串长度是 39 时,sdshdr 的大小为 8+39+1=48,那一个字节是'\0'。加起来刚好 64。
                - 从 2.4 版本开始,redis 开始使用 jemalloc 内存分配器。这个比 glibc 的 malloc 要好不少,还省内存。在这里可以简单理解,jemalloc 会分配 8,16,32,64 等字节的内存。embstr 最小为 16+8+8+1=33,所以最小分配 64 字节。当字符数小于 39 时,都会分配 64 字节。这个默认 39 就是这样来的。
                - 本身就是针对短字符串的 embstr 自然会使用最小的 sdshdr8,而 sdshdr8 与之前的 sdshdr 相比正好减少了 5 个字节(sdsdr8 = uint8_t * 2 + char = 1*2+1 = 3, sdshdr = unsigned int * 2 = 4 * 2 = 8),所以其能容纳的字符串长度增加了 5 个字节变成了 44。
                - list(列表)
                - Redis 链表是一个双向无环链表结构,可以通过 push 和 pop 操作从列表的头部或者尾部添加或者删除元素,这样 list 即可以作为栈,也可以作为队列。很多发布订阅、慢查询、监视器功能都是使用到了链表来实现。
                - 原理
                - 底层有 linkedList、zipList 和 quickList 这三种存储方式。
                - 数据结构
                - linkedList、zipList
                - 当列表对象中元素的长度较小或者数量较少时,通常采用 zipList 来存储;当列表中元素的长度较大或者数量比较多的时候,则会转而使用双向链表 linkedList 来存储。
                - 双向链表 linkedList 便于在表的两端进行 push 和 pop 操作,在插入节点上复杂度很低,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还有额外保存两个指针;其次,双向链表的各个节点都是单独的内存块,地址不连续,容易形成内存碎片。
                - zipList 存储在一块连续的内存上,所以存储效率很高。但是它不利于修改操作,插入和删除操作需要频繁地申请和释放内存。特别是当 zipList 长度很长时,一次 realloc 可能会导致大量的数据拷贝。
                - quickList
                - 在 Redis3.2 版本之后,list 的底层实现方式又多了一种,quickList。qucikList 是由 zipList 和双向链表 linkedList 组成的混合体。它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。
                - ![Redis_quickList](Redis-0-知识点汇总/Redis_quickList.png)
                - hash(散列)
                - hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
                - 原理
                - 哈希对象的编码有两种,分别是:ziplist、dict。
                - 数据结构
                - ziplist
                - 使用压缩列表实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的节点推入到压缩列表的表尾,然后再将保存了值的节点推入到压缩列表表尾。
                - ![Redis_ziplist_2](Redis-0-知识点汇总/Redis_ziplist_2.jpg)
                - dict
                - 使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值来保存,跟 java 中的 HashMap 类似。
                - ![Redis_hashtable](Redis-0-知识点汇总/Redis_hashtable.jpg)
                - 切换条件
                - 当哈希对象保存的键值对数量小于 512,并且所有键值对的长度都小于 64 字节时,使用压缩列表存储;否则使用 dict 存储。
                - 扩容流程(渐进式 rehash)
                1. 计算新表 size、掩码,为新表 ht[1] 分配空间,让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
                2. 将 rehash 索引计数器变量 rehashidx 的值设置为 0,表示 rehash 正式开始。
                3. rehash 进行期间,每次对字典执行添加、删除、査找、更新操作时,程序除了执行指定的操作以外,还会触发额外的 rehash 操作,在源码中的 _dictRehashStep 方法。
                - 该方法会从 ht[0] 表的 rehashidx 索引位置上开始向后查找,找到第一个不为空的索引位置,将该索引位置的所有节点 rehash 到 ht[1],当本次 rehash 工作完成之后,将 ht[0] 索引位置为 rehashidx 的节点清空,同时将 rehashidx 属性的值加一。
                4. 将 rehash 分摊到每个操作上确实是非常妙的方式,但是万一此时服务器比较空闲,一直没有什么操作,难道 redis 要一直持有两个哈希表吗?
                - 答案当然不是的。我们知道,redis 除了文件事件外,还有时间事件,redis 会定期触发时间事件,这些时间事件用于执行一些后台操作,其中就包含 rehash 操作:当 redis 发现有字典正在进行 rehash 操作时,会花费 1 毫秒的时间,一起帮忙进行 rehash。
                5. 随着操作的不断执行,最终在某个时间点上,ht[0] 的所有键值对都会被 rehash 至 ht[1],此时 rehash 流程完成,会执行最后的清理工作:释放 ht[0] 的空间、将 ht[0] 指向 ht[1]、重置 ht[1]、重置 rehashidx 的值为 -1。
                - sets(集合)
                - Redis 提供的 set 数据结构,可以存储一些集合性的数据。set 中的元素是没有顺序的。
                - 原理
                - 集合对象的编码有两种,分别是:intset、dict。
                - 切换条件
                - set 的底层存储 intset 和 dict 是存在编码转换的,使用 intset 存储必须满足下面两个条件,否则使用 dict,条件如下:
                - 结合对象保存的所有元素都是整数值
                - 集合对象保存的元素数量不超过 512 个
                - sorted set(有序集合)
                - sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列
                - 原理
                - 底层数据结有序集合对象的编码有两种,分别是:ziplist、skiplist。
                - 数据结构
                - ziplist
                - 当保存的元素长度都小于 64 字节,同时数量小于 128 时,使用该编码方式,否则会使用 skiplist。
                - ![Redis_ziplist](Redis-0-知识点汇总/Redis_ziplist.jpg)
                - skiplist
                - zset 实现,一个 zset 同时包含一个字典(dict)和一个跳跃表(zskiplist)
                - ![Redis_skiplist](Redis-0-知识点汇总/Redis_skiplist.jpg)
                - 切换条件
                - 当有序集合的长度小于 128,并且所有元素的长度都小于 64 字节时,使用压缩列表存储;否则使用 skiplist 存储。
                ### 高级
                - HyperLogLog
                - 通常用于基数统计。使用少量固定大小的内存,来统计集合中唯一元素的数量。统计结果不是精确值,而是一个带有 0.81% 标准差(standard error)的近似值。
                - HyperLogLog 适用于一些对于统计结果精确度要求不是特别高的场景,例如网站的 UV 统计。
                - 在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
                - 基数
                - 比如数据集 `{1, 3, 5, 7, 5, 7, 8}`,那么这个数据集的基数集为 `{1, 3, 5 ,7, 8}`, 基数(不重复元素)为 5。基数估计就是在误差可接受的范围内,快速计算基数。
                - Geo
                - 可以将用户给定的地理位置信息储存起来,并对这些信息进行操作:获取 2 个位置的距离、根据给定地理位置坐标获取指定范围内的地理位置集合。
                - Bitmap
                - 位图。
                - Stream
                - 主要用于消息队列,类似于 kafka,可以认为是 pub/sub 的改进版。提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
                ### 原理
                - RedisObject
                - 为了便于操作,Redis 定义了 RedisObject 结构体来表示 string、hash、list、set、zset 五种数据类型。
                - ![Redis_RedisObject](Redis-0-知识点汇总/Redis_RedisObject.jpg)
                - 结构
                - 源码
                - ```
                /*
                * Redis 对象
                */
                typedef struct redisObject {
                // 类型
                unsigned type:4;
                // 对齐位
                unsigned notused:2;
                // 编码方式
                unsigned encoding:4;
                // LRU 时间(相对于 server.lruclock)
                unsigned lru:22;
                // 引用计数
                int refcount;
                // 指向对象的值
                void *ptr;
                } robj;
        • type 记录了对象所保存的值的类型, 它的值可能是以下常量的其中一个
          • REDIS_STRING // 字符串
          • REDIS_LIST // 列表
          • REDIS_SET // 集合
          • REDIS_ZSET // 有序集
          • REDIS_HASH // 哈希表
        • encoding 记录了对象所保存的值的编码, 它的值可能是以下常量的其中一个
          • REDIS_ENCODING_INT // 编码为整数
          • REDIS_ENCODING_EMBSTR // embstr 编码
          • REDIS_ENCODING_RAW // 编码为字符串
          • REDIS_ENCODING_HT // 编码为哈希表
          • REDIS_ENCODING_LINKEDLIST // 编码为双端链表
          • REDIS_ENCODING_ZIPLIST // 编码为压缩列表
          • REDIS_ENCODING_INTSET // 编码为整数集合
          • REDIS_ENCODING_SKIPLIST // 编码为跳跃表
        • ptr 是一个指针, 指向实际保存值的数据结构, 这个数据结构由 type 属性和 encoding 属性决定
        • refcount
          • refcount 表示引用计数,由于 C 语言并不具备内存回收功能,所以 Redis 在自己的对象系统中添加了这个属性,当一个对象的引用计数为 0 时,则表示该对象已经不被任何对象引用,则可以进行垃圾回收了。
        • lru 表示对象最后一次被命令程序访问的时间。
  • 底层数据结构
    • 字符串
      • Redis 没有直接使用 C 语言传统的字符串表示,而是自己实现的叫做简单动态字符串 SDS 的抽象类型。C 语言的字符串不记录自身的长度信息,而 SDS 则保存了长度信息,这样将获取字符串长度的时间由 O(N) 降低到了 O(1),同时可以避免缓冲区溢出和减少修改字符串长度时所需的内存重分配次数。
    • linkedlist
      • Redis链表特性:
        • 双端:链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为 $O(1)$。
        • 无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结束。
        • 带链表长度计数器:通过 len 属性获取链表长度的时间复杂度为 $O(1)$。
        • 多态:链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。
    • dict
      • 用于保存键值对的抽象数据结构。
      • Redis 使用 hash 表作为底层实现,每个字典带有两个 hash 表,供平时使用和 rehash 时使用,hash 表使用链地址法来解决键冲突,被分配到同一个索引位置的多个键值对会形成一个单向链表,在对 hash 表进行扩容或者缩容的时候,为了服务的可用性,rehash 的过程不是一次性完成的,而是渐进式的。
    • ziplist
      • 压缩列表是为节约内存而开发的顺序性数据结构,他可以包含多个节点,每个节点可以保存一个字节数组或者整数值。
    • skiplist
      • 跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。
      • 结构
        • Redis_skiplist_2
      • 特性
        • 由很多层结构组成;
        • 每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点;
        • 最底层的链表包含了所有的元素;
        • 如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集);
        • 链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的同一个链表节点;
      • 操作方式
        • 搜索:从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下找,也就是和当前层的下一层的节点的下一个节点进行比较,以此类推,一直找到最底层的最后一个节点,如果找到则返回,反之则返回空。
        • 插入:首先确定插入的层数,有一种方法是假设抛一枚硬币,如果是正面就累加,直到遇见反面为止,最后记录正面的次数作为插入的层数。当确定插入的层数 k 后,则需要将新元素插入到从底层到 k 层。
        • 删除:在各个层中找到包含指定值的节点,然后将节点从链表中删除即可,如果删除以后只剩下头尾两个节点,则删除这一层。
    • intset
      • 用于保存整数值的集合抽象数据结构,不会出现重复元素,底层实现为数组。

持久化

  • AOF
    • Append-only file,将“操作 + 数据”以格式化指令的方式追加到操作日志文件的尾部,在 append 操作返回后(已经写入到文件或者即将写入),才进行实际的数据变更,“日志文件”保存了历史所有的操作过程;当 server 需要数据恢复时,可以直接 replay 此日志文件,即可还原所有的操作过程。
    • 优点
      • 可以保持更高的数据完整性,如果设置追加 file 的时间是 1s,如果 Redis 发生故障,最多会丢失 1s 的数据;且如果日志写入不完整支持 redis-check-aof 来进行日志修复;AOF 文件没被 rewrite 之前(文件过大时会对命令进行合并重写),可以删除其中的某些命令(比如误操作的 flushall)。
    • 缺点
      • AOF 文件比 RDB 文件大,且恢复速度慢。
    • AOF 重写
      • 引入原因
        • AOF 持久化是通过保存被执行的写命令来记录数据库状态的,随着写入命令的不断增加,AOF 文件中的内容会越来越多,文件的体积也会越来越大。
        • 如果不加以控制,体积过大的 AOF 文件可能会对 Redis 服务器、甚至整个宿主机造成影响,并且 AOF 文件的体积越大,使用 AOF 文件来进行数据还原所需的时间就越多。
        • 举个例子,如果你对一个计数器调用了 100 次 INCR,那么仅仅是为了保存这个计数器的当前值,AOF 文件就需要使用 100 条记录。
        • 然而在实际上,只使用一条 SET 命令已经足以保存计数器的当前值了,其余 99 条记录实际上都是多余的。
        • 为了处理这种情况,Redis 引入了 AOF 重写:可以在不打断服务端处理请求的情况下,对 AOF 文件进行重建(rebuild)。
      • 存在的问题
        • AOF 后台重写使用子进程进行从写,解决了主进程阻塞的问题,但是仍然存在另一个问题:子进程在进行 AOF 重写期间,服务器主进程还需要继续处理命令请求,新的命令可能会对现有的数据库状态进行修改,从而使得当前的数据库状态和重写后的 AOF 文件保存的数据库状态不一致。
          • 如何解决 AOF 后台重写存在的数据不一致问题?
            • 为了解决上述问题,Redis 引入了 AOF 重写缓冲区(aof_rewrite_buf_blocks),这个缓冲区在服务器创建子进程之后开始使用,当 Redis 服务器执行完一个写命令之后,它会同时将这个写命令追加到 AOF 缓冲区和 AOF 重写缓冲区。当 aof 重写完成时,主进程在把 aof 重写缓冲区的数据写到 aof 缓冲区,最后 fsync 到 aof 文件中。
      • rewrite 流程
        • Redis_aof_rewrite流程
      • 触发时机
        • 触发 rewrite 的时机可以通过配置文件来声明,同时 redis 中可以通过 bgrewriteaof 指令人工干预。
    • 触发时机
      • redis 提供了 3 中 aof 记录同步选项:
        • always:每一条 aof 记录都立即同步到文件,这是最安全的方式,也以为更多的磁盘操作和阻塞延迟,是 I/O 开支较大。
        • everysec:每秒同步一次,性能和安全都比较中庸的方式,也是 redis 推荐的方式。如果遇到物理服务器故障,有可能导致最近一秒内 aof 记录丢失(可能为部分丢失)。
        • no:redis 并不直接调用文件同步,而是交给操作系统来处理,操作系统可以根据 buffer 填充情况/通道空闲时间等择机触发同步;这是一种普通的文件操作方式。性能较好,在物理服务器故障时,数据丢失量会因 OS 配置有关。
  • RDB
    • RDB 是在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化的文件,达到数据恢复。
    • 优点
      • 使用单独子进程来进行持久化,主进程不会进行任何 I/O 操作,保证了 Redis 的高性能
    • 缺点
      • RDB 是间隔一段时间进行持久化,如果持久化之间 Redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候
      • 每次快照持久化都是将内存数据完整写入到磁盘一次,并不是增量的只同步脏数据。如果数据量大的话,而且写操作比较多,必然会引起大量的磁盘 I/O 操作,可能会严重影响性能。
    • RDB 持久化的两种方法
      • save
        • 描述
          • 同步、阻塞
        • 缺点
          • 致命的问题,持久化的时候 redis 服务阻塞(准确的说会阻塞当前执行 save 命令的线程,但是 redis 是单线程的,所以整个服务会阻塞),不能继对外提供请求
      • bgsave
        • 描述
          • 异步、非阻塞
        • 原理
          • fork() + copyonwrite
            • fork()
              • fork() 是什么
                • fork() 是 unix 和 linux 这种操作系统的一个 api,而不是 Redis 的 api。
              • fork() 有什么用
                • fork() 用于创建一个子进程,注意是子进程,不是子线程。fork() 出来的进程共享其父类的内存数据。仅仅是共享 fork() 出子进程的那一刻的内存数据,后期主进程修改数据对子进程不可见,同理,子进程修改的数据对主进程也不可见。
                • 比如:A 进程 fork() 了一个子进程 B,那么 A 进程就称之为主进程,这时候主进程子进程所指向的内存空间是同一个,所以他们的数据一致。但是 A 修改了内存上的一条数据,这时候 B 是看不到的,A 新增一条数据,删除一条数据,B 都是看不到的。而且子进程 B 出问题了,对我主进程 A 完全没影响,我依然可以对外提供服务,但是主进程挂了,子进程也必须跟随一起挂。这一点有点像守护线程的概念。Redis 正是巧妙的运用了 fork() 这个牛逼的 api 来完成 RDB 的持久化操作。
              • Redis 中的 fork()
                • Redis 巧妙的运用了 fork()。当 bgsave 执行时,Redis 主进程会判断当前是否有 fork() 出来的子进程,若有则忽略,若没有则会 fork() 出一个子进程来执行 rdb 文件持久化的工作,子进程与 Redis 主进程共享同一份内存空间,所以子进程可以搞他的 rdb 文件持久化工作,主进程又能继续他的对外提供服务,二者互不影响。我们说了他们之后的修改内存数据对彼此不可见,但是明明指向的都是同一块内存空间,这是咋搞得?肯定不可能是 fork() 出来子进程后顺带复制了一份数据出来,如果是这样的话比如我有 4g 内存,那么其实最大有限空间是 2g,我要给 rdb 留出一半空间来,扯淡一样!那他咋做的?采取了 copyonwrite 技术。
            • copyonwrite
              • 原理
                • 主进程 fork() 子进程之后,内核把主进程中所有的内存页的权限都设为 read-only,然后子进程的地址空间指向主进程。这也就是共享了主进程的内存,当其中某个进程写内存时(这里肯定是主进程写,因为子进程只负责 rdb 文件持久化工作,不参与客户端的请求),CPU 硬件检测到内存页是 read-only 的,于是触发页异常中断(page-fault),陷入内核的一个中断例程。中断例程中,内核就会把触发的异常的页复制一份(这里仅仅复制异常页,也就是所修改的那个数据页,而不是内存中的全部数据),于是主子进程各自持有独立的一份。
              • 回到原问题
                • 其实就是更改数据的之前进行 copy 一份更改数据的数据页出来,比如主进程收到了 set k 1 请求(之前 k 的值是 2),然后这同时又有子进程在 rdb 持久化,那么主进程就会把 k 这个 key 的数据页拷贝一份,并且主进程中 k 这个指针指向新拷贝出来的数据页地址上,然后进行更改值为 1 的操作,这个主进程 k 元素地址引用的新拷贝出来的地址,而子进程引用的内存数据k还是修改之前的。
        • 优点
          • 他可以一边进行持久化,一边对外提供读写服务,互不影响,新写的数据对我持久化不会造成数据影响,你持久化的过程中报错或者耗时太久都对我当前对外提供请求的服务不会产生任何影响。持久化完会将新的 rdb 文件覆盖之前的。
    • 触发时机
      • 执行数据写入到临时文件的时间点是可以通过配置来自己确定的,通过配置 redis 在 n 秒内如果超过 m 个 key 被修改这执行一次 RDB 操作。这个操作就类似于在这个时间点来保存一次 Redis 的所有数据,一次快照数据。所有这个持久化方法也通常叫做 snapshots。
      • snapshot 触发的时机,是有“间隔时间”和“变更次数”共同决定,同时符合 2 个条件才会触发 snapshot,否则“变更次数”会被继续累加到下一个“间隔时间”上。snapshot 过程中并不阻塞客户端请求。snapshot 首先将数据写入临时文件,当成功结束后,将临时文件重名为 dump.rdb。
  • 混合持久化
    • 混合持久化只发生于 AOF 重写过程。使用了混合持久化,重写后的新 AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。
    • 优点:结合 RDB 和 AOF 的优点, 更快的重写和恢复。
    • 缺点:AOF 文件里面的 RDB 部分不再是 AOF 格式,可读性差。

内存淘汰策略

  • 删除过期键的策略(Redis 使用惰性删除和定期删除。)
    • 清除策略
      • 定时删除
        • 在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。对内存最友好,对 CPU 时间最不友好。
      • 惰性删除
        • 放任键过期不管,但是每次获取键时,都检査键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。对 CPU 时间最优化,对内存最不友好。
      • 定期删除
        • 每隔一段时间,默认 100ms,程序就对数据库进行一次检査,删除里面的过期键。至于要删除多少过期键,以及要检査多少个数据库,则由算法决定。前两种策略的折中,对 CPU 时间和内存的友好程度较平衡。
    • Redis 具体实现
      • 比如 Redis-3.0.0 中的 hz 默认值是 10,代表每秒钟调用 10 次后台任务。
        • 典型的方式为,Redis 每秒做 10 次如下的步骤:
          1. 随机测试 100 个设置了过期时间的 key
          2. 删除所有发现的已过期的 key
          3. 若删除的 key 超过 25 个则重复步骤 1
        • 这是一个基于概率的简单算法,基本的假设是抽出的样本能够代表整个 key 空间,redis 持续清理过期的数据直至将要过期的 key 的百分比降到了 25% 以下。这也意味着在任何给定的时刻已经过期但仍占据着内存空间的 key 的量最多为每秒的写操作量除以 4。
  • 不管是定期采样删除还是惰性删除都不是一种完全精准的删除,就还是会存在 key 没有被删除掉的场景,所以就需要内存淘汰策略进行补充。
    • 内存淘汰(驱逐)策略
      • noeviction:默认策略,不淘汰任何 key,直接返回错误
      • allkeys-lru:在所有的 key 中,使用 LRU 算法淘汰部分 key
      • allkeys-lfu:在所有的 key 中,使用 LFU 算法淘汰部分 key,该算法于 Redis 4.0 新增
      • allkeys-random:在所有的 key 中,随机淘汰部分 key
      • volatile-lru:在设置了过期时间的 key 中,使用 LRU 算法淘汰部分 key
      • volatile-lfu:在设置了过期时间的 key 中,使用 LFU 算法淘汰部分 key,该算法于 Redis 4.0 新增
      • volatile-random:在设置了过期时间的 key 中,随机淘汰部分 key
      • volatile-ttl:在设置了过期时间的 key 中,挑选 TTL(time to live,剩余时间)短的 key 淘汰

主从、哨兵、集群

  • 主从复制
    • 复制的流程(Redis6.0)
      1. 开启主从复制。通常有以下三种方式:
        • 在 slave 直接执行命令:slaveof <masterip> <masterport>
        • 在 slave 配置文件中加入:slaveof <masterip> <masterport>
        • 使用启动命令:--slaveof <masterip> <masterport>
      2. 建立套接字(socket)连接
        • slave 将根据指定的 IP 地址和端口,向 master 发起套接字(socket)连接,master 在接受(accept) slave 的套接字连接之后,为该套接字创建相应的客户端状态,此时连接建立完成。
      3. 发送 PING 命令
        • slave 向 master 发送一个 PING 命令,以检査套接字的读写状态是否正常、 master 能否正常处理命令请求。
      4. 身份验证
        • slave 向 master 发送 AUTH password 命令来进行身份验证。
      5. 发送端口信息
        • 在身份验证通过后后,slave 将向 master 发送自己的监听端口号, master 收到后记录在 slave 所对应的客户端状态的 slave_listening_port 属性中。
      6. 发送 IP 地址
        • 如果配置了 slave_announce_ip,则 slave 向 master 发送 slave_announce_ip 配置的 IP 地址, master 收到后记录在 slave 所对应的客户端状态的 slave_ip 属性。
          • 该配置是用于解决服务器返回内网 IP 时,其他服务器无法访问的情况。可以通过该配置直接指定公网 IP。
      7. 发送 CAPA
        • CAPA 全称是 capabilities,这边表示的是同步复制的能力。slave 会在这一阶段发送 capa 告诉 master 自己具备的(同步)复制能力, master 收到后记录在 slave 所对应的客户端状态的 slave_capa 属性。
      8. 数据同步
        • slave 将向 master 发送 PSYNC 命令, master 收到该命令后判断是进行部分重同步还是完整重同步,然后根据策略进行数据的同步。
      9. 命令传播
        • 当完成了同步之后,就会进入命令传播阶段,这时 master 只要一直将自己执行的写命令发送给 slave ,而 slave 只要一直接收并执行 master 发来的写命令,就可以保证 master 和 slave 一直保持一致了。
      • Redis_复制的流程
      • Redis_主从复制方案
    • 相关问题
      • Redis 主从复制延迟问题
        • 将主从模式更换为哨兵模式则无需自己去做监控
      • 脑裂导致数据丢失
        • Redis 已经提供了两个配置项来限制主库的请求处理,分别是 min-slaves-to-write 和 min-slaves-max-lag。
          • min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;
          • min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)。
        • 我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的请求了。
  • 哨兵
    • 哨兵(Sentinel)是 Redis 的高可用性解决方案:由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器。
    • Sentinel 可以在被监视的主服务器进入下线状态时,自动将下线主服务器的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
    • 主要功能
      • 客户端可以通过哨兵节点 + masterName 获取主节点信息,在这里哨兵起到的作用就是配置提供者。
      • 哨兵故障检测
        • 检查主观下线状态
          • 在默认情况下,Sentinel 会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他 Sentinel 在内)发送 PING 命令,并通过实例返回的 PING 命令回复来判断实例是否在线。
          • 如果一个实例在 down-after-miliseconds 毫秒内,连续向 Sentinel 返回无效回复,那么 Sentinel 会修改这个实例所对应的实例结构,在结构的 flags 属性中设置 SRI_S_DOWN 标识,以此来表示这个实例已经进入主观下线状态。
        • 检查客观下线状态
          • 当 Sentinel 将一个主服务器判断为主观下线之后,为了确定这个主服务器是否真的下线了,它会向同样监视这一服务器的其他 Sentinel 进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。
          • 当 Sentinel 从其他 Sentinel 那里接收到足够数量(quorum,可配置)的已下线判断之后,Sentinel 就会将服务器置为客观下线,在 flags 上打上 SRI_O_DOWN 标识,并对主服务器执行故障转移操作。
      • 哨兵故障转移流程
        1. 发起一次选举,选举出领头 Sentinel
        2. 领头 Sentinel 在已下线主服务器的所有从服务器里面,挑选出一个从服务器,并将其升级为新的主服务器。
        3. 领头 Sentinel 将剩余的所有从服务器改为复制新的主服务器。
        4. 领头 Sentinel 更新相关配置信息,当这个旧的主服务器重新上线时,将其设置为新的主服务器的从服务器。
    • 架构
      • Redis_哨兵架构图
  • 集群模式(Cluster)
    • 哨兵模式最大的缺点就是所有的数据都放在一台服务器上,无法较好的进行水平扩展。
    • 为了解决哨兵模式存在的问题,集群模式应运而生。在高可用上,集群基本是直接复用的哨兵模式的逻辑,并且针对水平扩展进行了优化。
    • 集群模式具备的特点
      • 采取去中心化的集群模式,将数据按槽存储分布在多个 Redis 节点上。集群共有 16384 个槽,每个节点负责处理部分槽。
      • 使用 CRC16 算法来计算 key 所属的槽:crc16(key,keylen) & 16383
      • 所有的 Redis 节点彼此互联,通过 PING-PONG 机制来进行节点间的心跳检测。
        • 交换的数据信息,由消息体和消息头组成。消息体无外乎是一些节点标识啊,IP 啊,端口号啊,发送时间啊。这与本文关系不是太大。我们来看消息头,结构如下
          • Redis_节点间消息结构
            • 注意看红框的内容,type 表示消息类型。另外,消息头里面有个 myslots 的 char 数组,长度为 16383/8,这其实是一个 bitmap,每一个位代表一个槽,如果该位为 1,表示这个槽是属于这个节点的。
            • 在消息头中,最占空间的是 myslots[CLUSTER_SLOTS/8]。这块的大小是: 16384÷8÷1024=2kb
            • 那在消息体中,会携带一定数量的其他节点信息用于交换。那这个其他节点的信息,到底是几个节点的信息呢?
              • 约为集群总节点数量的 1/10,至少携带 3 个节点的信息。 这里的重点是:节点数量越多,消息体内容越大。
        • 发送规律
          1. 每秒会随机选取 5 个节点,找出最久没有通信的节点发送 ping 消息
          2. 每 100 毫秒(1 秒 10 次)都会扫描本地节点列表,如果发现节点最近一次接受 pong 消息的时间大于 cluster-node-timeout/2 则立刻发送 ping 消息
          • 每秒单节点发出 ping 消息数量为数量=1+10*num,num=(node.pong_received>cluster_node_timeout/2)的数量
      • 分片内采用一主多从保证高可用,并提供复制和故障恢复功能。在实际使用中,通常会将主从分布在不同机房,避免机房出现故障导致整个分片出问题。
      • 客户端与 Redis 节点直连,不需要中间代理层(proxy)。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
    • 架构
      • Redis_集群架构图
    • 集群选举
      1. 当从节点发现自己正在复制的主节点进入已下线状态时,会发起一次选举:将 currentEpoch(配置纪元)加 1,然后向集群广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。
      2. 其他节点收到消息后,会判断是否要给发送消息的节点投票,判断流程如下:
        1. 当前节点是 slave,或者当前节点是 master,但是不负责处理槽,则当前节点没有投票权,直接返回。
        2. 请求节点的 currentEpoch 小于当前节点的 currentEpoch,校验失败返回。因为发送者的状态与当前集群状态不一致,可能是长时间下线的节点刚刚上线,这种情况下,直接返回即可。
        3. 当前节点在该 currentEpoch 已经投过票,校验失败返回。
        4. 请求节点是 master,校验失败返回。
        5. 请求节点的 master 为空,校验失败返回。
        6. 请求节点的 master 没有故障,并且不是手动故障转移,校验失败返回。因为手动故障转移是可以在 master 正常的情况下直接发起的。
        7. 上一次为该 master 的投票时间,在 cluster_node_timeout 的 2 倍范围内,校验失败返回。这个用于使获胜从节点有时间将其成为新主节点的消息通知给其他从节点,从而避免另一个从节点发起新一轮选举又进行一次没必要的故障转移
        8. 请求节点宣称要负责的槽位,是否比之前负责这些槽位的节点,具有相等或更大的 configEpoch,如果不是,校验失败返回。
        9. 如果通过以上所有校验,那么主节点将向要求投票的从节点返回一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示这个主节点支持从节点成为新的主节点。
      3. 每个参与选举的从节点都会接收 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,并根据自己收到了多少条这种消息来统计自己获得了多少个主节点的支持。
      4. 如果集群里有 N 个具有投票权的主节点,那么当一个从节点收集到大于等于 N/2+1 张支持票时,这个从节点就会当选为新的主节点。因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有 N个主节点进行投票,那么具有大于等于 N/2+1 张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
      5. 如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。
      • 这个选举新主节点的方法和选举领头 Sentinel 的方法非常相似,因为两者都是基于 Raft 算法的领头选举(leader election)方法来实现的。
    • 相关问题
      • 如何保证集群在线扩容的安全性?(Redis 集群要增加分片,槽的迁移怎么保证无损)
        • Redis 使用了 ASK 错误来保证在线扩容的安全性。
        • 在槽的迁移过程中若有客户端访问,依旧先访问源节点,源节点会先在自己的数据库里面査找指定的键,如果找到的话,就直接执行客户端发送的命令。
        • 如果没找到,说明该键可能已经被迁移到目标节点了,源节点将向客户端返回一个 ASK 错误,该错误会指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令,从而获取到结果。
          • ASK 错误
            • 在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
            • 当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时。源节点会先在自己的数据库里面査找指定的键,如果找到的话,就直接执行客户端发送的命令。
            • 否则,这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个 ASK 错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令,从而获取到结果。
      • 为什么 Redis 集群有 16384 个槽?
        1. 如果槽位为 65536,发送心跳信息的消息头达 8k,发送的心跳包过于庞大。
          • 在消息头中,最占空间的是 myslots[CLUSTER_SLOTS/8]。 当槽位为 65536 时,这块的大小是: 65536÷8÷1024=8kb 因为每秒钟,redis 节点需要发送一定数量的 ping 消息作为心跳包,如果槽位为 65536,这个 ping 消息的消息头太大了,浪费带宽。
        2. Redis 的集群主节点数量基本不可能超过 1000 个。
          • 集群节点越多,心跳包的消息体内携带的数据越多。如果节点过 1000 个,也会导致网络拥堵。因此 Redis 作者,不建议 redis cluster 节点数量超过 1000 个。那么,对于节点数在 1000 以内的 redis cluster 集群,16384 个槽位够用了。没有必要拓展到 65536 个。
        3. 槽位越小,节点少的情况下,压缩比高
          • Redis 主节点的配置信息中,它所负责的哈希槽是通过一张 bitmap 的形式来保存的,在传输过程中,会对 bitmap 进行压缩,但是如果 bitmap 的填充率slots / N很高的话(N 表示节点数),bitmap 的压缩率就很低。如果节点数很少,而哈希槽数量很多的话,bitmap 的压缩率就很低。

            事务

  • Redis 的事务并不推荐在实际中使用,如果要使用事务,推荐使用 Lua 脚本,Redis 会保证一个 Lua 脚本里的所有命令的原子性。

常用方式

  • 缓存
    • 可能遇到的问题:
      • 缓存雪崩
        • 同一时刻大量缓存失效
        • 处理方法
          • 缓存数据增加过期标记
          • 设置不同的缓存失效时间
          • 双层缓存策略 C1 为短期,C2 为⻓期
          • 定时更新策略
      • 缓存穿透
        • 频繁请求查询系统中不存在的数据导致
        • 处理方法
          • 对请求参数进行校验,不合理直接返回
          • 查询不到的数据也放到缓存,value 为空,如 set -999 “”
          • 使用布隆过滤器,快速判断 key 是否在数据库中存在,不存在直接返回
            • 布隆过滤器
              • 布隆过滤器的特点是判断不存在的,则一定不存在;判断存在的,大概率存在,但也有小概率不存在。并且这个概率是可控的,我们可以让这个概率变小或者变高,取决于用户本身的需求。
      • 缓存击穿
        • 设置了过期时间的 key,承载着高并发,是一种热点数据。从这个 key 过期到重新从 MySQL 加载数据放到缓存的一段时间,大量的请求有可能把数据库打死。缓存雪崩是指大量缓存失效,缓存击穿是指热点数据的缓存失效
        • 处理方法
          • 设置 key 永远不过期,或者快过期时,通过另一个异步线程重新设置 key
          • 当从缓存拿到的数据为 null,重新从数据库加载数据的过程上锁
  • 分布式锁
    • 涉及操作
      • set + lua 脚本
    • 看门狗策略
      • Redis_分布式锁_看门狗策略
    • 存在的问题
      • 步骤
        1. 线程 1 首先获取锁成功,将键值对写入 redis 的 master 节点
        2. 在 redis 将该键值对同步到 slave 节点之前,master 发生了故障
        3. redis 触发故障转移,其中一个 slave 升级为新的 master
        4. 此时新的 master 并不包含线程 1 写入的键值对,因此线程2尝试获取锁也可以成功拿到锁
        5. 此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据
      • 解决方法和思路
        • Zookeeper 实现的分布式锁
        • Redlock
          • 方案思路
            • 假设我们有 N 个 Redis 主节点,例如 N = 5,这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,为了取到锁,客户端应该执行以下操作:
              1. 获取当前时间,以毫秒为单位。
              2. 依次尝试从 5 个实例,使用相同的 key 和随机值(例如UUID)获取锁。当向 Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁。
              3. 客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功。
              4. 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤3计算的结果)。
              5. 如果由于某些原因未能获得锁(无法在至少 N/2+1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些 Redis 实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
          • 存在的问题
            • 该方案的成本似乎有点高,需要使用5个实例;
            • 严重依赖系统时钟。如果线程 1 从 3 个实例获取到了锁,但是这 3 个实例中的某个实例的系统时间走的稍微快一点,则它持有的锁会提前过期被释放,当他释放后,此时又有 3 个实例是空闲的,则线程 2 也可以获取到锁,则可能出现两个线程同时持有锁了。
            • 如果线程 1 从 3 个实例获取到了锁,但是万一其中有 1 台重启了,则此时又有 3 个实例是空闲的,则线程 2 也可以获取到锁,此时又出现两个线程同时持有锁了。
  • 排行榜
    • 涉及操作
      • zset
  • 计数
    • 涉及操作
      • incrby
  • 消息队列
    • 涉及操作
      • stream
  • 地理位置
    • 涉及操作
      • geo
  • 访客统计
    • 涉及操作
      • hyperloglog

性能优化

  • 缩短键值对的存储长度
    • 键值对的长度是和性能成反比
  • 使用 lazy free 特性
  • 设置键值的过期时间
  • 禁用长耗时的查询命令
    • Redis 绝大多数读写命令的时间复杂度都在 $O(1)$ 到 $O(N)$ 之间
    • 要避免 $O(N)$ 命令对 Redis造 成影响,可以从以下几个方面入手改造:
      • 禁止使用 keys 命令;
      • 避免一次查询所有的成员,要使用 scan 命令进行分批的、游标式的遍历;
      • 通过机制严格控制 Hash、Set、Sorted Set 等结构的数据大小;
      • 将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力;
      • 删除一个大数据的时候,可能会需要很长时间,所以建议用异步删除的方式 unlink,它会启动一个新的线程来删除目标数据,而不阻塞 Redis 的主线程。
    • 使用 slowlog 优化耗时命令
    • 使用 Pipeline 批量操作数据
      • Pipeline(管道技术)是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。
    • 避免大量数据同时失效
      • 如果在大型系统中有大量缓存在同一时间同时过期,那么会导致 Redis 循环多次持续扫描过期字典,直到过期字典中过期键值被删除的比较稀疏为止,而在整个执行过程中会导致 Redis 的读写出现明显的卡顿,卡顿的另一种原因是内存管理器需要频繁回收内存页,因此也会消耗一定的 CPU。
      • 为了避免这种卡顿现象的产生,我们需要预防大量的缓存在同一时刻一起过期,最简单的解决方案就是在过期时间的基础上添加一个指定范围的随机数。
    • 客户端使用优化
      • 在客户端的使用上我们除了要尽量使用 Pipeline 的技术外,还需要注意尽量使用 Redis 连接池,而不是频繁创建销毁 Redis 连接,这样就可以减少网络传输次数和减少了非必要调用指令。
    • 限制 Redis 内存大小
    • 使用物理机而非虚拟机
    • 检查数据持久化策略
    • 禁用 THP 特性
      • Linux kernel 在 2.6.38 内核增加了 Transparent Huge Pages(THP)特性,支持大内存页 2MB 分配,默认开启。
      • 当开启了 THP 时,fork 的速度会变慢,fork 之后每个内存页从原来 4KB 变为 2MB,会大幅增加重写期间父进程内存消耗。同时每次写命令引起的复制内存页单位放大了 512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。
    • 使用分布式架构来增加读写速度
      • 主从同步
      • 哨兵模式
      • Redis Cluster 集群

相关问题

  • Redis 单线程为什么执行速度这么快?
    • 纯内存操作,避免大量访问数据库,减少直接读取磁盘数据,Redis 将数据储存在内存里面,读写数据的时候都不会受到硬盘 I/O 速度的限制,所以速度快
    • 单线程操作,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换 而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
    • 采用了非阻塞 I/O 多路复用机制
  • Redis 是单线程还是多线程?
    • redis 4.0 之前,redis 是完全单线程的。
    • redis 4.0 时,redis 引入了多线程,但是额外的线程只是用于后台处理,例如:删除对象。核心流程还是完全单线程的。这也是为什么有些人说 4.0 是单线程的,因为他们指的是核心流程是单线程的。
      • 这边的核心流程指的是 redis 正常处理客户端请求的流程,通常包括:接收命令、解析命令、执行命令、返回结果等。
    • redis 6.0 版本又一次引入了多线程概念,与 4.0 不同的是,这次的多线程会涉及到上述的核心流程。
      • redis 6.0 中,多线程主要用于网络 I/O 阶段,也就是接收命令和写回结果阶段,而在执行命令阶段,还是由单线程串行执行。由于执行时还是串行,因此无需考虑并发安全问题。
      • 值得注意的时,redis 中的多线程组不会同时存在“读”和“写”,这个多线程组只会同时“读”或者同时“写”。
      • 处理流程
        1. 当有读事件到来时,主线程将该客户端连接放到全局等待读队列
        2. 读取数据:
          1. 主线程将等待读队列的客户端连接通过轮询调度算法分配给 I/O 线程处理;
          2. 同时主线程也会自己负责处理一个客户端连接的读事件;
          3. 当主线程处理完该连接的读事件后,会自旋等待所有 I/O 线程处理完毕
        3. 命令执行:主线程按照事件被加入全局等待读队列的顺序(这边保证了执行顺序是正确的),串行执行客户端命令,然后将客户端连接放到全局等待写队列
        4. 写回结果:跟等待读队列处理类似,主线程将等待写队列的客户端连接使用轮询调度算法分配给 I/O 线程处理,同时自己也会处理一个,当主线程处理完毕后,会自旋等待所有 I/O 线程处理完毕,最后清空队列。
        • Redis6_多线程
  • 服务重启时如何加载?
    • Redis_数据载入流程
  • Redis 怎么保证高可用、有哪些集群模式?
    • 主从复制
    • 哨兵模式
    • 集群模式
  • Redis 和 Memcached 的比较
    1. 数据结构:memcached 支持简单的 key-value 数据结构,而 redis 支持丰富的数据结构:String、List、Set、Hash、SortedSet 等。
    2. 数据存储:memcached 和 redis 的数据都是全部在内存中。
    3. 持久化:memcached 不支持持久化,redis 支持将数据持久化到磁盘。
    4. 灾难恢复:实例挂掉后,memcached 数据不可恢复,redis 可通过 RDB、AOF 恢复,但是还是会有数据丢失问题。
    5. 事件库:memcached 使用 Libevent 事件库,redis 自己封装了简易事件库 AeEvent。
    6. 过期键删除策略:memcached 使用惰性删除,redis 使用惰性删除+定期删除。
    7. 内存驱逐(淘汰)策略:memcached 主要为 LRU 算法,redis 当前支持 8 种淘汰策略。
    8. 性能比较
      • 按“CPU 单核” 维度比较:由于 Redis 只使用单核,而 Memcached 可以使用多核,所以在比较上:在处理小数据时,平均每一个核上 Redis 比 Memcached 性能更高,而在 100k 左右的大数据时, Memcached 性能要高于 Redis。
      • 按“实例”维度进行比较:由于 Memcached 多线程的特性,在 Redis 6.0 之前,通常情况下 Memcached 性能是要高于 Redis 的,同时实例的 CPU 核数越多,Memcached 的性能优势越大。
  • 如何保证数据库和缓存的数据一致性?
    • 无论是先操作数据库,还是先操作缓存,都会存在脏数据的情况
      • 先操作数据库
        • Redis_缓存一致性_先操作数据库
        • 可能存在的脏数据时间范围:更新数据库后,失效缓存前。这个时间范围很小,通常不会超过几毫秒。
      • 先操作缓存
        • Redis_缓存一致性_先操作缓存
        • ​可能存在的脏数据时间范围:更新数据库后,下一次对该数据的更新前。这个时间范围不确定性很大,情况如下:
          • 如果下一次对该数据的更新马上就到来,那么会失效缓存,脏数据的时间就很短。
          • 如果下一次对该数据的更新要很久才到来,那这期间缓存保存的一直是脏数据,时间范围很长。
      • 通过上述案例可以看出,先操作数据库和先操作缓存都会存在脏数据的情况。但是相比之下,先操作数据库,再操作缓存是更优的方式,即使在并发极端情况下,也只会出现很小量的脏数据。
      • 为什么是让缓存失效,而不是更新缓存?
        • 更新缓存
          • Redis_缓存一致性_更新缓存
          • 数据库中的数据是请求B的,缓存中的数据是请求A的,数据库和缓存存在数据不一致。
        • 失效(删除)缓存
          • Redis_缓存一致性_删除缓存
          • 由于是删除缓存,所以不存在数据不一致的情况。
    • 由于数据库和缓存是两个不同的数据源,要保证其数据一致性,其实就是典型的分布式事务场景,可以引入分布式事务来解决,常见的有:2PC、TCC、MQ 事务消息等。
    • 但是引入分布式事务必然会带来性能上的影响,这与我们当初引入缓存来提升性能的目的是相违背的。
    • 所以在实际使用中,通常不会去保证缓存和数据库的强一致性,而是做出一定的牺牲,保证两者数据的最终一致性。
    • 保证数据库和缓存数据最终一致性的常用方案如下:
      1. 更新数据库,数据库产生 binlog。
      2. 监听和消费 binlog,执行失效缓存操作。
      3. 如果步骤 2 失效缓存失败,则引入重试机制,将失败的数据通过MQ方式进行重试,同时考虑是否需要引入幂等机制。
  • Redis 热 key 查找与处理
    • 查找
      • 使用 Redis 内置功能发现大 Key 及热 Key
        • 通过 Redis 内置命令对目标 Key 进行分析
          • 使用 debug object 命令对 Key 进行分析。
          • Redis 自 4.0 起提供了 MEMORY USAGE 命令来帮助分析 Key 的内存占用,相对 debug object 它的执行代价更低,但由于其时间复杂度为 $O(N)$ 因此在分析大 Key 时仍有阻塞风险。
        • 通过 Redis 官方客户端 redis-cli 的 bigkeys 参数发现大 Key
        • 通过业务层定位热 Key
          • 可以通过在业务层增加相应的代码对 Redis 的访问进行记录并异步汇总分析
        • 使用 monitor 命令在紧急情况时找出热 Key
          • Redis 的 monitor 命令能够忠实的打印 Redis 中的所有请求,包括时间信息、Client 信息、命令以及 Key 信息。在发生紧急情况时,我们可以通过短暂执行 monitor 命令并将输出重定向至文件,在关闭 monitor 命令后通过对文件中请求进行归类分析即可找出这段时间中的热 Key。
          • 由于 monitor 命令对 Redi s的 CPU、内存、网络资源均有一定的占用。因此,对于一个已处于高压状态的 Redis,monitor 可能会起到雪上加霜的作用。同时,这种异步收集并分析的方案的时效性较差,并且由于分析的精确度取决于 monitor 的执行时间,因此在多数无法长时间执行该命令的线上场景中本方案的精确度也不够好。
        • hotkeys 参数,redis 4.0.3 提供了 redis-cli 的热点 key 发现功能,执行 redis-cli 时加上 -hotkeys 选项即可。但是该参数在执行的时候,如果 key 比较多,执行起来比较慢。
      • 使用开源工具发现大 Key
        • 使用 redis-rdb-tools 工具以定制化方式找出大 Key
          • 该工具能够对 Redis 的 RDB 文件进行定制化的分析,但由于分析 RDB 文件为离线工作,因此对线上服务不会有任何影响,这是它的最大优点但同时也是它的最大缺点:离线分析代表着分析结果的较差时效性。对于一个较大的 RDB 文件,它的分析可能会持续很久很久。
    • 处理
      • 大 Key 的常见处理办法
        • 对大 Key 进行拆分
          • 如将一个含有数万成员的 HASH Key 拆分为多个 HASH Key,并确保每个 Key 的成员数量在合理范围,在 Redis Cluster 结构中,大 Key 的拆分对 node 间的内存平衡能够起到显著作用。
        • 对大 Key 进行清理
          • 将不适合 Redis 能力的数据存放至其它存储,并在 Redis 中删除此类数据。
            • Redis 自 4.0 起提供了 UNLINK 命令,该命令能够以非阻塞的方式缓慢逐步的清理传入的 Key,通过 UNLINK,你可以安全的删除大 Key 甚至特大 Key。
        • 时刻监控 Redis 的内存水位
          • 在大 Key 产生问题前发现它并进行处理是保持服务稳定的重要手段。我们可以通过监控系统并设置合理的 Redis 内存报警阈值来提醒我们此时可能有大 Key 正在产生,如:Redis 内存使用率超过 70%,Redis 内存 1 小时内增长率超过 20% 等。
        • 对失效数据进行定期清理
          • 例如我们会在 HASH 结构中以增量的形式不断写入大量数据而忽略了这些数据的时效性,这些大量堆积的失效数据会造成大 Key 的产生,可以通过定时任务的方式对失效数据进行清理。在此类场景中,建议使用 HSCAN 并配合 HDEL 对失效数据进行清理,这种方式能够在不阻塞的前提下清理无效数据。
      • 热 Key 的常见处理办法
        • 在 Redis Cluster 结构中对热 Key 进行复制
          • 在 Redis Cluster 中,热 Key 由于迁移粒度问题造成请求无法打散使单一node 的压力无法下降。此时可以将对应热 Key 进行复制并迁移至其他 node,例如为热 Key foo 复制出 3 个内容完全一样的 Key 并名为 foo2,foo3,foo4,然后将这三个 Key 迁移到其他 node 来解决单一 node 的热 Key 压力。
          • 该方案的缺点在于代码需要联动修改,同时,Key 一变多带来了数据一致性挑战:由更新一个 Key 演变为需要同时更新多个 Key,在很多时候,该方案仅建议用来临时解决当前的棘手问题。
        • 使用读写分离架构
          • 如果热 Key 的产生来自于读请求,那么读写分离是一个很好的解决方案。在使用读写分离架构时可以通过不断的增加从节点来降低每个 Redis 实例中的读请求压力。

基础

三范式

  1. 第一范式:确保每列的原子性
  2. 第二范式:非主键列不存在对主键的部分依赖(要求每个表只描述一件事情)
  3. 第三范式:满足第二范式,并且表中的列不存在对非主键列的传递依赖

SQL

  • 分类
    • DQL
      • Data Query Language,数据查询语言
      • 最常用的为保留字 SELECT,并且常与 FROM 子句、WHERE 子句组成查询 SQL 查询语句。
    • DML
      • Data Manipulation Language,数据操纵语言
      • 主要用来对数据库的数据进行一些操作,常用的就是 INSERT、UPDATE、DELETE。
    • DPL
      • 事务处理语言
      • 事务处理语句能确保被 DML 语句影响的表的所有行及时得以更新。DPL 语句包括 BEGIN TRANSACTION、COMMIT 和 ROLLBACK。
    • DCL
      • 数据控制语言
      • 通过 GRANT 和 REVOKE,确定单个用户或用户组对数据库对象的访问权限。
    • DDL
      • 数据定义语言
      • 常用的有 CREATE 和 DROP,用于在数据库中创建新表或删除表,以及为表加入索引等。
    • CCL
      • 指针控制语言
      • 它的语句,像 DECLARE CURSOR、FETCH INTO 和 UPDATE WHERE CURRENT 用于对一个或多个表单独行的操作。

引擎

  • MyISAM
    • 不支持事务
    • 不支持外键
    • 是非聚集索引,也是使用 B+ Tree 作为索引结构,索引和数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
      • MyISAM索引文件
    • 用一个变量保存了整个表的行数
    • 支持全文索引
    • 支持表级锁
    • 可以没有唯一索引
    • Myisam 存储文件有 frm、MYD、MYI
      • frm 是表定义文件
      • myd 是数据文件
      • myi 是索引文件
  • InnoDB
    • 支持事务
    • 支持外键
    • 是聚集索引,使用 B+ Tree 作为索引结构,数据文件是和(主键)索引绑在一起的(表数据文件本身就是按 B+ Tree 组织的一个索引结构),必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。
      • InnoDB索引文件
      • 相关问题
        • 为什么 MySQL 用 B+ 树做索引而不用 B-树或红黑树?
          • 在大规模数据存储的时候,红黑树往往出现由于树的深度过大而造成磁盘 IO 读写过于频繁,进而导致效率低下的情况。
            • 磁盘读写 IO 跟树的深度有关系,磁盘一次 IO 读取的数据多能做的事也将更多。(我们知道要获取磁盘上数据,必须先通过磁盘移动臂移动到数据所在的柱面,然后找到指定盘面,接着旋转盘面找到数据所在的磁道,最后对数据进行读写。磁盘IO代价主要花费在查找所需的柱面上,树的深度过大会造成磁盘 IO 频繁读写。)根据磁盘查找存取的次数往往由树的高度所决定,所以,只要我们通过某种较好的树结构减少树的结构尽量减少树的高度,B 树可以有多个子女,从几十到上千,但是降低树的高度。
          • 数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。为了达到这个目的,在实际实现 B-Tree 还需要使用如下技巧:每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个 node 只需一次 I/O。
            • B-Tree 有许多变种,其中最常见的是 B+Tree,例如 MySQL 就普遍使用 B+Tree 实现其索引结构。
    • 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描
      • 想逛问题
        • 为什么 InnoDB 没有保存行数呢?
          • 因为 InnoDB 的事务特性,在同一时刻表中的行数对于不同的事务而言是不一样的,因此 count 统计会计算对于当前事务而言可以统计到的行数,而不是将总行数储存起来方便快速查询。
    • 不支持全文索引(5.7以后的InnoDB支持全文索引了)
    • 支持表、行(默认)级锁
    • 须有唯一索引(如主键)(用户没有指定的话会自己找/生产一个隐藏列 Row_id 来充当默认主键)
    • Innodb 存储文件有 frm、ibd
      • frm 是表定义文件
      • ibd 是数据文件
    • InnoDB 的内存结构和磁盘结构
      • MySQL_InnoDB_内存结构和磁盘结构
      • 内存结构
        • 主要分为:Buffer Pool、Change Buffer、Adaptive HashIndex、(redo)log buffer
        • Innodb buffer pool
          • MySQL InnoDB 缓冲池,CPU 读取或者写入数据时,不直接和低速的磁盘打交道,直接和缓冲区进行交互,从而解决了因为磁盘性能慢导致的数据库性能差的问题,弥补了两者之间的速度差异。
          • Buffer Pool 中更新的数据未刷新到磁盘中,该内存页称之为脏页。最终脏页的数据会刷新到磁盘中,将磁盘中的数据覆盖。
          • MySQL_语句执行内部流程
          • Buffer Pool 预热
            • MySQL 在重启后,Buffer Pool 里面没有什么数据,这个时候业务上对数据库的数据操作,MySQL 就只能从磁盘中读取数据到内存中,这个过程可能需要很久才能是内存中的数据是业务频繁使用的。Buffer Pool 中数据从无到业务频繁使用热数据的过程称之为预热。所以在预热这个过程中,MySQL 数据库的性能不会特别好,并且 Buffer Pool 越大,预热过程越长。
            • 为了减短这个预热过程,在 MySQL 关闭前,把 Buffer Pool 中的页面信息保存到磁盘,等到 MySQL 启动时,再根据之前保存的信息把磁盘中的数据加载到 Buffer Pool 中即可。
            • 结构
              • MySQL_buffer_pool_结构
        • Change Buffer
          • 在更新数据的时候,如果这个数据页不是唯一索引(索引的值不允许重复),也就不需要从磁盘加载索引页判断数据是不是重复(唯一性检查)。这种情况下可以先把修改记录在内存的缓冲池中,从而提升更新语句(Insert、Delete、Update)的执行速度。
          • 这一块区域就是 Change Buffer。5.5 之前叫 Insert Buffer 插入缓冲,现在也能支持 delete 和 update,最后把 Change Buffer 记录到数据页的操作叫做 merge。
            • 相关问题
              • 什么时候发生 merge?
                • 在访问这个数据页的时候、或者通过后台线程、或者数据库 shut down、redo log 写满时触发
        • Adaptive Hash Index
          • InnoDB 存储引擎会监控对表上索引的查找,如果观察到建立哈希索引可以带来速度的提升,则建立哈希索引,所以称之为自适应(adaptive)的。自适应哈希索引通过缓冲池的 B+ 树构造而来,因此建立的速度很快。而且不需要将整个表都建哈希索引,InnoDB 存储引擎会自动根据访问的频率和模式来为某些页建立哈希索引。
        • (redo)Log Buffer
          • 如果 Buffer Pool 里面的脏页还没有刷入磁盘时,数据库宕机或者重启,这些数据丢失。如果写操作写到一半,甚至可能会破坏数据文件导致数据库不可用,怎么办?
          • 为了避免这个问题,InnoDB 把所有的修改操作专门写入一个日志文件,并且在数据库启动时从这个文件进行恢复操作(实现 crash-safe)——用它来实现事务的持久性。
          • 这个文件就是磁盘的 redo log(叫做重做日志)。
      • 磁盘结构
        • 系统表空间 system tablespace
        • 独占表空间 file-per-table tablespaces
          • 我们可以让每张表独占一个表空间。这个开关通过 innodb_file_per_table 设置,默认开启。
        • 通用表空间 general tablespaces
          • 通用表空间也是一种共享的表空间,跟 ibdata1 类似。可以创建一个通用的表空间,用来存储不同数据库的表,数据路径和文件可以自定义
        • 临时表空间 temporary tablespaces
          • 存储临时表的数据,包括用户创建的临时表,和磁盘的内部临时表。对应数据目录下的 ibtmp1 文件。当数据服务器正常关闭时,该表空间被删除,下次重新产生。
        • redo log
        • undo Log
    • MySQL_Innodb架构

数据类型

  • 五大类
    • 整数类型:BIT、BOOL、TINY INT、SMALL INT、MEDIUM INT、INT、BIG INT
    • 浮点数类型:FLOAT、DOUBLE、DECIMAL
      • MySQL_数据类型_整数类型_浮点数类型
    • 字符串类型:CHAR、VARCHAR、TINY TEXT、TEXT、MEDIUM TEXT、LONGTEXT、TINY BLOB、BLOB、MEDIUM BLOB、LONG BLOB
      • MySQL_数据类型_字符串类型
    • 日期类型:Date、DateTime、TimeStamp、Time、Year
      • MySQL_数据类型_日期类型
    • 其他数据类型:BINARY、VARBINARY、ENUM、SET、Geometry、Point、MultiPoint、LineString、MultiLineString、Polygon、GeometryCollection 等

索引

  • InnoDB
    • 分类
      • 物理分类
        • 聚集索引
          • 聚集索引就是以主键创建的索引
          • 每个表只能有一个聚簇索引,因为一个表中的记录只能以一种物理顺序存放,实际的数据⻚只能按照一颗 B+ 树进行排序
          • 表记录的排列顺序和与索引的排列顺序一致
          • 聚集索引存储记录是物理上连续存在
          • 聚簇索引主键的插入速度要比非聚簇索引主键的插入速度慢很多
          • 聚簇索引适合排序,非聚簇索引不适合用在排序的场合,因为聚簇索引叶节点本身就是索引和 数据按相同顺序放置在一起,索引序即是数据序,数据序即是索引序,所以很快。非聚簇索引叶节点是保留了一个指向数据的指针,索引本身当然是排序的,但是数据并未排序,数据查询的时候需要消耗额外更多的 I/O,所以较慢
          • 更新聚集索引列的代价很高,因为会强制 innodb 将每个被更新的行移动到新的位置
        • 非聚集索引
          • 除了主键以外的索引
          • 聚集索引的叶节点就是数据节点,而非聚簇索引的叶节点仍然是索引节点,并保留一个链接指向对应数据块
          • 聚簇索引适合排序,非聚簇索引不适合用在排序的场合
          • 聚集索引存储记录是物理上连续存在,非聚集索引是逻辑上的连续。
      • 逻辑分类
        • 唯一索引
        • 主键索引
        • 普通索引
        • 全文索引
        • 联合索引
          • 最左匹配原则
          • 索引覆盖
            • 在查询里,联合索引已经“覆盖了”我们的查询需求,故称为覆盖索引。从辅助索引中就能直接得到查询结果,而不需要回表到聚簇索引中进行再次查询,所以可以减少搜索次数(不需要从辅助索引树回表到聚簇索引树),或者说减少IO操作(通过辅助索引树可以一次性从磁盘载入更多节点),从而提升性能。
          • 索引下推
            • 例子
              • 在开始之前先先准备一张用户表(user),其中主要几个字段有:id、name、age、address。建立联合索引(name,age)。
              • 执行SELECT * from user where name like '陈%' and age=20
            • MySQL5.6 之前的版本
              • MySQL索引下推_1
              • 会忽略 age 这个字段,直接通过 name 进行查询,在索引课树上查找到了两个结果,id 分别为 2、1,然后拿着取到的 id 值一次次的回表查询,因此这个过程需要回表两次。
            • MySQL5.6 及之后版本
              • MySQL索引下推_2
              • 并没有忽略 age 这个字段,而是在索引内部就判断了 age 是否等于 20,对于不等于 20 的记录直接跳过,因此在 (name,age) 这棵索引树中只匹配到了一个记录,此时拿着这个 id 去主键索引树中回表查询全部数据,这个过程只需要回表一次。
    • 优化使用
      • 对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
      • 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描
      • 不在索引上进行任何操作
        • 索引上进行计算、函数、类型转换等操作都会导致索引从当前位置(联合索引多个字段,不影响前面字段的匹配)失效,可能会进行全表扫描。
        • 隐式类型转换
          • 在查询时一定要注意字段类型问题,比如a字段时字符串类型的,而匹配参数用的是int类型,此时就会发生隐式类型转换,相当于相当于在索引上使用函数。
      • 只查询需要的列
        • 查询无用的列在数据传输和解析绑定过程中会增加网络 IO,以及 CPU 的开销
        • 会使得覆盖索引”失效”,这里的失效并非真正的不走索引。覆盖索引的本质就是在索引中包含所要查询的字段,而select *将使覆盖索引失去意义,仍然需要进行回表操作
      • 应尽量避免在 where 子句中使用 != 或 <> 操作符,否则将引擎放弃使用索引而进行全表扫描。
      • 应尽量避免在 where 子句中使用 or 来连接条件,如果一个字段有索引,一个字段没有索引,将导致引擎放弃使用索引而进行全表扫描

事务

  • 事务特点(ACID)
    • 原子性(atomicity)
      • MySQL 怎么保证原子性的?
        • 利用 Innodb 的undo log。
        • undo log 名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的 sql 语句,他需要记录你要回滚的相应日志信息。
        • 例如
          1. 当你 delete 一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert 这条旧数据
          2. 当你 update 一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行 update 操作
          3. 当你 insert 一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行 delete 操作
        • undo log 记录了这些回滚需要的信息,当事务执行失败或调用了 rollback,导致事务需要回滚,便可以利用 undo log 中的信息将数据回滚到修改之前的样子。
    • 一致性(consistency)
      • MySQL 怎么保证一致性的?
        • 这个问题分为两个层面来说。
          • 从数据库层面,数据库通过原子性、隔离性、持久性来保证一致性。也就是说 ACID 四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。数据库必须要实现 AID 三大特性,才有可能实现一致性。例如,原子性无法保证,显然一致性也无法保证。
          • 但是,如果你在事务里故意写出违反约束的代码,一致性还是无法保证的。例如,你在转账的例子中,你的代码里故意不给 B 账户加钱,那一致性还是无法保证。因此,还必须从应用层角度考虑。
          • 从应用层面,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据!
    • 隔离性(isolation)
      • MySQL 怎么保证隔离性的?
        • 利用的是锁和 MVCC 机制。
    • 持久性(durability)
      • MySQL 怎么保证持久性的?
        • 是利用 Innodb 的 redo log。MySQL 是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失。怎么解决这个问题?简单啊,事务提交前直接把数据写入磁盘就行啊。这么做有什么问题?
          • 只修改一个页面里的一个字节,就要将整个页面刷入磁盘,太浪费资源了。毕竟一个页面 16kb 大小,你只改其中一点点东西,就要将 16kb 的内容刷入磁盘,听着也不合理。
          • 毕竟一个事务里的 SQL 可能牵涉到多个数据页的修改,而这些数据页可能不是相邻的,也就是属于随机 IO。显然操作随机 IO,速度会比较慢。
        • 于是,决定采用 redo log 解决上面的问题。当做数据修改的时候,不仅在内存中操作,还会在 redo log 中记录这次操作。当事务提交的时候,会将 redo log 日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将 redo log 中的内容恢复到数据库中,再根据 undo log 和 binlog 内容决定回滚数据还是提交数据。
          • 采用 redo log 的好处?
            • 好处就是将 redo log 进行刷盘比对数据页刷盘效率高,具体表现如下
              • redo log 体积小,毕竟只记录了哪一页修改了啥,因此体积小,刷盘快。
              • redo log 是一直往末尾进行追加,属于顺序 IO。效率显然比随机 IO 来的快。
  • 实现方式
    • 通过预写日志方式实现的,redo 和 undo 机制是数据库实现事务的基础
    • redo 日志用来在断电/数据库崩溃等状况发生时重演一次刷数据的过程,把 redo 日志里的数据刷到数据库里,保证了事务的持久性(Durability)
    • undo 日志是在事务执行失败的时候撤销对数据库的操作,保证了事务的原子性
  • 事务的离级别
    • Read Uncommitted(读未提交)
      • 所有事务都可以看到其他未提交事务的执行结果
      • 会产生脏读(Dirty Read)
    • Read Committed(读已提交)
      • 一个事务只能看⻅已经提交事务所做的改变
      • 会产生不可重复读(Nonrepeatable Read)
      • 通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了脏读(Dirty Read)问题
        • 每次发起查询,都重新生成一个 ReadView
    • Repeatable Read(可重读)
      • MySQL 的默认事务隔离级别
      • 确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
      • 会产生幻读(Phantom Read)
        • 幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影”行。
      • 通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了不可重复读(Nonrepeatable Read)问题
        • 创建事务 trx 结构的时候,就生成了当前的 global read view。使用 trx_assign_read_view 函数创建,一直维持到事务结束。在事务结束这段时间内每一次查询都不会重新重建 Read View,从而实现了可重复读。
    • Serializable(串行化)
      • 通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。在每个读的数据行上加上共享锁

并发控制

  • LBCC
    • 基于锁的并发控制 Lock Based Concurrency Control(LBCC)
  • MVCC
    • 多版本的并发控制 Multi Version Concurrency Control(MVCC)
    • 在 Mysql 的 InnoDB 引擎中就是指在读已提交(READ COMMITTD)和可重复读(REPEATABLE READ)这两种隔离级别下的事务对于 SELECT 操作会访问版本链中的记录的过程。
    • 当前读、快照读
      • 当前读
        • 像 select lock in share mode(共享锁),select for update、update、insert、delete(排他锁) 这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
      • 快照读
        • 不加锁的 select 操作就是快照读
    • 实现原理
      • 通过 Undo 日志中的版本链和 ReadView 一致性视图来实现的。
      • 每行记录的隐藏字段:
        • MySQL_MVCC_1
        • DB_TRX_ID
          • 6byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务ID
        • DB_ROLL_PTR
          • 7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)
        • DB_ROW_ID
          • 6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
        • 实际还有一个删除 flag 隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除 flag 变了
      • Undo 日志
        • undo log 主要分为两种:
          • insert undo log
            • 代表事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃
          • update undo log
            • 事务在进行 update 或 delete 时产生的 undo log;不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除
              • purge
                • 更新或者删除操作都只是设置一下老记录的 deleted_bit,并不真正将过时的记录删除。
                • 为了节省磁盘空间,InnoDB 有专门的 purge 线程来清理 deleted_bit 为 true 的记录。为了不影响 MVCC 的正常工作,purge 线程自己也维护了一个 read view(这个 read view 相当于系统中最老活跃事务的 read view);如果某个记录的 deleted_bit 为 true,并且 DB_TRX_ID 相对于 purge 线程的 read view 可见,那么这条记录一定是可以被安全清除的。
        • 插入数据时流程
          • 举例
            1. 比如一个有个事务插入 person 表插入了一条新记录,记录如下,name 为 Jerry, age 为 24 岁,隐式主键是 1,事务 ID 和回滚指针,我们假设为 NULL
              • MySQL_MVCC_插入数据_1
            2. 现在来了一个事务 1 对该记录的 name 做出了修改,改为 Tom
              • 在事务 1 修改该行(记录)数据时,数据库会先对该行加排他锁
              • 然后把该行数据拷贝到 undo log 中,作为旧记录,既在 undo log 中有当前行的拷贝副本
              • 拷贝完毕后,修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID, 我们默认从 1 开始,之后递增,回滚指针指向拷贝到 undo log 的副本记录,既表示我的上一个版本就是它
              • 事务提交后,释放锁
              • MySQL_MVCC_插入数据_2
            3. 又来了个事务 2 修改 person 表的同一个记录,将 age 修改为 30 岁
              • 在事务 2 修改该行数据时,数据库也先为该行加锁
              • 然后把该行数据拷贝到 undo log 中,作为旧记录,发现该行记录已经有 undo log 了,那么最新的旧数据作为链表的表头,插在该行记录的 undo log 最前面
              • 修改该行 age 为 30 岁,并且修改隐藏字段的事务 ID 为当前事务 2 的 ID, 那就是 2,回滚指针指向刚刚拷贝到 undo log 的副本记录
              • 事务提交,释放锁
              • MySQL_MVCC_插入数据_3
            • 从上面,我们就可以看出,不同事务或者相同事务的对同一记录的修改,会导致该记录的 undo log 成为一条记录版本线性表,既链表,undo log 的链首就是最新的旧记录,链尾就是最早的旧记录(当然就像之前说的该 undo log 的节点可能是会 purge 线程清除掉,向图中的第一条 insert undo log,其实在事务提交之后可能就被删除丢失了,不过这里为了演示,所以还放在这里)
      • Read View(读视图)
        • Read View 就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。
      • 流程
        • 三个全局属性
          • trx_list
            • 一个数值列表,用来维护 Read View 生成时刻系统正活跃的事务 ID
          • up_limit_id
            • 记录 trx_list 列表中事务 ID 最小的ID
          • low_limit_id
            • ReadView 生成时刻系统尚未分配的下一个事务 ID,也就是目前已出现过的事务 ID 的最大值+1
        • 流程
          • MySQL_MVCC_流程
          1. 首先比较 DB_TRX_ID < up_limit_id, 如果小于,则当前事务能看到 DB_TRX_ID 所在的记录,如果大于等于进入下一个判断
          2. 接下来判断 DB_TRX_ID >= low_limit_id , 如果大于等于则代表 DB_TRX_ID 所在的记录在 Read View 生成后才出现的,那对当前事务肯定不可见,如果小于则进入下一个判断
          3. 判断 DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在,则代表我 Read View 生成时刻,你这个事务还在活跃,还没有 Commit,你修改的数据,我当前事务也是看不见的;如果不在,则说明,你这个事务在 Read View 生成之前就已经 Commit 了,你修改的结果,我当前事务是能看见的

日志

  • undo log
    • 基本概念
      • undo log 有两个作用
        • 提供回滚
        • 多个行版本控制(MVCC)
      • undo log 和 redo log 记录物理日志不一样,它是逻辑日志。可以认为当 delete 一条记录时,undo log 中会记录一条对应的 insert 记录,反之亦然,当 update 一条记录时,它记录一条对应相反的 update 记录。
      • 当执行 rollback 时,就可以从 undo log 中的逻辑记录读取到相应的内容并进行回滚。有时候应用到行版本控制的时候,也是通过 undo log 来实现的:当读取的某一行被其他事务锁定时,它可以从 undo log 中分析出该行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。
      • undo log 是采用段(segment)的方式来记录的,每个 undo 操作在记录的时候占用一个 undo log segment。
      • 另外,undo log 也会产生 redo log,因为 undo log 也要实现持久性保护。
    • 存储方式
      • innodb 存储引擎对undo的管理采用段的方式。rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment。
        • 版本区别
          • 老版本,只支持 1 个 rollback segment,这样就只能记录 1024 个 undo log segment。
          • MySQL5.5 可以支持 128 个 rollback segment,即支持 128*1024 个 undo 操作,还可以通过变量 innodb_undo_logs(5.6 版本以前该变量是 innodb_rollback_segments)自定义多少个 rollback segment,默认值为 128。
    • delete/update 操作的内部机制
      • insert 操作无需分析,就是插入行而已
      • delete 操作实际上不会直接删除,而是将 delete 对象打上 delete flag,标记为删除,最终的删除操作是 purge 线程完成的。
      • update 分为两种情况:update 的列是否是主键列。
        • 如果不是主键列,在 undo log 中直接反向记录是如何 update 的。即 update 是直接进行的。
        • 如果是主键列,update 分两部执行:先删除该行,再插入一行目标行。
  • redo log
    • 出现原因
      • 在每次事务提交的时候,将该事务涉及修改的数据页全部刷新到磁盘中。这么做会有严重的性能问题,主要体现在两个方面:
        • 因为 Innodb 是以页为单位进行磁盘交互的,而一个事务很可能只修改一个数据页里面的几个字节,这个时候将完整的数据页刷到磁盘的话,太浪费资源了!
        • 一个事务可能涉及修改多个数据页,并且这些数据页在物理上并不连续,使用随机 I/O 写入性能太差!
    • redo log 是重做日志,提供再写入操作,实现事务的持久性。日志记录事务对数据页做了哪些修改。
    • redo log 又包括了内存中的日志缓冲(redo log buffer)以及保存在磁盘的重做日志文件(redo log file)
      • 前者存储在内存中,容易丢失,后者持久化在磁盘中,不会丢失。
    • 底层原理
      • 结构
        • InnoDB 的 redo log 的大小是固定的,分别有多个日志文件采用循环方式组成一个循环闭环,当写到结尾时,会回到开头循环写日志。
          • MySQL_redo_log_环形
            • write pos 表示 redo log 当前记录的 LSN(逻辑序列号)位置,check point 表示数据页更改记录刷盘后对应 redo log 所处的 LSN(逻辑序列号)位置。
            • write pos 到 check point 之间的部分是 redo log 空着的部分,用于记录新的记录;check point 到 write pos 之间是 redo log 待落盘的数据页更改记录。当 write pos 追 上check point 时,会先推动 check point 向前移动,空出位置再记录新的日志。
      • 日志块(log block)
        • innodb 存储引擎中,redo log 以块为单位进行存储的,每个块占 512 字节,这称为 redo log block。所以不管是 log buffer 中还是 os buffer 中以及 redo log file on disk 中,都是这样以 512 字节的块存储的。
        • 每个 redo log block 由 3 部分组成:日志块头、日志块尾和日志主体。其中日志块头占用 12 字节,日志块尾占用 8 字节,所以每个 redo log block 的日志主体部分只有 512-12-8=492 字节。
          • MySQL_redo_log_日志块
          • 日志块头包含 4 部分:
            • log_block_hdr_no:(4 字节)该日志块在 redo log buffer 中的位置 ID。
            • log_block_hdr_data_len:(2 字节)该 log block 中已记录的 log 大小。写满该 log block 时为 0x200,表示 512 字节。
            • log_block_first_rec_group:(2 字节)该 log block 中第一个 log 的开始偏移位置。
              • 因为有时候一个数据页产生的日志量超出了一个日志块,这是需要用多个日志块来记录该页的相关日志。例如,某一数据页产生了 552 字节的日志量,那么需要占用两个日志块,第一个日志块占用 492 字节,第二个日志块需要占用 60个字节,那么对于第二个日志块来说,它的第一个 log 的开始位置就是 73 字节(60+12)。如果该部分的值和 log_block_hdr_data_len 相等,则说明该 log block 中没有新开始的日志块,即表示该日志块用来延续前一个日志块。
            • lock_block_checkpoint_no:(4 字节)写入检查点信息的位置。
          • 日志尾只有一个部分:log_block_trl_no,该值和块头的 log_block_hdr_no 相等。
        • 因为 redo log 记录的是数据页的变化,当一个数据页产生的变化需要使用超过 492 字节的 redo log 来记录,那么就会使用多个 redo log block 来记录该数据页的变化。
      • log group 和 redo log file
        • log group 表示的是 redo log group,一个组内由多个大小完全相同的 redo log file 组成。这个组是一个逻辑的概念,并没有真正的文件来表示这是一个组。
        • 写入方式
          • 在 innodb 将 log buffer 中的 redo log block 刷到这些 log file 中时,会以追加写入的方式循环轮训写入。即先在第一个 log file(即 ib_logfile0)的尾部追加写,直到满了之后向第二个 log file(即 ib_logfile1)写。当第二个 log file 满了会清空一部分第一个 log file 继续写入。
        • 结构
          • 在每个组的第一个 redo log file 中,前 2KB 记录 4 个特定的部分,从 2KB 之后才开始记录 log block。除了第一个 redo log file 中会记录,log group 中的其他 log file 不会记录这 2KB,但是却会腾出这 2KB 的空间。
            • MySQL_redo_log_log_group
        • redo log file 的大小对 innodb 的性能影响非常大,设置的太大,恢复的时候就会时间较长,设置的太小,就会导致在写 redo log 的时候循环切换 redo log file。
      • redo log 的格式
        • 因为 innodb 存储引擎存储数据的单元是页(和 SQL Server 中一样),所以 redo log 也是基于页的格式来记录的。默认情况下,innodb 的页大小是 16KB,一个页内可以存放非常多的 log block(每个 512 字节),而 log block 中记录的又是数据页的变化。
        • 其中 log block 中 492 字节的部分是 log body,该 log body 的格式分为 4 部分:
          • redo_log_type:占用 1 个字节,表示 redo log 的日志类型。
          • space:表示表空间的 ID,采用压缩的方式后,占用的空间可能小于 4 字节。
          • page_no:表示页的偏移量,同样是压缩过的。
          • redo_log_body 表示每个重做日志的数据部分,恢复时会调用相应的函数进行解析。
            • 例如 insert 语句和 delete 语句写入 redo log 的内容是不一样的。
        • MySQL_redo_log_格式
    • 流程
      • MySQL_redo_log_写入流程
      • InnoDB 的更新操作采用的是 Write Ahead Log 策略。
        • WAL 即 Write Ahead Log,WAL 的主要意思是说在将元数据的变更操作写入磁盘之前,先预先写入到一个 log 文件中。
        • 可以将对数据文件的随机写转换为堆 redo log 的顺序写,提高了性能。
      • 只有当 redo log 日志满了的情况下,才会主动触发脏页刷新到磁盘,而脏页不仅只有 redo log 日志满了的情况才会刷新到磁盘,以下几种情况同样会触发脏页的刷新:
        • 系统内存不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
        • MySQL 认为空闲的时间,这种情况没有性能问题;
        • MySQL 正常关闭之前,会把所有的脏页刷入到磁盘,这种情况也没有性能问题。
      • 启动、宕机时的写入
        • 启动 InnoDB 的时候,不管上次是正常关闭还是异常关闭,总是会进行恢复操作。因为 redo log 记录的是数据页的物理变化,因此恢复的时候速度比逻辑日志(如 binlog)要快很多。
        • 重启 InnoDB 时,首先会检查磁盘中数据页的 LSN,如果数据页的 LSN 小于日志中的 LSN,则会从 checkpoint 开始恢复。
        • 还有一种情况,在宕机前正处于 checkpoint 的刷盘过程,且数据页的刷盘进度超过了日志页的刷盘进度,此时会出现数据页中记录的 LSN 大于日志中的 LSN,这时超出日志进度的部分将不会重做,因为这本身就表示已经做过的事情,无需再重做。
    • 写入机制
      • MySQL 支持用户自定义在 commit 时如何将 log buffer 中的日志刷 log file 中。这种控制通过变量 innodb_flush_log_at_trx_commit 的值来决定。该变量有 3 种值:0、1、2,默认为 1。但注意,这个变量只是控制 commit 动作是否刷新 log buffer 到磁盘。
        • 当设置为 1 的时候,事务每次提交都会将 log buffer 中的日志写入 os buffer 并调用 fsync() 刷到 log file on disk 中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO 的性能较差。
        • 当设置为 0 的时候,事务提交时不会将 log buffer 中日志写入到 os buffer,而是每秒写入 os buffer 并调用 fsync() 写入到 log file on disk 中。也就是说设置为 0 时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失 1 秒钟的数据。
        • 当设置为 2 的时候,每次提交都仅写入到 os buffer,然后是每秒调用 fsync() 将 os buffer 中的日志写入到 log file on disk。【一般建议选择取值 2,因为 MySQL 挂了数据没有损失,整个服务器挂了才会损失 1 秒的事务提交数据。】
        • MySQL_redo_log_刷盘
    • 相关问题
      • redo log 与 binlog 的区别?
        1. redo log 是在 InnoDB 存储引擎层产生,而 binlog 是 MySQL 数据库的上层产生的,并且二进制日志不仅仅针对 INNODB 存储引擎,MySQL 数据库中的任何存储引擎对于数据库的更改都会产生二进制日志。
        2. 两种日志记录的内容形式不同。MySQL 的 binlog 是逻辑日志,其记录是对应的 SQL 语句。而 innodb 存储引擎层面的重做日志是物理日志。
        3. 两种日志与记录写入磁盘的时间点不同,二进制日志只在事务提交完成后进行一次写入。而 innodb 存储引擎的重做日志在事务进行中不断地被写入,并日志不是随事务提交的顺序进行写入的。
          • 二进制日志仅在事务提交时记录,并且对于每一个事务,仅在事务提交时记录,并且对于每一个事务,仅包含对应事务的一个日志。而对于 innodb 存储引擎的重做日志,由于其记录是物理操作日志,因此每个事务对应多个日志条目,并且事务的重做日志写入是并发的,并非在事务提交时写入,其在文件中记录的顺序并非是事务开始的顺序。
        4. binlog 不是循环使用,在写满或者重启之后,会生成新的 binlog 文件,redo log 是循环使用。
        5. binlog 可以作为恢复数据使用,主从复制搭建,redo log 作为异常宕机或者介质故障后的数据恢复使用。
  • binlog
    • binlog 是二进制日志文件,用于记录 MySQL 的数据更新或者潜在更新(比如 DELETE 语句执行删除而实际并没有符合条件的数据),在 MySQL 主从复制中就是依靠的 binlog。
    • binlog 是 MySQL 的逻辑日志,并且由 Server 层进行记录,使用任何存储引擎的 MySQL 数据库都会记录 binlog 日志
      • 逻辑日志:可以简单理解为记录的就是 sql 语句。
      • 物理日志:因为 MySQL 数据最终是保存在数据页中的,物理日志记录的就是数据页变更。
    • binlog 是通过追加的方式进行写入的,可以通过 max_binlog_size 参数设置每个 binlog 文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。
    • binlog 的三种工作模式:
      • Row level
        • 简介:日志中会记录每一行数据被修改的情况,然后在 slave 端对相同的数据进行修改。
        • 优点:能清楚的记录每一行数据修改的细节
        • 缺点:数据量太大
      • Statement level(默认)
        • 简介:每一条被修改数据的 sql 都会记录到 master 的 bin-log 中,slave 在复制的时候 sql 进程会解析成和原来 master 端执行过的相同的 sql 再次执行。在主从同步中一般是不建议用 statement 模式的,因为会有些语句不支持,比如语句中包含 UUID 函数,以及 LOAD DATA IN FILE 语句等
        • 优点:解决了 Row level 的缺点,不需要记录每一行的数据变化,减少 bin-log 日志量,节约磁盘 IO,提高新能 
        • 缺点:容易出现主从复制不一致
      • Mixed
        • 简介:在 Mixed 模式下,一般的语句修改使用 statment 格式保存 binlog,如一些函数,statement 无法完成主从复制的操作,则采用 row 格式保存 binlog,MySQL 会根据执行的每一条具体的 sql 语句来区分对待记录的日志形式,也就是在 Statement 和 Row 之间选择一种。
    • 写入机制
      • 事务执行过程中,先把日志写到 binlog cache,事务提交的时候,再把 binlog cache 写到 binlog 文件中。
        • binlog cache
          • 系统给 binlog cache 分配了一片内存,每个线程一个,参数 binlog_cache_size 用于控制单个线程内 binlog cache 所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
      • 一个事务的 binlog 是不能被拆开的,因此不论这个事务多大,也要确保一次性写入。
      • MySQL_binlog写入
        • 可以看到,每个线程有自己 binlog cache,但是共用同一份 binlog 文件。
        • 图中的 write,指的就是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快。
        • 图中的 fsync,才是将数据持久化到磁盘的操作。一般情况下,我们认为 fsync 才占磁盘的 IOPS(Input/Output Operations Per Second)。
      • write 和 fsync 的时机,是由参数 sync_binlog 控制的:
        • 0:表示每次提交事务都只 write,不 fsync;
        • 1:表示每次提交事务都会执行 fsync;(默认值)
        • N:表示每次提交事务都 write,但累积 N 个事务后才 fsync。
  • 两阶段提交
    • MySQL 最开始是没有 InnoDB 引擎的,binlog 日志位于 Server 层,只是用于归档和主从复制,本身不具备 crash safe 的能力。而 InnoDB 依靠 redo log 具备了 crash safe 的能力,redo log 和 binlog 同时记录,就需要保证两者的一致性。
    • 提交过程
      • MySQL_Innodb_二阶段提交
      1. prepare 阶段
        • 此阶段负责:
          • 在 Innodb 层获取独占模式的 prepare_commit_mutex,将事务的 trx_id 写入 redo log(redo 日志的写机制为 WAL 所以在事务修改前就会写 redo buffer 而不是 commit 时一次性写入)。
      2. commit 阶段
        1. 第一步,写 binlog
          • 此阶段调用两个方法 write()fsync(),前者负责将 binlog 从 binlog cache 写入文件系统缓存,后者负责将文件系统缓存中的 binlog 写入 disk,后者的调用机制是由 sync_binlog 参数控制的。
          • 注意 binlog 也是有 cache 的,在事务执行过程中生成的 binlog 会被存储在 binlog cache 中,此 cache 大小由 binlog_cache_size,这个 size 是 session 级别的,即每个会话都有一个 binlog cache。
        2. 第二步,innodb 进行 commit
          • 在 Innodb 层写入 commit flag,调用 write 和 fsync 将 commit 信息的 redo 写入磁盘,然后释放 prepare_commit_mutex。
          • 引擎层将 redo log buffer 中的 redo 写入文件系统缓存(write),然后将文件系统缓存中的 redo log 写入 disk(fsync),写入机制取决于 innodb_flush_log_at_trx_commit 参数。
    • redo log 和 binlog 是两种不同的日志,就类似于分布式中的多节点提交请求,需要保证事务的一致性。redo log 和 binlog 有一个公共字段 XID,代表事务 ID。当参数 innodb_support_xa 打开时,在执行事务的第一条 SQL 时候会去注册 XA,根据第一条 SQL 的 query id 拼凑 XID 数据,然后存储在事务对象中。
    • 如果两个日志单纯的分开提交,则可能会引发一些问题,如果简单分开提交,那么对于一条更新语句执行,有两种情况:
      • 先写 binlog,后写 redo log:如果 binlog 写入了,在写 redo log 之前数据库宕机。那么在重启恢复的时候,通过 binlog 恢复了数据没问题。但是由于 redo log 没有写入,这个事务应该无效,也就是原库中就不应该有这条语句对应的更新。但是通过 binlog 恢复数据后,数据库中就多了这条更新
      • 先写 redo log,后写 binlog:如果 redo log 写入了,在写 binlog 之前数据库宕机。那么在重启恢复的时候,通过 binlog 恢复从库,那么相对于主库来说,从库就少了这条更新
    • 采取了两段提交之后,怎么做 crash 恢复呢?
      • 如果在写入 binlog 之前宕机了,那么事务需要回滚;如果事务 commit 之前宕机了,那么此时 binlog cache 中的数据可能还没有刷盘,那么验证 binlog 的完整性:到 redo log 中找到最近事务的 XID,根据这个 XID 到 binlog 中去找(XID Event),如果找到了,说明在 binlog 中对应事务已经提交,那么提交 redo log 中事务即可;否则需要回滚事务。

主从复制

  • 主从复制、读写分离就是为了数据库能支持更大的并发。
  • 原理
    1. 当 Master 节点进行 insert、update、delete 操作时,会按顺序写入到 binlog 中。
    2. salve 从库连接 master 主库,Master 有多少个 slave 就会创建多少个 binlog dump 线程。
    3. 当 Master 节点的 binlog 发生变化时,binlog dump 线程会通知所有的 salve 节点,并将相应的 binlog 内容推送给 slave 节点。
    4. I/O 线程接收到 binlog 内容后,将内容写入到本地的 relay-log。
    5. SQL 线程读取 I/O 线程写入的 relay-log,并且根据 relay-log 的内容对从数据库做对应的操作。
    • MySQL_主从复制的原理
  • 同步策略
    • 「同步策略」:Master 会等待所有的 Slave 都回应后才会提交,这个主从的同步的性能会严重的影响。
    • 「半同步策略」:Master 至少会等待一个 Slave 回应后提交。
      • 从 MySQL5.5 开始,引入了半同步复制,此时的技术暂且称之为传统的半同步复制。技术发展到 MySQL5.7后,已经演变为增强半同步复制(也成为无损复制)。
        • 传统的半同步复制
          • 在传统的半同步复制中,主库写数据到 BINLOG,且执行 Commit 操作后,会一直等待从库的 ACK,即从库写入 Relay Log 后,并将数据落盘,返回给主库消息,通知主库可以返回前端应用操作成功,这样会出现一个问题,就是实际上主库已经将该事务 Commit 到了事务引擎层,应用已经可以可以看到数据发生了变化,只是在等待返回而已,如果此时主库宕机,有可能从库还没能写入 Relay Log,就会发生主从库不一致。
        • 增强半同步复制就
          • 增强半同步复制就是为了解决这个问题,做了微调,即主库写数据到 BINLOG 后,就开始等待从库的应答 ACK,直到至少一个从库写入 Relay Log 后,并将数据落盘,然后返回给主库消息,通知主库可以执行 Commit 操作,然后主库开始提交到事务引擎层,应用此时可以看到数据发生了变化。
          • MySQL_主从同步_半同步策略
      • 半同步复制模式下,假如在传送 BINLOG 日志到从库时,从库宕机或者网络延迟,导致 BINLOG 并没有及时地传送到从库上,此时主库上的事务会等待一段时间(时间长短由参数 rpl_semi_sync_master_timeout 设置的毫秒数决定),如果 BINLOG 在这段时间内都无法成功发送到从库上,则 MySQL 自动调整复制为异步模式,事务正常返回提交结果给客户端。
    • 「异步策略」:Master 不用等待 Slave 回应就可以提交。
      • MySQL_主从同步_异步策略
    • 「延迟策略」:Slave 要落后于 Master 指定的时间。
  • 缺点
    • 从机是通过 binlog 日志从 master 同步数据的,如果在网络延迟的情况,从机就会出现数据延迟。那么就有可能出现 master 写入数据后,slave 读取数据不一定能马上读出来。
  • 主从不同步的可能情况
    • 网络延迟
      • 由于 MySQL 主从复制是基于 binlog 的一种异步复制,通过网络传送 binlog 文件,理所当然网络延迟是主从不同步的绝大多数的原因,特别是跨机房的数据同步出现这种几率非常的大,所以做读写分离,注意从业务层进行前期设计。
    • 主从两台机器的负载不一致
      • 由于 MySQL 主从复制是主数据库上面启动 1 个 io 线程,而从上面启动 1 个 sql 线程和 1 个 io 线程,当中任何一台机器的负载很高,忙不过来,导致其中的任何一个线程出现资源不足,都将出现主从不一致的情况。
    • max_allowed_packet 设置不一致
      • 主数据库上面设置的 max_allowed_packet 比从数据库大,当一个大的 sql 语句,能在主数据库上面执行完毕,从数据库上面设置过小,无法执行,导致的主从不一致。
    • 自增键不一致
      • key 自增键开始的键值跟自增步长设置不一致引起的主从不一致。
    • 同步参数设置问题
      • MySQL 异常宕机情况下,如果未设置 sync_binlog=1 或者 innodb_flush_log_at_trx_commit=1 很有可能出现 binlog 或者 relaylog 文件出现损坏,导致主从不一致。
    • 主库 binlog 格式为 Statement,同步到从库执行后可能造成主从不一致。
    • 主库执行更改前有执行 set sql_log_bin=0,会使主库不记录 binlog,从库也无法变更这部分数据。
    • 从节点未设置只读,误操作写入数据。
    • 主库或从库意外宕机,宕机可能会造成 binlog 或者 relaylog 文件出现损坏,导致主从不一致
    • 主从实例版本不一致,特别是高版本是主,低版本是从的情况下,主数据库上面支持的功能从数据库上面可能不支持
  • 关于事务
    • 在同一事务内,读写操作应该均走主库,用于保证数据一致性。
  • 主从一致性检查
    • 利用 percona-toolkit 工具
    • 主库增加或者修改数据即往 MQ 里面放入消息,异步验证一致性

语句执行顺序

  • MySQL 的语句一共分为 11 步,最先执行的总是 FROM 操作,最后执行的是 LIMIT 操作。其中每一个操作都会产生一张虚拟的表,这个虚拟的表作为一个处理的输入,只是这些虚拟的表对用户来说是透明的,但是只有最后一个虚拟的表才会被作为结果返回。如果没有在语句中指定某一个子句,那么将会跳过相应的步骤。
  • 步骤
    1. FROM: 对 FROM 的左边的表和右边的表计算笛卡尔积。产生虚表 VT1。
    2. ON: 对虚表 VT1 进行 ON 筛选,只有那些符合 <join-condition> 的行才会被记录在虚表 VT2 中。
    3. JOIN: 如果指定了 OUTER JOIN(比如 left join、 right join),那么保留表中未匹配的行就会作为外部行添加到虚拟表 VT2 中,产生虚拟表 VT3, rug from 子句中包含两个以上的表的话,那么就会对上一个 join 连接产生的结果 VT3 和下一个表重复执行步骤 1~3 这三个步骤,一直到处理完所有的表为止。
    4. WHERE: 对虚拟表 VT3 进行WHERE条件过滤。只有符合 <where-condition> 的记录才会被插入到虚拟表 VT4 中。
    5. GROUP BY: 根据 group by 子句中的列,对 VT4 中的记录进行分组操作,产生 VT5。
    6. CUBE | ROLLUP: 对表 VT5 进行 cube 或者 rollup 操作,产生表 VT6。使用聚集函数进行计算。
    7. HAVING: 对虚拟表 VT6 应用 having 过滤,只有符合 <having-condition> 的记录才会被插入到虚拟表 VT7 中。
    8. SELECT: 执行 select 操作,选择指定的列,插入到虚拟表 VT8 中。
    9. DISTINCT: 对 VT8 中的记录进行去重。产生虚拟表 VT9。
    10. ORDER BY: 将虚拟表 VT9 中的记录按照<order_by_list>进行排序操作,产生虚拟表 VT10。
    11. LIMIT:取出指定行的记录,产生虚拟表 VT11, 并将结果返回。

命令

  • 事务
    • MySQL 默认是开启事务的(自动提交)
      • select @@autocommit;(autocommit=1)
    • 事务开启
      1. 修改默认提交 set autocommit=0;
      2. begin;start transaction;
      3. 事务手动提交:commit;
      4. 事务手动回滚:rollback;

中间件

路由与 web 服务器

  • 阿里基于 Nginx 研发的 Tengine
  • 阿里内部的集中式路由服务 VipServer

RPC 框架

  • grpc
  • Thrift
  • 阿里的 HSF
  • Dubbo
    • 节点说明
      • Consumer
        • 需要调用远程服务的服务消费方 Registry 注册中心
      • Provider
        • 服务提供方
      • Container
        • 服务运行的容器
      • Monitor
        • 监控中心
    • 大致流程:
      • 首先服务提供者 Provider 启动然后向注册中心注册自己所能提供的服务。
      • 服务消费者 Consumer 启动向注册中心订阅自己所需的服务。
      • 然后注册中心将提供者元信息通知给 Consumer, 之后 Consumer 因为已经从注册中心获取提供者的地址,因此可以通过负载均衡选择一个 Provider 直接调用。
      • 之后服务提供方元数据变更的话注册中心会把变更推送给服务消费者。
      • 服务提供者和消费者都会在内存中记录着调用的次数和时间,然后定时的发送统计数据到监控中心。
    • 注意:
      • 注册中心和监控中心是可选的,可以直接在配置文件里面写然后提供方和消费方直连。
      • 注册中心、提供方和消费方之间都是⻓连接,和监控方不是⻓连接,并且消费方是直接调用提供方,不经过注册中心。
      • 注册中心和监控中心宕机了也不会影响到已经正常运行的提供者和消费者,因为消费者有本地缓存提供者的信息。
    • 分层:
      • dubbo_1
      • Service,业务层,就是咱们开发的业务逻辑层。
      • Config,配置层,主要围绕 ServiceConfig 和 ReferenceConfig,初始化配置信息。
      • Proxy,代理层,服务提供者还是消费者都会生成一个代理类,使得服务接口透明化,代理层做远程调用和返回结果。
      • Register,注册层,封装了服务注册和发现。
      • Cluster,路由和集群容错层,负责选取具体调用的节点,处理特殊的调用要求和负责远程调用失败的容错措施。
      • Monitor,监控层,负责监控统计调用时间和次数。
      • Portocol,远程调用层,主要是封装 RPC 调用,主要负责管理 Invoker,Invoker 代表一个抽象封装了的执行体。
      • Exchange,信息交换层,用来封装请求响应模型,同步转异步。
      • Transport,网络传输层,抽象了网络传输的统一接口,这样用户想用 Netty 就用 Netty,想用 Mina 就用 Mina。
      • Serialize,序列化层,将数据序列化成二进制流,当然也做反序列化。
    • 调用过程:
      • 服务暴露过程
        1. 首先 Provider 启动,通过 Proxy 组件根据具体的协议 Protocol 将需要暴露出去的接口封装成 Invoker, Invoker 是 Dubbo 一个很核心的组件,代表一个可执行体。
        2. 然后再通过 Exporter 包装一下,这是为了在注册中心暴露自己套的一层,然后将 Exporter 通过 Registry 注册到注册中心。 这就是整体服务暴露过程。
      • 消费过程
        1. 首先消费者启动会向注册中心拉取服务提供者的元信息,然后调用流程也是从 Proxy 开始,毕竟都需要代理才能无感知。
        2. Proxy 持有一个 Invoker 对象,调用 invoke 之后需要通过 Cluster 先从 Directory 获取所有可调用的远程服务的 Invoker 列表,如果配置了某些路由规则,比如某个接口只能调用某个节点的那就再过滤一遍 Invoker 列表。
        3. 剩下的 Invoker 再通过 LoadBalance 做负载均衡选取一个。然后再经过 Filter 做一些统计什么的,再通过 Client 做数据传输,比如用 Netty 来传输。
        4. 传输需要经过 Codec 接口做协议构造,再序列化。最终发往对应的服务提供者。
        5. 服务提供者接收到之后也会进行 Codec 协议处理,然后反序列化后将请求扔到线程池处理。某个线程会根据请求找到对应的 Exporter ,而找到 Exporter 其实就是找到了 Invoker,但是还会有一层层 Filter,经过一层层过滤链之后最终调用实现类然后原路返回结果。
      • dubbo_2
    • 负载均衡策略
      • 随机 Random LoadBalance
        • 按照权重设置的大小,随机
      • 轮询 RoundRobin LoadBalance
        • 例如:a b c,a 执行完 b 执行然后c,然后在到 a
      • 最少活跃调用数(权重)LeastActive LoadBalance
        • 活跃数指调用前后计数差,优先调用高的,相同活跃数的随机。使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。
      • 一致性 Hash ConsistentHash LoadBalance
        • 相同参数总是发送到同一个提供者,如果这个提供者挂掉了,它会根据它的虚拟节点,平摊到其它服务者,不会引起巨大的变动
    • 相关问题
      • Dubbo 和 Spring Cloud 有什么区别?
        • 通信方式不同
          • Dubbo 使用的是 RPC 通信,而 Spring Cloud 使用的是 HTTP RESTFul 方式。
        • 组成部分不同
      • 当一个服务接口有多种实现时怎么做?
        • 当一个接口有多种实现时,可以用 group 属性来分组,服务提供方和消费方都指定同一个 group 即可。
      • 服务上线怎么兼容旧版本?
        • 可以用版本号(version)过渡,多个不同版本的服务注册到注册中心,版本号不同的服务相互间不引用。这个和服务分组的概念有一点类似。
  • SOFA-RPC

消息中间件

  • 消息队列通信的模式
    • 点对点模式
      • 消息队列_点对点模式
      • 点对点模式通常是基于拉取或者轮询的消息传送模型,这个模型的特点是发送到队列的消息被一个且只有一个消费者进行处理。生产者将消息放入消息队列后,由消费者主动的去拉取消息进行消费。点对点模型的的优点是消费者拉取消息的频率可以由自己控制。但是消息队列是否有消息需要消费,在消费者端无法感知,所以在消费者端需要额外的线程去监控。
    • 发布订阅模式
      • 消息队列_发布订阅模式
      • 发布订阅模式是一个基于消息送的消息传送模型,该模型可以有多种不同的订阅者。生产者将消息放入消息队列后,队列会将消息推送给订阅过该类消息的消费者(类似微信公众号)。由于是消费者被动接收推送,所以无需感知消息队列是否有待消费的消息!但是 consumer1、consumer2、consumer3 由于机器性能不一样,所以处理消息的能力也会不一样,但消息队列却无法感知消费者消费的速度!所以推送的速度成了发布订阅模模式的一个问题!假设三个消费者处理速度分别是 8M/s、5M/s、2M/s,如果队列推送的速度为 5M/s,则 consumer3 无法承受!如果队列推送的速度为 2M/s,则 consumer1、consumer2 会出现资源的极大浪费!
  • 消息队列使用场景
    • 解耦
      • 解耦是消息队列要解决的最本质问题。
    • 最终一致性
      • 最终一致性指的是两个系统的状态保持一致,要么都成功,要么都失败。
      • 最终一致性不是消息队列的必备特性,但确实可以依靠消息队列来做最终一致性的事情。
    • 广播
      • 消息队列的基本功能之一是进行广播。
      • 有了消息队列,我们只需要关心消息是否送达了队列,至于谁希望订阅,是下游的事情,无疑极大地减少了开发和联调的工作量。
    • 错峰与流控
      • 典型的使用场景就是秒杀业务用于流量削峰场景。
  • 常用的消息队列
    • Apache Kafka
      • Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据,具有高性能、持久化、多副本备份、横向扩展能力。
      • 架构
        • 消息队列_架构
        • Producer
          • Producer 即生产者,消息的产生者,是消息的入口。
        • Kafka cluster
          • Broker
            • Broker 是 Kafka 实例,每个服务器上有一个或多个 Kafka 的实例,我们姑且认为每个 broker 对应一台服务器。每个 Kafka 集群内的 broker 都有一个不重复的编号,如图中的 broker-0、broker-1 等……
            • Controller Broker
              • 在 Kafka 早期版本,对于分区和副本的状态的管理依赖于 zookeeper 的 Watcher 和队列:每一个 broker 都会在 zookeeper 注册 Watcher,所以 zookeeper 就会出现大量的 Watcher, 如果宕机的 broker 上的 partition 很多比较多,会造成多个 Watcher 触发,造成集群内大规模调整;每一个 replica 都要去再次 zookeeper 上注册监视器,当集群规模很大的时候,zookeeper 负担很重。这种设计很容易出现脑裂和羊群效应以及 zookeeper 集群过载。
              • 新版本该变了这种设计,使用 Kafka Controller,Leader 会向 zookeeper 上注册 Watcher,其他 broker 几乎不用监听 zookeeper 的状态变化。
              • Kafka 集群中多个 broker,有一个会被选举为 controller leader,负责管理整个集群中分区和副本的状态,比如 partition 的 leader 副本故障,由 controller 负责为该 partition 重新选举新的 leader 副本;当检测到 ISR 列表发生变化,由 controller 通知集群中所有 broker 更新其 MetadataCache 信息;或者增加某个 topic 分区的时候也会由 controller 管理分区的重新分配工作。
              • 当 broker 启动的时候,都会创建 KafkaController 对象,但是集群中只能有一个 leader 对外提供服务,这些每个节点上的 KafkaController 会在指定的 zookeeper 路径下创建临时节点,只有第一个成功创建的节点的 KafkaController 才可以成为 leader,其余的都是 follower。当 leader 故障后,所有的 follower 会收到通知,再次竞争在该路径下创建节点从而选举新的 leader。
              • Controller Broker 的具体作用
                • 创建、删除主题,增加分区并分配 leader 分区
                • 集群 Broker 管理(新增 Broker、Broker 主动关闭、Broker 故障)
                • preferred leader 选举
                • 分区重分配
          • Topic
            • 消息的主题,可以理解为消息的分类,Kafka 的数据就保存在 topic。在每个 broker 上都可以创建多个 topic。
          • Partition
            • Topic 的分区,每个 topic 可以有多个分区,分区的作用是做负载,提高 Kafka 的吞吐量。同一个 topic 在不同的分区的数据是不重复的,partition 的表现形式就是一个一个的文件夹!
            • 分区的主要目的
              • 方便扩展
                • 因为一个 topic 可以有多个 partition,所以我们可以通过扩展机器去轻松的应对日益增长的数据量。
              • 提高并发
                • 以 partition 为读写单位,可以多个消费者同时消费数据,提高了消息的处理效率。
            • Partition 结构
              • Partition 在服务器上的表现形式就是一个一个的文件夹,每个 partition 的文件夹下面会有多组 segment 文件,每组 segment 文件又包含 .index 文件、.log 文件、.timeindex 文件(早期版本中没有)三个文件,
                • log 文件就实际是存储 message 的地方
                • index 和 timeindex 文件为索引文件,用于检索消息。
              • Kafka_partition结构
                • 这个 partition 有三组 segment 文件,每个 log 文件的大小是一样的,但是存储的 message 数量是不一定相等的(每条的 message 大小不一致)。文件的命名是以该 segment 最小 offset 来命名的,如 000.index 存储 offset 为 0~368795 的消息,kafka 就是利用分段+索引的方式来解决查找效率的问题。
          • Replication
            • 每一个分区都有多个副本,副本的作用是做备胎。当主分区(Leader)故障的时候会选择一个备胎(Follower)上位,成为 Leader。在 Kafka 中默认副本的最大数量是 10 个,且副本的数量不能大于 Broker 的数量,follower 和 leader 绝对是在不同的机器,同一机器对同一个分区也只可能存放一个副本(包括自己)。
        • Message
          • 每一条发送的消息主体。
          • Message 结构
            • 消息主要包含消息体、消息大小、offset、压缩类型等等
              • offset
                • offset 是一个占 8byte 的有序 id 号,它可以唯一确定每条消息在 parition 内的位置!
              • 消息大小
                • 消息大小占用 4byte,用于描述消息的大小。
              • 消息体
                • 消息体存放的是实际的消息数据(被压缩过),占用的空间根据具体的消息而不一样。
        • Consumer
          • 消费者,即消息的消费方,是消息的出口。
        • Consumer Group
          • 我们可以将多个消费组组成一个消费者组,在 Kafka 的设计中同一个分区的数据只能被消费者组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个 topic 的不同分区的数据,这也是为了提高 Kafka 的吞吐量!
          • GroupCoordinator
            • 每个 consumer group 都会选择一个 broker 作为自己的 coordinator,他是负责监控整个消费组里的各个分区的心跳,以及判断是否宕机,和开启 rebalance 的。
            • 如何选择 coordinator 机器
              • 首先对 group id 进行 hash,接着对 __consumer_offsets 的分区数量进行取模,默认分区数量是 50
              • __consumer_offsets 的分区数量可以通过 offsets.topic.num.partitions 来设置,找到分区以后,这个分区所在的 broker 机器就是 coordinator 机器。
                • __consumer_offsets topic 了,它是 Kafka 内部使用的一个 topic,专门用来存储 group 消费的情况,默认情况下有50个 partition,每个 partition 默认有三个副本,而具体的一个 group 的消费情况要存储到哪一个 partition 上,是根据 $abs(GroupId.hashCode()) % NumPartitions$ 来计算的(其中,NumPartitions 是 __consumer_offsets 的 partition 数,默认是50个)。
              • 对于 consumer group 而言,是根据其 group.id 进行 hash 并计算得到其具对应的 partition 值,该 partition leader 所在 Broker 即为该 Group 所对应的 GroupCoordinator,GroupCoordinator 会存储与该 group 相关的所有的 Meta 信息。
        • Zookeeper
          • Kafka 集群依赖 zookeeper 来保存集群的的元信息,来保证系统的可用性。
          • 具体功能
            • 对于 broker
              • 记录状态
                • zookeeper 记录了所有 broker 的存活状态,broker 会向 zookeeper 发送心跳请求来上报自己的状态。
                • zookeeper 维护了一个正在运行并且属于集群的 broker 列表。
              • 控制器选举
                • kafka 集群中有多个 broker,其中有一个会被选举为控制器。
                • 控制器负责管理整个集群所有分区和副本的状态,例如某个分区的 leader 故障了,控制器会选举新的 leader。
                • 从多个 broker 中选出控制器,这个工作就是 zookeeper 负责的。
              • 限额权限
                • kafka 允许一些 client 有不同的生产和消费的限额。
                • 这些限额配置信息是保存在 zookeeper 里面的。
                • 所有 topic 的访问控制信息也是由 zookeeper 维护的。
              • 记录 ISR
                • ISR(in-sync replica) 是 partition 的一组同步集合,就是所有 follower 里面同步最积极的那部分。
                • 一条消息只有被 ISR 中的成员都接收到,才被视为“已同步”状态。
                • 只有处于 ISR 集合中的副本才有资格被选举为 leader。
                • zookeeper 记录着 ISR 的信息,而且是实时更新的,只要发现其中有成员不正常,马上移除。
              • node 和 topic 注册
                • zookeeper 保存了所有 node 和 topic 的注册信息,可以方便的找到每个 broker 持有哪些 topic。
                • node 和 topic 在 zookeeper 中是以临时节点的形式存在的,只要与 zookeeper 的 session 一关闭,他们的信息就没有了。
              • topic 配置
                • zookeeper 保存了 topic 相关配置,例如 topic 列表、每个 topic 的 partition 数量、副本的位置等等。
            • 对于 consumer
              • offset
                • kafka 老版本中,consumer 的消费偏移量是默认存储在 zookeeper 中的。
                • 新版本中,这个工作由 kafka 自己做了,kafka 专门做了一个 offset manager。
              • 注册
                • 和 broker 一样,consumer 也需要注册。
                • consumer 会自动注册,注册的方式也是创建一个临时节点,consumer down 了之后就会自动销毁。
              • 分区注册
                • kafka 的每个 partition 只能被消费组中的一个 consumer 消费,kafka 必须知道所有 partition 与 consumer 的关系。
          • 相关问题
            • Kafka 为什么要放弃 Zookeeper?
              • confluent 社区发表了一篇文章,主要讲述了 Kafka 未来的 2.8 版本将要放弃 Zookeeper,这对于 Kafka 用户来说,是一个重要的改进。之前部署 Kafka 就必须得部署 Zookeeper,而之后就只要单独部署 Kafka 就行了。
              • Kafka 本身就是一个分布式系统,但是需要另一个分布式系统来管理,复杂性无疑增加了。
                • 运维复杂度
                • Controller 故障处理
                  • Kafaka 依赖一个单一 Controller 节点跟 Zookeeper 进行交互,如果这个 Controller 节点发生了故障,就需要从 broker 中选择新的 Controller。
                  • 新的 Controller 选举成功后,会重新从 Zookeeper 拉取元数据进行初始化,并且需要通知其他所有的 broker 更新 ActiveControllerId。老的 Controller 需要关闭监听、事件处理线程和定时任务。分区数非常多时,这个过程非常耗时,而且这个过程中 Kafka 集群是不能工作的。
                • 分区瓶颈
                  • 当分区数增加时,Zookeeper 保存的元数据变多,Zookeeper 集群压力变大,达到一定级别后,监听延迟增加,给 Kafaka 的工作带来了影响。
              • 升级
                • Kafka_取消zookeeper后的架构图
                • KIP-500 用 Quorum Controller 代替之前的 Controller,Quorum 中每个 Controller 节点都会保存所有元数据,通过 KRaft 协议保证副本的一致性。这样即使 Quorum Controller 节点出故障了,新的 Controller 迁移也会非常快。
                • 官方介绍,升级之后,Kafka 可以轻松支持百万级别的分区。
              • Kafaka 计划在 3.0 版本会兼容 Zookeeper Controller 和 Quorum Controller,这样用户可以进行灰度测试。
      • 重平衡机制
        • 重平衡其实就是一个协议,它规定了如何让消费者组下的所有消费者来分配 topic 中的每一个分区。比如一个 topic 有 100 个分区,一个消费者组内有 20 个消费者,在协调者的控制下让组内每一个消费者分配到 5 个分区,这个分配的过程就是重平衡。
        • 重平衡的触发条件
          • 消费者组内成员发生变更,这个变更包括了增加和减少消费者。注意这里的减少有很大的可能是被动的,就是某个消费者崩溃退出了
          • 主题的分区数发生变更,kafka 目前只支持增加分区,当增加的时候就会触发重平衡
          • 订阅的主题发生变化,当消费者组使用正则表达式订阅主题,而恰好又新建了对应的主题,就会触发重平衡
        • 重平衡策略
          • Range
            • 具体实现位于,package org.apache.kafka.clients.consumer.RangeAssignor。
            • 把若干个连续的分区分配给消费者,如存在分区 1-5,假设有 3 个消费者,则消费者 1 负责分区 1-2,消费者 2 负责分区 3-4,消费者 3 负责分区 5。
          • RoundRobin
            • 具体实现位于,package org.apache.kafka.clients.consumer.RoundRobinAssignor。
            • 就是把所有分区逐个分给消费者,如存在分区 1-5,假设有 3 个消费者,则分区 1->消费 1,分区 2->消费者 2,分区 3>消费者 3,分区 4>消费者 1,分区 5->消费者 2。
          • Sticky
            • Sticky 分配策略是最新的也是最复杂的策略,其具体实现位于 package org.apache.kafka.clients.consumer.StickyAssignor。
            • 这种分配策略是在 0.11.0 才被提出来的,主要是为了一定程度解决上面提到的重平衡非要重新分配全部分区的问题。称为粘性分配策略。
        • 重平衡过程
          • 消费端重平衡流程
            • Rebalance 是通过消费者群组中的称为“群主”消费者客户端进行的。
              • “群主”就是第一个加入群组的消费者。消费者第一次加入群组时,它会向群组协调器发送一个 JoinGroup 的请求,如果是第一个,则此消费者被指定为“群主”。
                • Kafka_重平衡_JoinGroup
            1. 群主从群组协调器获取群组成员列表,然后给每一个消费者进行分配分区 Partition。
              • Kafka_消费端重平衡_1
            2. 群主分配完成之后,把分配情况发送给群组协调器。
              • Kafka_消费端重平衡_2
            3. 群组协调器再把这些信息发送给消费者。每一个消费者只能看到自己的分配信息,只有群主知道所有消费者的分配信息。
          • Broker 端重平衡
            • 新成员加入组
              • Kafka_新成员加入组
            • 组成员主动离组
              • Kafka_组成员主动离组
            • 组成员崩溃离组
              • Kafka_组成员崩溃离组
            • 组成员提交位移
              • Kafka_组成员提交位移
        • 避免重平衡
          • 未及时发送心跳
            • 第一类非必要 Rebalance 是因为未能及时发送心跳,导致 Consumer 被“踢出” Group 而引发的。因此,你需要仔细地设置 session.timeout.ms 和 heartbeat.interval.ms 的值。
          • Consumer 消费时间过长
            • 第二类非必要 Rebalance 是 Consumer 消费时间过长导致的。
      • ISR 机制
        • Kafka 提供了数据复制算法保证,如果 leader 发生故障或挂掉,一个新 leader 被选举并被接受客户端的消息成功写入。Kafka 确保从同步副本列表中选举一个副本为 leader,或者说 follower 追赶 leader 数据。leader 负责维护和跟踪 ISR(In-Sync Replicas 的缩写,表示副本同步队列)中所有 follower 滞后的状态。当 producer 发送一条消息到 broker 后,leader 写入消息并复制到所有 follower。消息提交之后才被成功复制到所有的同步副本。消息复制延迟受最慢的 follower 限制,重要的是快速检测慢副本,如果 follower “落后”太多或者失效,leader 将会把它从 ISR 中删除。
        • 相关概念
          • AR:所有的副本(replicas)统称为 Assigned Replicas
          • ISR:in-Sync Replicas,这个是指副本同步队列
          • OSR:follower 从 leader 同步数据有一些延迟,任意一个超过阈值都会把 follower 剔除出 ISR, 存入 OSR(Outof-Sync Replicas)列表,新加入的 follower 也会先存放在 OSR 中
          • HW:HighWatermark,是指 consumer 能够看到的此 partition 的位置
          • LEO:LogEndOffset,表示每个 partition 的 log 最后一条 Message 的位置
        • 机制原理
          • 每个 replica 都有自己的 HW,leader 和 follower 各自负责更新自己的 HW 的状态。对于 leader 新写入的消息,consumer 不能立刻消费,leader 会等待该消息被所有 ISR 中的 replicas 同步后更新 HW,此时消息才能被 consumer 消费。这样就保证了如果 leader 所在的 broker 失效,该消息仍然可以从新选举的 leader 中获取。对于来自内部 broker 的读取请求,没有 HW 的限制。
            • Kafka_ISR以及HW和LEO的流转过程
          • Kafka 的复制机制既不是完全的同步复制,也不是单纯的异步复制。
            • 同步复制要求所有能工作的 follower 都复制完,这条消息才会被 commit,这种复制方式极大的影响了吞吐率。
            • 异步复制方式下,follower 异步的从 leader 复制数据,数据只要被 leader 写入 log 就被认为已经 commit,这种情况下如果 follower 都还没有复制完,落后于 leader 时,突然 leader 宕机,则会丢失数据。
          • 流程
            • 自动给每个 Partition 维护一个 ISR 列表,这个列表里一定会有 Leader,然后还会包含跟 Leader 保持同步的 Follower。也就是说,只要 Leader 的某个 Follower 一直跟他保持数据同步,那么就会存在于 ISR 列表里。
            • 但是如果 Follower 因为自身发生一些问题,导致不能及时的从 Leader 同步数据过去,那么这个 Follower 就会被认为是“out-of-sync”,从 ISR 列表里踢出去。
        • 生效时机
          • 当 acks 参数设置为 all 时,producer 需要等待 ISR 中的所有 follower 都确认接收到数据后才算一次发送完成,可靠性最高。
      • 工作流程
        • 发送数据
          • Producer 在写入数据的时候永远的找 leader,不会直接将数据写入 follower。
          • Kafka_发送数据
          • 消息写入 leader 后,follower 是主动的去 leader 进行同步的!producer 采用 push 模式将数据发布到 broker,每条消息追加到分区中,顺序写入磁盘,所以保证同一分区内的数据是有序的!
          • Kafka_producer_partition
          • 相关问题
            • 如果某个 topic 有多个 partition,producer 又怎么知道该将数据发往哪个 partition 呢?
              1. partition 在写入的时候可以指定需要写入的 partition,如果有指定,则写入对应的 partition。
              2. 如果没有指定 partition,但是设置了数据的 key,则会根据 key 的值 hash 出一个 partition。
              3. 如果既没指定 partition,又没有设置 key,则会轮询选出一个 partition。
            • producer 在向 kafka 写入消息的时候,怎么保证消息不丢失呢?
              • 通过 ACK 应答机制!在生产者向队列写入数据的时候可以设置参数来确定是否确认 Kafka 接收到数据,这个参数可设置的值为 0、1、-1。
                • 0 代表 producer 往集群发送数据不需要等到集群的返回,不确保消息发送成功。安全性最低但是效率最高。
                • 1 代表 producer 往集群发送数据只要 leader 应答就可以发送下一条,只确保 leader 发送成功。
                  • Kafka_应答机制_acks_1
                • -1 代表只有当 ISR 中的副本全部收到消息时,生产者才会认为消息生产成功了。这种配置是最安全的,因为如果 leader 副本挂了,当 follower 副本被选为 leader 副本时,消息也不会丢失。但是系统吞吐量会降低,因为生产者要等待所有副本都收到消息后才能再次发送消息。
            • 如果往不存在的 topic 写数据,能不能写入成功呢?
              • Kafka 会自动创建 topic,分区和副本的数量根据默认配置都是 1。
        • 保存数据
          • Kafka 将数据保存在磁盘,可能在我们的一般的认知里,写入磁盘是比较耗时的操作,不适合这种高并发的组件。Kafka 初始会单独开辟一块磁盘空间,顺序写入数据(效率比随机写入高)。
          • 存储策略
            1. 基于时间,默认配置是 168 小时(7 天)。
            2. 基于大小,默认配置是 1073741824(1G)。
            • 需要注意的是,kafka 读取特定消息的时间复杂度是 O(1),所以这里删除过期的文件并不会提高 kafka 的性能!
        • 消费数据
          • Kafka 采用的是点对点的模式,消费者主动的去 kafka 集群拉取消息,与 producer 相同的是,消费者在拉取消息的时候也是找 leader 去拉取。
          • 同一个消费组的消费者可以消费同一 topic 下不同分区的数据,但是不会组内多个消费者消费同一分区的数据!
            • Kafka_消费数据
            • 消费者组内的消费者小于 partition 数量的情况,所以会出现某个消费者消费多个 partition 数据的情况,消费的速度也就不及只处理一个 partition 的消费者的处理速度!
          • 建议消费者组的 consumer 的数量与 partition 的数量一致!
          • 相关问题
            • 查找消息的时候是怎么利用 segment+offset 配合查找的呢?假如现在需要查找一个 offset 为 368801 的 message 是什么样的过程呢?
              • Kafka_查数据
              1. 先找到 offset 的 368801 的 message 所在的 segment 文件(利用二分法查找),这里找到的就是在第二个 segment 文件。
              2. 打开找到的 segment 中的 .index 文件(也就是 368796.index 文件,该文件起始偏移量为 368796+1,我们要查找的 offset 为 368801 的 message 在该 index 内的偏移量为 368796+5=368801,所以这里要查找的相对 offset 为 5)。由于该文件采用的是稀疏索引的方式存储着相对 offset 及对应 message 物理偏移量的关系,所以直接找相对 offset 为 5 的索引找不到,这里同样利用二分法查找相对 offset 小于或者等于指定的相对 offset 的索引条目中最大的那个相对 offset,所以找到的是相对 offset 为 4 的这个索引。
              3. 根据找到的相对 offset 为 4 的索引确定 message 存储的物理偏移位置为 256。打开数据文件,从位置为 256 的那个地方开始顺序扫描直到找到 offset 为 368801 的那条 Message。
              • 这套机制是建立在 offset 为有序的基础上,利用 segment+有序 offset+稀疏索引+二分查找+顺序查找等多种手段来高效的查找数据!
            • 从 kafka 读取数据后,数据会自动删除吗?
              • 不会,kafka 中数据的删除跟有没有消费者消费完全无关。数据的删除,只跟 kafka broker 上面上面的这两个配置有关:
                • log.retention.hours=48 #数据最多保存48小时

                • log.retention.bytes=1073741824 #数据最多1G

      • log 的清除策略以及压缩策略
        • 日志的分段存储,一方面能够减少单个文件内容的大小,另一方面,方便 kafka 进行日志清理。
        • 清理策略有两个:
          1. 根据消息的保留时间,当消息在 kafka 中保存的时间超过了指定的时间,就会触发清理过程
          2. 根据 topic 存储的数据大小,当 topic 所占的日志文件大小大于一定的阀值,则可以开始删除最旧的消息。kafka 会启动一个后台线程,定期检查是否存在可以删除的消息
          • 当其中任意一个达到要求,都会执行删除。
        • 日志压缩策略:
          • 通过这个功能可以有效的减少日志文件的大小,缓解磁盘紧张的情况,在很多实际场景中,消息的 key 和 value 的值之间的对应关系是不断变化的,就像数据库中的数据会不断被修改一样,消费者只关心 key 对应的最新的 value。因此,我们可以开启 kafka 的日志压缩功能,服务端会在后台启动启动 Cleaner 线程池,定期将相同的 key 进行合并,只保留最新的 value 值。
      • 原理
        • producer
          • Kafka_producer架构
          • 整个生产者客户端由两个线程协调运行,这两个线程分别为主线程和发送线程。在主线程中由 KafkaProducer 创建消息,然后通过可能的拦截器、序列化器和分区器的作用之后缓存到消息收集器(RecordAccumulator,也称为消息累加器)中。发送线程负责从消息收集器中获取消息并将其发送到 Kafka 中。
          • 主线程中发送过来的消息都会被追加到消息收集器的某个双端队列(Deque)中,在其的内部为每个分区都维护了一个双端队列,队列中的内容就是ProducerBatch,即 Deque。消息写入缓存时,追加到双端队列的尾部;Sender 读取消息时,从双端队列的头部读取。注意 ProducerBatch 不是 ProducerRecord,ProducerBatch 中可以包含一至多个 ProducerRecord。
            • ProducerRecord 是生产者中创建的消息,而 ProducerBatch 是指一个消息批次,ProducerRecord 会被包含在 ProducerBatch 中,这样可以使字节的使用更加紧凑。与此同时,将较小的 ProducerRecord 拼凑成一个较大的 ProducerBatch,也可以减少网络请求的次数以提升整体的吞吐量。
        • broker
          • Kafka_broker架构图
        • consumer
          • Kafka_consumer_拉取消息原理
      • 相关问题
        • 为什么要使用 kafka?
          • 缓冲和削峰
          • 解耦和扩展性
          • 冗余
          • 健壮性
          • 异步通信
        • Kafka 是如何做到消息不丢失或不重复的?
          • 生产者数据的不丢失
            • 要使用带回调方法的 API。
            • 在 kafka 发送数据的时候,每次发送消息都会有一个确认反馈机制,确保消息正常的能够被收到。
              • 设置参数 acks=-1。
            • 设置参数 retries=3。
              • 参数 retries 表示生产者生产消息的重试次数。
              • 这里 retries=3 是一个建议值,一般情况下能满足足够的重试次数就能重试成功。但是如果重试失败了,对异常处理时就可以把消息保存到其他可靠的地方,如磁盘、数据库、远程缓存等,然后等到服务正常了再继续发送消息。
            • 设置参数 retry.backoff.ms=300。
              • retry.backoff.ms 指消息生产超时或失败后重试的间隔时间,单位是毫秒。
          • 消费者数据的不丢失
            • 从 kafka 拉取消息下来,由于自动的提交模式已经提交了 offset,但消费者是没有真正消费成功的,并且消费者可能日常发布重启或者挂掉了,那这条消息就丢了。
              • 如何费者数据的不丢失解决?
                • 关闭自动提交,改成手动提交,每次数据处理完后,再提交。
            • kafka 自己记录了每次消费的 offset 数值,下次继续消费的时候,会接着上次的 offset 进行消费。
            • 而 offset 的信息在 kafka0.8 版本之前保存在 zookeeper 中,在 0.8 版本之后保存到 topic 中,即使消费者在运行过程中挂掉了,再次启动的时候会找到 offset 的值,找到之前消费消息的位置,接着消费,由于 offset 的信息写入的时候并不是每条消息消费完成后都写入的,所以这种情况有可能会造成重复消费,但是不会丢失消息。
              • 如何解决重复消费问题?
                • 关闭自动提交,改成手动提交,每次数据处理完后,再提交。消费的接口幂等处理。
          • broker 的数据不丢失
            • 每个 broker 中的 partition 我们一般都会设置有 replication(副本)的个数,生产者写入的时候首先根据分发策略(有 partition 按 partition,有 key 按 key,都没有就轮询)写入到 leader 中,follower(副本)再跟 leader 同步数据,这样有了备份,也可以保证消息数据的不丢失。
              • 设置 replication.factor >1。
                • replication.factor 这个参数表示分区副本的个数,这里我们要将其设置为大于 1 的数,这样当 leader 副本挂了,follower 副本还能被选为 leader 副本继续接收消息。
              • 设置 min.insync.replicas >1。
                • min.insync.replicas 指的是 ISR 最少的副本数量,原理同上,也需要大于 1 的副本数量来保证消息不丢失。
              • 设置 unclean.leader.election.enable = false。
                • unclean.leader.election.enable 指是否能把非 ISR 集合中的副本选举为 leader 副本。unclean.leader.election.enable = true,也就是说允许非 ISR 集合中的 follower 副本成为 leader 副本。
          • 在 Kafka 中,生产者写入消息、消费者读取消息的操作都是与 leader 副本进行交互的,从而实现的是一种主写主读的生产消费模型。
          • Kafka 并不支持主写从读,因为主写从读有 2 个很明显的缺点:
            • 数据一致性问题
              • 数据从主节点转到从节点必然会有一个延时的时间窗口,这个时间窗口会导致主从节点之间的数据不一致。某一时刻,在主节点和从节点中 A 数据的值都为 X,之后将主节点中 A 的值修改为 Y,那么在这个变更通知到从节点之前,应用读取从节点中的 A 数据的值并不为最新的 Y,由此便产生了数据不一致的问题。
            • 延时问题
              • 类似 Redis 这种组件,数据从写入主节点到同步至从节点中的过程需要经历 网络→主节点内存→网络→从节点内存 这几个阶段,整个过程会耗费一定的时间。而在 Kafka 中,主从同步会比 Redis 更加耗时,它需要经历 网络→主节点内存→主节点磁盘→网络→从节点内存→从节点磁盘 这几个阶段。对延时敏感的应用而言,主写从读的功能并不太适用。
          • 而 kafka 的主写主读的优点就很多了:
            • 可以简化代码的实现逻辑,减少出错的可能;
            • 将负载粒度细化均摊,与主写从读相比,不仅负载效能更好,而且对用户可控;
            • 没有延时的影响;
            • 在副本稳定的情况下,不会出现数据不一致的情况。
        • 磁盘存储的性能问题
          • 为了规避随机读写带来的时间消耗,kafka 采用顺序写的方式存储数据。
            • 磁盘读取时间:
              • 寻道时间,表示磁头在不同磁道之间移动的时间。
              • 旋转延迟,表示在磁道找到时,中轴带动盘面旋转到合适的扇区开头处。
              • 传输时间,表示盘面继续转动,实际读取数据的时间。
            • 顺序读写,磁盘会预读,预读即在读取的起始地址连续读取多个页面,主要时间花费在了传输时间,而这个时间两种读写可以认为是一样的。
            • 随机读写,因为数据没有在一起,将预读浪费掉了。需要多次寻道和旋转延迟。而这个时间可能是传输时间的许多倍。
          • 零拷贝
            • 消息从发送到落地保存,broker 维护的消息日志本身就是文件目录,每个文件都是二进制保存,生产者和消费者使用相同的格式来处理。在消费者获取消息时,服务器先从硬盘读取数据到内存,然后把内存中的数据原封不动的通过 socket 发送给消费者。虽然这个操作描述起来很简单,但实际上经历了很多步骤。
              • 操作系统将数据从磁盘读入到内核空间的页缓存:
                • 应用程序将数据从内核空间读入到用户空间缓存中
                • 应用程序将数据写回到内核空间到 socket 缓存中
                • 操作系统将数据从 socket 缓冲区复制到网卡缓冲区,以便将数据经网络发出
              • 零拷贝_1
            • 通过“零拷贝”技术,可以去掉这些没必要的数据复制操作,同时也会减少上下文切换次数。现代的unix操作系统提供一个优化的代码路径,用于将数据从页缓存传输到 socket;在 Linux 中,是通过 sendfile 系统调用来完成的。Java 提供了访问这个系统调用的方法:FileChannel.transferTo API
            • 使用 sendfile,只需要一次拷贝就行,允许操作系统将数据直接从页缓存发送到网络上。所以在这个优化的路径中,只有最后一步将数据拷贝到网卡缓存中是需要的
            • 零拷贝_2
          • 页缓存
            • 页缓存是操作系统实现的一种主要的磁盘缓存,但凡设计到缓存的,基本都是为了提升 I/O 性能,所以页缓存是用来减少磁盘 I/O 操作的。
            • 磁盘高速缓存有两个重要因素:
              • 访问磁盘的速度要远低于访问内存的速度,若从处理器 L1 和 L2 高速缓存访问则速度更快。
              • 数据一旦被访问,就很有可能短时间内再次访问。正是由于基于访问内存比磁盘快的多,所以磁盘的内存缓存将给系统存储性能带来质的飞越。
            • 当一个进程准备读取磁盘上的文件内容时,操作系统会先查看待读取的数据所在的页(page)是否在页缓存(pagecache)中,如果存在(命中)则直接返回数据,从而避免了对物理磁盘的 I/O 操作;如果没有命中,则操作系统会向磁盘发起读取请求并将读取的数据页存入页缓存,之后再将数据返回给进程。
            • 同样,如果一个进程需要将数据写入磁盘,那么操作系统也会检测数据对应的页是否在页缓存中,如果不存在,则会先在页缓存中添加相应的页,最后将数据写入对应的页。被修改过后的页也就变成了脏页,操作系统会在合适的时间把脏页中的数据写入磁盘,以保持数据的一致性。
            • Kafka 中大量使用了页缓存,这是 Kafka 实现高吞吐的重要因素之一。虽然消息都是先被写入页缓存,然后由操作系统负责具体的刷盘任务的,但在 Kafka 中同样提供了同步刷盘及间断性强制刷盘(fsync),可以通过 log.flush.interval.messages 和 log.flush.interval.ms 参数来控制。
              • 同步刷盘能够保证消息的可靠性,避免因为宕机导致页缓存数据还未完成同步时造成的数据丢失。
                • 但是实际使用上,我们没必要去考虑这样的因素以及这种问题带来的损失,消息可靠性可以由多副本来解决,同步刷盘会带来性能的影响。
        • Kafka 为何兵法吞吐量高?
          • 生产端
            • 通过消息压缩、消息批量缓存发送、异步解耦等方面提升吞吐量
          • 服务端
            • 采用的优化技术比较多,比如网络层的 Reactor 设计提升了网络层的吞吐;顺序写、页缓存、零拷贝时利用操作系统的优化点来实现存储层读写的吞吐量
          • 消费端
            • 通过线程异步解耦的方式提升了拉取消息的效率,进而提升消费者的吞吐量
    • Apache RabbitMQ
      • 概念
        • broker:每个节点运行的服务程序,功能为维护该节点的队列的增删以及转发队列操作请求。
        • master queue:每个队列都分为一个主队列和若干个镜像队列。
        • mirror queue:镜像队列,作为 master queue 的备份。在 master queue 所在节点挂掉之后,系统把 mirror queue 提升为 master queue,负责处理客户端队列操作请求。注意,mirror queue 只做镜像,设计目的不是为了承担客户端读写压力。
      • 架构
        • RabbitMQ_架构
      • 工作流程
        • 队列消费
          • RabbitMQ_队列消费
          • 有两个 consumer 消费队列 A,这两个 consumer 连在了集群的不同机器上。RabbitMQ 集群中的任何一个节点都拥有集群上所有队列的元信息,所以连接到集群中的任何一个节点都可以,主要区别在于有的 consumer 连在 master queue 所在节点,有的连在非 master queue 节点上。
          • 因为 mirror queue 要和 master queue 保持一致,故需要同步机制,正因为一致性的限制,导致所有的读写操作都必须都操作在 master queue 上,然后由 master 节点同步操作到 mirror queue 所在的节点。即使 consumer 连接到了非 master queue 节点,该 consumer 的操作也会被路由到 master queue 所在的节点上,这样才能进行消费。
        • 队列生产
          • RabbitMQ_队列生产
          • 原理和消费一样,如果连接到非 master queue 节点,则路由过去。
      • RabbitMQ 的不足:由于 master queue 单节点,导致性能瓶颈,吞吐量受限。虽然为了提高性能,内部使用了 Erlang 这个语言实现,但是终究摆脱不了架构设计上的致命缺陷。
    • NSQ
    • 阿里孵化开源的 Apache RocketMQ
    • ActiveMQ
    • 比较
      • 特性 ActiveMQ RabbitMQ RocketMQ Kafka
        单机吞吐量 万级,吞吐量比RocketMQ和Kafka要低了一个数量级 万级,吞吐量比RocketMQ和Kafka要低了一个数量级 10万级,RocketMQ也是可以支撑高吞吐的一种MQ 10万级别,这是kafka最大的优点,就是吞吐量高。一般配合大数据类的系统来进行实时数据计算、日志采集等场景
        topic数量对吞吐量的影响 topic可以达到几百,几千个的级别,吞吐量会有较小幅度的下降。这是RocketMQ的一大优势,在同等机器下,可以支撑大量的topic topic从几十个到几百个的时候,吞吐量会大幅度下降。所以在同等机器下,kafka尽量保证topic数量不要过多。如果要支撑大规模topic,需要增加更多的机器资源
        时效性 ms级 微秒级,这是rabbitmq的一大特点,延迟是最低的 ms级 延迟在ms级以内
        可用性 高,基于主从架构实现高可用性 高,基于主从架构实现高可用性 非常高,分布式架构 非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
        功能支持 MQ领域的功能极其完备 基于erlang开发,所以并发能力很强,性能极其好,延时很低 MQ功能较为完善,还是分布式的,扩展性好 功能较为简单,主要支持简单的MQ功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准
        优劣势总结 非常成熟,功能强大,在业内大量的公司以及项目中都有应用。偶尔会有较低概率丢失消息。而且现在社区以及国内应用都越来越少,官方社区现在对ActiveMQ 5.x维护越来越少,几个月才发布一个版本,而且确实主要是基于解耦和异步来用的,较少在大规模吞吐的场景中使用 erlang语言开发,性能极其好,延时很低;吞吐量到万级,MQ功能比较完备;而且开源提供的管理界面非常棒,用起来很好用;社区相对比较活跃,几乎每个月都发布几个版本分;在国内一些互联网公司近几年用rabbitmq也比较多一些;但是问题也是显而易见的,RabbitMQ确实吞吐量会低一些,这是因为他做的实现机制比较重。而且erlang开发,国内有几个公司有实力做erlang源码级别的研究和定制?如果说你没这个实力的话,确实偶尔会有一些问题,你很难去看懂源码,你公司对这个东西的掌控很弱,基本职能依赖于开源社区的快速维护和修复bug。而且rabbitmq集群动态扩展会很麻烦,不过这个我觉得还好。其实主要是erlang语言本身带来的问题。很难读源码,很难定制和掌控。 接口简单易用,而且毕竟在阿里大规模应用过,有阿里品牌保障。日处理消息上百亿之多,可以做到大规模吞吐,性能也非常好,分布式扩展也很方便,社区维护还可以,可靠性和可用性都是ok的,还可以支撑大规模的topic数量,支持复杂MQ业务场景,而且一个很大的优势在于,阿里出品都是java系的,我们可以自己阅读源码,定制自己公司的MQ,可以掌控。社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些,然后接口这块不是按照标准JMS规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用RocketMQ挺好的 kafka的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展;同时kafka最好是支撑较少的topic数量即可,保证其超高吞吐量;而且kafka唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略,这个特性天然适合大数据实时计算以及日志收集

缓存服务

  • 阿里 Tair
  • 业界的 Redis
  • Memcached
  • Ehcache

配置中心

  • 阿里 Nacos
  • 携程 Apollo
  • 百度 Disconf

分布式事务

  • 阿里 seata
  • 腾讯 DTF

任务调度

  • 阿里 SchedulerX
  • 业界 xxl-job
    • 大众点评员工徐雪里于 2015 年发布的分布式任务调度平台,是一个轻量级分布式任务调度框架,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。
    • 架构
      • xxl_job_架构图
  • 当当 elastic-job
    • 当当开发的弹性分布式任务调度系统,功能丰富强大,采用 zookeeper 实现分布式协调,实现任务高可用以及分片,并且可以支持云开发,由两个相互独立的子项目 Elastic-Job-Lite 和 Elastic-Job-Cloud 组成。基于 Quartz ⼆次开发的。
  • 有赞 TSP

数据库层

  • 用于支持弹性扩容和分库分表的 TDDL
  • 数据库连接池 Driud
  • Binlog 同步的 Canal
    • Canal 是阿里巴巴旗下的一款开源项目,纯 Java 开发。基于数据库增量日志解析,提供增量数据订阅&消费,目前主要支持了 MySQL(也支持 MariaDB)。
  • Mycat
    • 相关问题
      • Sharding-JDBC 和 Mycat 的区别?
        • 工作层次:Sharding-JDBC 实现了 JDBC 协议,工作在 JDBC 层;Mycat 可以当做一个 MySQL 数据库使用,其实就是在 Proxy 层的。
        • 运行方式:Sharding-JDBC 只需要在工程中导入一个 Sharding-JDBC 的 jar 包,然后在配置文件中配置相应的数据源和分片策略即可;Mycat 则是需要单独提供一个端口为 8066 的服务,然后在 Mycat 的配置文件中配置相关的数据源和分片策略。
        • 开发方式:Sharding-JDBC 只需要在配置文件中进行配置即可使用;Mycat 需要在其配置文件中修改数据源等一系列参数。
        • 运维成本:Sharding-JDBC 的运维成本低,java 开发人员的维护成本高;Mycat 运维成本高,得配置 Mycat 的一系列参数以及高可用负载均衡的配置,需要一定的运维实力。
        • 支持的语言:Sharding-JDBC 只支持 java 语言;Mycat 支持实现了 JDBC 规范的语言。

其他

  • Zookeeper
    • Zookeeper 是一个开源的分布式协调服务,由雅虎公司创建,由于最初雅虎公司的内部研究小组的项目大多以动物的名字命名,所以后来就以 Zookeeper(动物管理员)来命名了,而就是由 Zookeeper 来负责这些分布式组件环境的协调工作。
    • 可以用 ZooKeeper 来做:统一配置管理、统一命名服务、分布式锁、集群管理。
    • ZooKeeper 的数据结构,跟 Unix 文件系统非常类似,可以看做是一颗树,每个节点叫做 ZNode。每一个节点可以通过路径来标识
      • ZooKeeper_结构图
      • Znode 类型:
        • 短暂/临时(Ephemeral)
          • 当客户端和服务端断开连接后,所创建的 Znode(节点)会自动删除
        • 持久(Persistent)
          • 当客户端和服务端断开连接后,所创建的 Znode(节点)不会删除
        • 临时顺序
          • ZK 会自动在这两种节点之后增加一个数字的后缀,而路径 + 数字后缀是能保证唯一的,这数字后缀的应用场景可以实现诸如分布式队列,分布式公平锁等。
        • 持久顺序
          • ZK 会自动在这两种节点之后增加一个数字的后缀,而路径 + 数字后缀是能保证唯一的,这数字后缀的应用场景可以实现诸如分布式队列,分布式公平锁等。
        • 容器
          • 容器节点是 3.5 以后新增的节点类型,只要在调用 create 方法时,指定 CreateMode 为 CONTAINER 即可创建容器的节点类型,容器节点的表现形式和持久节点是一样的,但是区别是 ZK 服务端启动后,会有一个单独的线程去扫描,所有的容器节点,当发现容器节点的子节点数量为 0 时,会自动删除该节点,除此之外和持久节点没有区别,官方注释给出的使用场景是 Container nodes are special purpose nodes useful for recipes such as leader, lock, etc. 说可以用在 leader 或者锁的场景中。
        • 持久 TTL、持久顺序 TTL
          • 带有存活时间。就是当该节点下面没有子节点的话,超过了 TTL 指定时间后就会被自动删除,特性跟上面的容器节点很像,只是容器节点没有超时时间而已,但是 TTL 启用是需要额外的配置(这个之前也有提过)配置是 zookeeper.extendedTypesEnabled 需要配置成 true,否则的话创建 TTL 时会收到 Unimplemented 的报错
      • ACL(access control list 访问控制列表)
        • zookeeper 在分布式系统中承担中间件的作用,它管理的每一个节点上都可能存储着重要的信息,因为应用可以读取到任意节点,这就可能造成安全问题,ACL 的作用就是帮助 zookeeper 实现权限控制。
        • zookeeper 的权限控制基于节点,每个 znode 可以有不同的权限。
        • 子节点不会继承父节点的权限,访问不了该节点,并不代表访问不到其子节点。
        • Schema: 鉴权策略
          • world
            • 默认方式,相当于全世界都能访问
          • digest
            • 即: “用户名+密码” 这种认证方式,也是业务中常用的
          • ip
            • 使用 IP 认证的方式
          • auth
            • 代表已经认证通过的用户(cli 中可以通过 addauth digest user:pwd 来添加当前上下文中的授权用户)
        • 授权对象
          • world
            • 只有一个 ID:“anyone”
          • digest
            • 自定义,通常是用户名:密码,在 ACl 中使用时,表达式将是 username:base64 编码的 SHA1.例如”admin:u53OoA8hprX59uwFsvQBS3QuI00=”(明文密码为123456)
          • ip
            • 通常是一个 Ip 地址或者是 Ip 段, 例如 192.168.xxx.xxx 或者 192.168.xxx.xxx/xxx
          • super
            • 与 digest 模式一样
        • 权限
          • create
            • 创建权限,授予权限的对象可以在数据节点下创建子节点;
          • read
            • 读取权限,授予权限的对象可以读取该节点的内容以及子节点的信息;
          • write
            • 更新权限,授予权限的对象可以更新该数据节点;
          • delete
            • 删除权限,授予权限的对象可以删除该数据节点的子节点;
          • admin
            • 管理者权限,授予权限的对象可以对该数据节点体进行 ACL 权限设置。
    • 集群
      • 角色介绍
        • Leader
          • Leader 不直接接受 client 的请求,但接受由其他 Follower 和 Observer 转发过来的 Client 请求,此外,Leader 还负责投票的发起和决议,即时更新状态和数据。
        • Follower
          • Follower 角色接受客户端请求并返回结果,参与 Leader 发起的投票和选举,但不具有写操作的权限。
        • Observer
          • Observer 角色接受客户端连接,将写操作转给 Leader,但 Observer 不参与投票(即不参加一致性协议的达成),只同步 Leader 节点的状态,Observer 角色是为集群系统扩展而生的。
      • ZAB(Zookeeper Atomic BroadCast)原子广播协议
        • 在 zookeeper 中,只有一台服务器机器作为 leader 机器,所以当客户端链接到机器的某一个节点时
          • 当这个客户端提交的是读取数据请求,那么当前连接的机器节点,就会把自己保存的数据返回出去。
          • 当这个客户端提交的是写数据请求时,首先会看当前连接的节点是不是 leader 节点,如果不是 leader 节点则会转发出去到 leader 机器的节点上,由 leader 机器写入,然后广播出去通知其他的节点过来同步数据
        • 在 ZAB 中的三个重点数据
          • Zxid:是 zookeeper 中的事务 ID,总长度为 64 位的长度的 Long 类型数据。其中有两部分构成前 32 位是 epoch 后 32 位是 xid
          • Epoch:每一个 leader 都会有一个这个值,表示当前 leader 获取到的最大 N 值,可以理解为“年代”
          • Xid:事务 ID,表示当前 zookeeper 集群当前提交的事物 ID 是多少(watch 机制),方便选举的过程后不会出现事务重复执行或者遗漏等一些特殊情况。
    • 监听器
      • 常见的监听场景有以下两项:
        • 监听 Znode 节点的数据变化
        • 监听子节点的增减变化
    • 用途
      • 统一配置管理
        • 问题描述
          • 比如我们现在有三个系统 A、B、C,他们有三份配置,分别是 ASystem.yml、BSystem.yml、CSystem.yml,然后,这三份配置又非常类似,很多的配置项几乎都一样。
          • 此时,如果我们要改变其中一份配置项的信息,很可能其他两份都要改。并且,改变了配置项的信息很可能就要重启系统
          • 于是,我们希望把 ASystem.yml、BSystem.yml、CSystem.yml 相同的配置项抽取出来成一份公用的配置 common.yml,并且即便 common.yml 改了,也不需要系统 A、B、C 重启。
        • 做法
          • 我们可以将 common.yml 这份配置放在 ZooKeeper 的 Znode 节点中,系统 A、B、C 监听着这个 Znode 节点有无变更,如果变更了,及时响应。
          • ZooKeeper_统一配置管理
      • 统一命名服务
        • 问题描述
          • 统一命名服务的理解其实跟域名一样,是我们为这某一部分的资源给它取一个名字,别人通过这个名字就可以拿到对应的资源。
          • 比如说,现在我有一个域名 www.java3y.com,但我这个域名下有多台机器:
            • 192.168.1.1、192.168.1.2、192.168.1.3、192.168.1.4
          • 别人访问 www.java3y.com 即可访问到我的机器,而不是通过 IP 去访问。
        • 做法
          • ZooKeeper_统一命名服务
      • 分布式锁
        • 做法
          • 系统 A、B、C 都去访问 /locks 节点
          • 访问的时候会创建带顺序号的临时/短暂(EPHEMERAL_SEQUENTIAL)节点,比如,系统 A 创建了 id_000000 节点,系统 B 创建了 id_000002 节点,系统 C 创建了 id_000001 节点。
          • 接着,拿到 /locks 节点下的所有子节点(id_000000,id_000001,id_000002),判断自己创建的是不是最小的那个节点
            • 如果是,则拿到锁。
              • 释放锁:执行完操作后,把创建的节点给删掉
            • 如果不是,则监听比自己要小 1 的节点变化
          • ZooKeeper_分布式锁
        • 例子
          • 系统 A 拿到 /locks 节点下的所有子节点,经过比较,发现自己(id_000000),是所有子节点最小的。所以得到锁。
          • 系统 B 拿到 /locks 节点下的所有子节点,经过比较,发现自己(id_000002),不是所有子节点最小的。所以监听比自己小 1 的节点 id_000001 的状态。
          • 系统 C 拿到 /locks 节点下的所有子节点,经过比较,发现自己(id_000001),不是所有子节点最小的。所以监听比自己小 1 的节点 id_000000 的状态。
          • 等到系统 A 执行完操作以后,将自己创建的节点删除(id_000000)。通过监听,系统 C 发现 id_000000 节点已经删除了,发现自己已经是最小的节点了,于是顺利拿到锁。
      • 集群状态
        • 做法
          • 三个系统 A、B、C,在 ZooKeeper 中创建临时节点
          • 只要系统 A 挂了,那 /groupMember/A 这个节点就会删除,通过监听 groupMember 下的子节点,系统 B 和 C 就能够感知到系统 A 已经挂了。
          • ZooKeeper_集群状态
        • 除了能够感知节点的上下线变化,ZooKeeper 还可以实现动态选举 Master 的功能。
          • 如果想要实现动态选举 Master 的功能,Znode 节点的类型是带顺序号的临时节点(EPHEMERAL_SEQUENTIAL)就好了。
          • Zookeeper 会每次选举最小编号的作为 Master,如果 Master 挂了,自然对应的 Znode 节点就会删除。然后让新的最小编号作为 Master,这样就可以实现动态选举的功能了。
    • 相关问题
      • 说说 Watcher 监听机制和它的原理?
        • Zookeeper 可以提供分布式数据的发布/订阅功能,依赖的就是 Watcher 监听机制。
        • 客户端可以向服务端注册 Watcher 监听,服务端的指定事件触发之后,就会向客户端发送一个事件通知。
        • 特性:
          • 一次性:一旦一个 Watcher 触发之后,Zookeeper 就会将它从存储中移除
          • 客户端串行:客户端的 Watcher 回调处理是串行同步的过程,不要因为一个 Watcher 的逻辑阻塞整个客户端
          • 轻量:Watcher 通知的单位是 WatchedEvent,只包含通知状态、事件类型和节点路径,不包含具体的事件内容,具体的时间内容需要客户端主动去重新获取数据
        • 流程
          • 客户端向服务端注册 Watcher 监听
          • 保存 Watcher 对象到客户端本地的 WatcherManager 中
          • 服务端 Watcher 事件触发后,客户端收到服务端通知,从 WatcherManager 中取出对应 Watcher 对象执行回调逻辑
      • Zookeeper 是如何保证数据一致性的?
        • Zookeeper 通过 ZAB 原子广播协议来实现数据的最终顺序一致性,他是一个类似 2PC 两阶段提交的过程。
        • 由于 Zookeeper 只有 Leader 节点可以写入数据,如果是其他节点收到写入数据的请求,则会将之转发给 Leader 节点。
        • 主要流程:
          1. Leader 收到请求之后,将它转换为一个 proposal 提议,并且为每个提议分配一个全局唯一递增的事务 ID:zxid,然后把提议放入到一个 FIFO 的队列中,按照 FIFO 的策略发送给所有的 Follower
          2. Follower 收到提议之后,以事务日志的形式写入到本地磁盘中,写入成功后返回 ACK 给 Leader
          3. Leader 在收到超过半数的 Follower 的 ACK 之后,即可认为数据写入成功,就会发送 commit 命令给 Follower 告诉他们可以提交 proposal 了
          • ZooKeeper_数据同步
        • ZAB 包含两种基本模式,崩溃恢复和消息广播
          • 整个集群服务在启动、网络中断或者重启等异常情况的时候,首先会进入到崩溃恢复状态,此时会通过选举产生 Leader 节点,当集群过半的节点都和 Leader 状态同步之后,ZAB 就会退出恢复模式。之后,就会进入消息广播的模式。
      • Zookeeper 如何进行 Leader 选举的?
        • Leader 的选举可以分为两个方面,同时选举主要包含事务 zxid 和 myid,节点主要包含 LEADING\FOLLOWING\LOOKING 3个状态。
        • 不同时期选举
          • 服务启动期间的选举
            • 过程
              1. 首先,每个节点都会对自己进行投票,然后把投票信息广播给集群中的其他节点
              2. 节点接收到其他节点的投票信息,然后和自己的投票进行比较,首先 zxid 较大的优先,如果 zxid 相同那么则会去选择 myid 更大者,此时大家都是 LOOKING 的状态
              3. 投票完成之后,开始统计投票信息,如果集群中过半的机器都选择了某个节点机器作为 leader,那么选举结束
              4. 最后,更新各个节点的状态,leader 改为 LEADING 状态,follower 改为 FOLLOWING 状态
          • 服务运行期间的选举
            • 如果开始选举出来的 leader 节点宕机了,那么运行期间就会重新进行 leader 的选举。
            1. leader 宕机之后,非 observer 节点都会把自己的状态修改为 LOOKING 状态,然后重新进入选举流程
            2. 生成投票信息(myid,zxid),同样,第一轮的投票大家都会把票投给自己,然后把投票信息广播出去
            3. 接下来的流程和上面的选举是一样的,都会优先以 zxid,然后选择 myid,最后统计投票信息,修改节点状态,选举结束
      • 选举之后又是怎样进行数据同步的?
        • 实际上 Zookeeper 在选举之后,Follower 和 Observer(统称为 Learner)就会去向 Leader 注册,然后就会开始数据同步的过程。
        • 数据同步包含 3 个主要值和 4 种形式。
        • 3 个主要值
          • PeerLastZxid:Learner 服务器最后处理的 ZXID
          • minCommittedLog:Leader 提议缓存队列中最小 ZXID
          • maxCommittedLog:Leader 提议缓存队列中最大 ZXID
        • 4 种形式
          • 直接差异化同步(DIFF 同步)
            • 流程
              1. 首先 Leader 向 Learner 发送 DIFF 指令,代表开始差异化同步,然后把差异数据(从 PeerLastZxid 到 maxCommittedLog 之间的数据)提议 proposal 发送给 Learner
              2. 发送完成之后发送一个 NEWLEADER 命令给 Learner,同时 Learner 返回 ACK 表示已经完成了同步
              3. 接着等待集群中过半的 Learner 响应了 ACK 之后,就发送一个 UPTODATE 命令,Learner 返回 ACK,同步流程结束
              • ZooKeeper_差异化同步
          • 先回滚再差异化同步(TRUNC+DIFF 同步)
            • 问题描述
              • 如果 Leader 刚生成一个 proposal,还没有来得及发送出去,此时 Leader 宕机,重新选举之后作为 Follower,但是新的 Leader 没有这个 proposal 数据。
            • 例子
              • 假设现在的 Leader 是 A,minCommittedLog=1,maxCommittedLog=3,刚好生成的一个 proposal 的 ZXID=4,然后挂了。
              • 重新选举出来的 Leader 是 B,B 之后又处理了 2 个提议,然后 minCommittedLog=1,maxCommittedLog=5。
              • 这时候A的 PeerLastZxid=4,在(1,5)之间。
            • 处理方式
              • A 要进行事务回滚,相当于抛弃这条数据,并且回滚到最接近于 PeerLastZxid 的事务,对于 A 来说,也就是 PeerLastZxid=3。
            • 流程
              • 流程和 DIFF 一致,只是会先发送一个 TRUNC 命令,然后再执行差异化 DIFF 同步。
          • 仅回滚同步(TRUNC 同步)
            • 针对 PeerLastZxid 大于 maxCommittedLog 的场景,流程和上述一致,事务将会被回滚到 maxCommittedLog 的记录。
            • 例子
              • 可以认为 TRUNC+DIFF 中的例子,新的 Leader B没有处理提议,所以 B 中 minCommittedLog=1,maxCommittedLog=3。
              • 所以 A 的 PeerLastZxid=4 就会大于 maxCommittedLog 了,也就是 A 只需要回滚就行了,不需要执行差异化同步 DIFF 了。
          • 全量同步(SNAP 同步)
            • 适用于两个场景:
              • PeerLastZxid 小于 minCommittedLog
              • Leader 服务器上没有提议缓存队列,并且 PeerLastZxid 不等于 Leader 的最大 ZXID
      • 有可能会出现数据不一致的问题吗?
        • 查询不一致
          • 因为 Zookeeper 是过半成功即代表成功,假设我们有 5 个节点,如果 123 节点写入成功,如果这时候请求访问到 4 或者 5 节点,那么有可能读取不到数据,因为可能数据还没有同步到 4、5 节点中,也可以认为这算是数据不一致的问题。
          • 解决方案可以在读取前使用 sync 命令。
        • leader 未发送 proposal 宕机
          • 这也就是数据同步说过的问题。
          • leader 刚生成一个 proposal,还没有来得及发送出去,此时 leader 宕机,重新选举之后作为 follower,但是新的 leader 没有这个 proposal。
          • 这种场景下的日志将会被丢弃。
        • leader 发送 proposal 成功,发送 commit 前宕机
          • 如果发送 proposal 成功了,但是在将要发送 commit 命令前宕机了,如果重新进行选举,还是会选择 zxid 最大的节点作为 leader,因此,这个日志并不会被丢弃,会在选举出 leader 之后重新同步到其他节点当中。
      • 如果作为注册中心,Zookeeper 和 Eureka、Consul、Nacos 有什么区别?
        • ZooKeeper_注册中心区别

Web 服务器

Servlet 服务器

  • Tomcat
  • Jetty
    • 基于 netty 实现的服务端 Nio MVC 业务开发平台,提供性能监控、日志分析、动态扩展的功能。
    • Jetty 更轻量级
      • 由于 Tomcat 除了遵循 Java Servlet 规范之外,自身还扩展了大量 JEE 特性以满足企业级应用的需求,所以 Tomcat 是较重量级的,而且配置较 Jetty 亦复杂许多。但对于大量普通互联网应用而言,并不需要用到 Tomcat 其他高级特性,所以在这种情况下,使用 Tomcat 是很浪费资源的。这种劣势放在分布式环境下,更是明显。换成 Jetty,每个应用服务器省下那几兆内存,对于大的分布式环境则是节省大量资源。而且,Jetty 的轻量级也使其在处理高并发细粒度请求的场景下显得更快速高效。
    • Jetty 更灵活
      • 体现在其可插拔性和可扩展性,更易于开发者对 Jetty 本身进行二次开发,定制一个适合自身需求的 Web Server。
  • JBoss
    • JBoss 是一个管理 EJB 的容器和服务器,但 JBoss 核心服务不包括支持 servlet/JSP 的 WEB 容器,一般与 Tomcat 或 Jetty 绑定使用。

Nginx

  • 是一个 web 服务器和反响代理服务器,用于 HTTP、HTTPS、SMTP、POP3 和 IMAP 协议。
  • 限流
    • Nginx 的限流都是基于漏桶流算法
      • 突发流量会进入到一个漏桶,漏桶会按照我们定义的速率依次处理请求,如果水流过大也就是突发流量过大就会直接溢出,则多余的请求会被拒绝。所以漏桶算法能控制数据的传输速率。
    • 限流有 3 种
      • 正常限制访问频率(正常流量)
        • 限制一个用户发送的请求,Nginx 多久接收一个请求。
        • Nginx 中使用 ngx_http_limit_req_module 模块来限制的访问频率,限制的原理实质是基于漏桶算法原理来实现的。在 nginx.conf 配置文件中可以使用 limit_req_zone 命令及 limit_req 命令限制单个IP的请求处理频率。
        • 1r/s 代表 1 秒一个请求,1r/m 一分钟接收一个请求, 如果 Nginx 这时还有别人的请求没有处理完,Nginx 就会拒绝处理该用户请求。
      • 突发限制访问频率(突发流量)
        • 限制一个用户发送的请求,Nginx 多久接收一个
        • 上面的配置一定程度可以限制访问频率,但是也存在着一个问题:如果突发流量超出请求被拒绝处理,无法处理活动时候的突发流量,这时候应该如何进一步处理呢?Nginx 提供 burst 参数结合 nodelay 参数可以解决流量突发的问题,可以设置能处理的超过设置的请求数外能额外处理的请求数。
        • 为什么就多了一个 burst=5 nodelay; 呢,多了这个可以代表 Nginx 对于一个用户的请求会立即处理前五个,多余的就慢慢来落,没有其他用户的请求我就处理你的,有其他的请求的话我 Nginx 就漏掉不接受你的请求
      • 限制并发连接数
        • Nginx 中的 ngx_http_limit_conn_module 模块提供了限制并发连接数的功能,可以使用 limit_conn_zone 指令以及 limit_conn 执行进行配置。
  • 负载均衡
    • 轮询(默认)
      • 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某个服务器宕机,能自动剔除故障系统。
    • 权重 weight
      • weight 的值越大分配到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。其次是为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。
    • ip_hash
      • 每个请求按访问 IP 的哈希结果分配,使来自同一个 IP 的访客固定访问一台后端服务器,并且可以有效解决动态网页存在的 session 共享问题
    • fair(第三方插件)
      • 必须安装 upstream_fair 模块
      • 对比 weight、ip_hash 更加智能的负载均衡算法,fair 算法可以根据页面大小和加载时间长短智能地进行负载均衡,响应时间短的优先分配。哪个服务器的响应速度快,就将请求分配到那个服务器上。
    • url_hash(第三方插件)
      • 必须安装 Nginx 的 hash 软件包
      • 按访问 url 的 hash 结果来分配请求,使每个 url 定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。
  • 问题
    • 为什么要用 Nginx?
      • 跨平台、配置简单、方向代理、高并发连接:处理 2-3 万并发连接数,官方监测能支持 5 万并发,内存消耗小:开启 10 个 Nginx 才占 150M 内存 ,Nginx 处理静态文件好,耗费内存少。
      • 而且 Nginx 内置的健康检查功能:如果有一个服务器宕机,会做一个健康检查,再发送的请求就不会发送到宕机的服务器了。重新将请求提交到其他的节点上。
      • 使用 Nginx 的话还能:
        • 节省宽带:支持 GZIP 压缩,可以添加浏览器本地缓存
        • 稳定性高:宕机的概率非常小
        • 接收用户请求是异步的