分布式原理RPC

1. 分布式理论

1.1 分布式架构系统回顾

1.1.1 分布式系统概念

分布式系统是一个硬件或软件组件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。

通俗的理解,所谓分布式系统,就是一个业务拆分成多个子业务,分布在不同的服务器节点,共同构成的系统称为分布式系统,同一个分布式系统中的服务器节点在空间部署上是可以随意分布的,这些服务器可能放在不同的机柜中,也可能在不同的机房中,甚至分布在不同的城市。
分布式系统

分布式与集群的区别:

  • 集群:多个服务器做同一个事情。
    集群
  • 分布式 :多个服务器做不同的事情 。
    分布式

分布式系统的特点:

  1. 分布性
    空间中随机分布。这些计算机可以分布在不同的机房,不同的城市,甚至不同的国家。
  2. 对等性
    分布式系统中的计算机没有主/从之分,组成分布式系统的所有节点都是对等的。
  3. 并发性
    同一个分布式系统的多个节点,可能会并发地操作一些共享的资源,诸如数据库或分布式存储。
  4. 缺乏全局时钟
    既然各个计算机之间是依赖于交换信息来进行相互通信,很难定义两件事件的先后顺序,缺乏全局时钟控制序列。
  5. 故障总是会发生
    组成分布式的计算机,都有可能在某一时刻突然间崩掉。分的计算机越多,可能崩掉一个的几率就越大。如果再考虑到设计程序时的异常故障,也会加大故障的概率。
  6. 处理单点故障
    单点SPoF(Single Point of Failure):某个角色或者功能只有某一台计算机在支撑,在这台计算机上出现的故障是单点故障。

1.1.2 分布式系统的发展

阿里巴巴发起的"去 IOE"运动 (IOE 指的是 IBM 小型机、Oracle 数据库、EMC 的高端存储)。阿里巴巴2009 年“去IOE”战略技术总监透露,截止到 2013 年 5 月 17 日阿里巴巴最后一台 IBM 小型机在支付宝下线。
为什么要去IOE

  1. 升级单机处理能力的性价比越来越低
  2. 单机处理能力存在瓶颈
  3. 稳定性和可用性这两个指标很难达到

1.1.3 分布式系统的演进

完整分布式系统架构

1.2 分布式系统面临的问题

  1. 通信异常
    网络本身的不可靠性,因此每次网络通信都会伴随着网络不可用的风险(光纤、路由、DNS等硬件设备或系统的不可用),都会导致最终分布式系统无法顺利进行一次网络通信,另外,即使分布式系统各节点之间的网络通信能够正常执行,其延时也会大于单机操作,存在巨大的延时差别,也会影响消息的收发过程,因此消息丢失和消息延迟变的非常普遍。
    通信异常

  2. 网络分区
    网络之间出现了网络不连通,但各个子网络的内部网络是正常的,从而导致整个系统的网络环境被切分成了若干个孤立的区域,分布式系统就会出现局部小集群,在极端情况下,这些小集群会独立完成原本需要整个分布式系统才能完成的功能,包括数据的事务处理,这就对分布式一致性提出非常大的挑战。
    网络分区

  3. 节点故障
    节点故障是分布式系统下另一个比较常见的问题,指的是组成分布式系统的服务器节点出现的宕机或"僵死"现象,根据经验来说,每个节点都有可能出现故障,并且经常发生。
    节点故障

  4. 三态
    分布式系统每一次请求与响应存在特有的“三态”概念,即成功、失败和超时

    分布式系统中,由于网络是不可靠的,虽然绝大部分情况下,网络通信能够接收到成功或失败的响应,但当网络出现异常的情况下,就会出现超时现象,通常有以下两种情况:

    1. 由于网络原因,该请求并没有被成功的发送到接收方,而是在发送过程就发生了丢失现象。
    2. 该请求成功的被接收方接收后,并进行了处理,但在响应反馈给发送方过程中,发生了消息丢失现象。
      三态
  5. 重发
    分布式系统在发生调用的时候可能会出现 失败 超时 的情况.,这个时候需要重新发起调用。
    重发

  6. 幂等
    一次和多次请求某一个资源对于资源本身应该具有同样的结果(网络超时等问题除外)。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。

    • 场景1:
      幂等1
    • 场景2:
      幂等2
    • 场景3:
      非幂等

1.3 分布式理论

1.3.1 一致性

  1. 什么是分布式一致性
    分布式数据一致性,指的是数据在多份副本中存储时,各副本中的数据是一致的。
    分布式一致性

  2. 副本一致性
    分布式系统当中,数据往往会有多个副本。如果是一台数据库处理所有的数据请求,那么通过ACID四原则,基本可以保证数据的一致性。而多个副本就需要保证数据会有多份拷贝。这就带来了同步的问题,因为我们几乎没有办法保证可以同时更新所有机器当中的包括备份所有数据。

    由于网络延迟,即使我们在同一时间给所有机器发送了更新数据的请求,也不能保证这些请求被响应的时间保持一致存在时间差,就会存在某些机器之间的数据不一致的情况。
    分布式理论:一致性问题
    总得来说,我们无法找到一种能够满足分布式系统所有系统属性的分布式一致性解决方案。因此,如何既保证数据的一致性,同时又不影响系统运行的性能,是每一个分布式系统都需要重点考虑和权衡的。于是,一致性级别由此诞生。

  3. 一致性分类

    • 强一致性
      这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大。但是强一致性很难实现。

    • 弱一致性
      这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。

    • 最终一致性
      最终一致性也是弱一致性的一种,它无法保证数据更新后,所有后续的访问都能看到最新数值,而是需要一个时间,在这个时间之后可以保证这一点(就是在一段时间后,节点间的数据会最终达到一致状态),而在这个时间内,数据也许是不一致的,这个系统无法保证强一致性的时间片段被称为「不一致窗口」。不一致窗口的时间长短取决于很多因素,比如备份数据的个数、网络传输延迟速度、系统负载等。
      最终一致性
      最终一致性在实际应用中又有多种变种:

      a. 因果一致性

      • 指的是:如果节点 A 在更新完某个数据后通知了节点 B,那么节点 B 之后对该数据的访问和修改都是基于 A 更新后的值。于此同时,和节点 A 无因果关系的节点 C 的数据访问则没有这样的限制。


      因果一致性

      b. 读己之所写一致性

      • 当进程A自己更新一个数据项之后,它总是访问到更新过的值,绝不会看到旧值。这是因果一致性模型的一个特例。


      读已之所写一致性


      1. 读写一致性

      • 用户读取自己写入结果的一致性,保证用户永远能够第一时间看到自己更新的内容。
      • 比如我们发一条朋友圈,朋友圈的内容是不是第一时间被朋友看见不重要,但是一定要显示在自己的列表上。


      解决方案:

      • 方案1:一种方案是对于一些特定的内容我们每次都去主库读取。 (问题主库压力大)
      • 方案2:我们设置一个更新时间窗口,在刚刚更新的一段时间内,我们默认都从主库读取,过了这个窗口之后,我们会挑选最近有过更新的从库进行读取
      • 方案3:我们直接记录用户更新的时间戳,在请求的时候把这个时间戳带上,凡是最后更新时间小于这个时间戳的从库都不予以响应。

      c. 会话一致性

      • 它把访问存储系统的进程放到会话的上下文中。只要会话还存在,系统就保证“读己之所写”一致性。如果由于某些失败情形令会话终止,就要建立新的会话,而且系统的保证不会延续到新的会话。


      会话一致性

      d. 单调读一致性

      • 本次读到的数据不能比上次读到的旧。
        由于主从节点更新数据的时间不一致,导致用户在不停地刷新的时候,有时候能刷出来,再次刷新之后会发现数据不见了,再刷新又可能再刷出来,就好像遇见灵异事件一样。


      解决方案:

      • 根据用户ID计算一个hash值,再通过hash值映射到机器。同一个用户不管怎么刷新,都只会被映射到同一台机器上。这样就保证了不会读到其他从库的内容,带来用户体验不好的影响。


      单调读一致性

      e. 单调写一致性

      • 系统保证对同一个进程的写操作串行化。


      单调写一致性

  4. 一致性模型图
    一致性模型

1.3.2 CAP定理

  1. CAP定理介绍
    CAP 理论含义是,一个分布式系统不可能同时满足 一致性(C:Consistency)可用性(A: Availability)分区容错性(P:Partition tolerance) 这三个基本需求,最多只能同时满足其中的2个

    选项描述
    C 一致性一致性指的是所有节点的数据一致,或者说是所有副本的数据一致,所有节点访问时都是同一份最新的数据副本
    A 可用性每次请求都能获取到非错的响应,但是不保证获取的数据为最新数据
    P 分区容错性分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障

    以下图商品服务为例:
    商品服务

    C - Consistency(一致性)
    这里指的是强一致性
    在写操作完成后开始的任何读操作都必须返回该值,或者后续写操作的结果。也就是说,在一致性系统中,一旦客户端将值写入任何一台服务器并获得响应,那么之后client从其他任何服务器读取的都是刚写入的数据。

    商品信息读写要满足一致性需要实现如下目标:

    1. 商品服务写入主数据库成功, 则想从数据库查询数据也成功
    2. 商品服务写入主数据库失败,则向从数据库查询也失败


    如何实现一致性?

    1. 写入主数据库后要数据同步到从数据库
    2. 写入主数据库后,在向从数据库同步期间要将从数据库锁定,等待同步完成后在释放锁,以免在写新数据后,向从数据库查询到旧的数据。


    分布式一致性的特点

    1. 由于存在数据库同步过程,写操作的响应会有一定的延迟
    2. 为了保定数据的一致性,对资源暂时锁定,待数据同步完成后释放锁定资源
    3. 如果请求数据同步失败的节点则会返回错误信息,一定不会返回旧数据。


    一致性

    A - Availability(可用性)
    系统中非故障节点收到的每个请求都必须有响应。在可用系统中,如果我们的客户端向服务器发送请求,并且服务器未崩溃,则服务器必须最终响应客户端,不允许服务器忽略客户的请求。


    商品信息读写要满足可用性需要实现如下目标:

    1. 从数据库接收到数据库查询的请求则立即能够响应数据查询结果
    2. 从数据库不允许出现响应超时或错误


    如何实现可用性?

    1. 写入主数据库后要将数据同步到从数据
    2. 由于要保证数据库的可用性,不可以将数据库中资源锁定
    3. 即使数据还没有同步过来,从数据库也要返回查询数据, 哪怕是旧数据,但不能返回错误和超时

    P - Partition tolerance(分区容错性)
    分布式系统的各个节点部署在不同的子网中,不可避免的会出现由于网络问题导致节点之间通信失败,此时仍可以对外提供服务,,这个就是分区容错性 (分区容忍性)。


    商品信息读写要满足分区容错性需要实现如下目标:

    1. 主数据库向从数据库同步数据失败不影象写操作
    2. 其中一个节点挂掉不会影响另一个节点对外提供服务


    如何实现分区容错性?

    1. 尽量使用异步取代同步操作,举例 使用异步方式将数据从主数据库同步到从数据库,这样节点之间能有效的实现松耦合;
    2. 添加数据库节点,其中一个从节点挂掉,由其他从节点提供服务

    CAP在各种软件中的应用:
    CAP应用

    CAP只能 3 选 2

  2. CAP三者不可能同时满足论证:
    假设有一个系统如下:

    CAP系统

    有用户向N1发送了请求更改了数据,将数据库从V0更新成了V1。由于网络断开,所以N2数据库依然是V0,如果这个时候有一个请求发给了N2,但是N2并没有办法可以直接给出最新的结果V1,这个时候该怎么办呢?

    这个时候无法两种方法,一种是将错就错,将错误的V0数据返回给用户。第二种是阻塞等待,等待网络通信恢复,N2中的数据更新之后再返回给用户。显然前者牺牲了一致性,后者牺牲了可用性。

    这个例子虽然简单,但是说明的内容却很重要。在分布式系统当中,CAP三个特性我们是无法同时满足的,必然要舍弃一个。三者舍弃一个,显然排列组合一共有三种可能。

    1. 舍弃A(可用性),保留CP(一致性和分区容错性)
      一个系统保证了一致性和分区容错性,舍弃可用性。
      也就是说在极端情况下,允许出现系统无法访问的情况出现,这个时候往往会牺牲用户体验,让用户保持等待,
      一直到系统数据一致了之后,再恢复服务。
      
    2. 舍弃C(一致性),保留AP(可用性和分区容错性)
      这种是大部分的分布式系统的设计,保证高可用和分区容错,但是会牺牲一致性。
      
    3. 舍弃P(分区容错性),保留CA(一致性和可用性)
      如果要舍弃P,那么就是要舍弃分布式系统,CAP也就无从谈起了。可以说P是分布式系统的前提,所以这
      种情况是不存在的。
      
  3. CAP三者如何权衡
    三选二利弊如何:

    • CA (Consistency + Availability):关注一致性可用性,它需要非常严格的全体一致的协议。CA系统不能容忍网络错误或节点错误,一旦出现这样的问题,整个系统就会拒绝写请求,因为它并不知道对面的那个结点是否挂掉了,还是只是网络问题。唯一安全的做法就是把自己变成只读的。
    • CP (consistency + partition tolerance):关注一致性分区容忍性。它关注的是系统里大多数人的一致性协议。这样的系统只需要保证大多数结点数据一致,而少数的结点会在没有同步到最新版本的数据时变成不可用的状态。这样能够提供一部分的可用性。
    • AP (availability + partition tolerance):这样的系统关心可用性分区容忍性。因此,这样的系统不能达成一致性,需要给出数据冲突,给出数据冲突就需要维护数据版本。

    如何进行三选二:

    放弃了一致性,满足分区容错,那么节点之间就有可能失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会容易导致全局数据不一致性。
    对于互联网应用来说,机器数量庞大,节点分散,网络故障再正常不过了,那么此时就是保障AP,放弃C的场景,而从实际中理解,像网站这种偶尔没有一致性是能接受的,但不能访问问题就非常大了。

    对于银行来说,就是必须保证强一致性,也就是说C必须存在,那么就只用CA和CP两种情况,当保障强一致性和可用性(CA),那么一旦出现通信故障,系统将完全不可用。另一方面,如果保障了强一致性和分区容错(CP),那么就具备了部分可用性。
    实际究竟应该选择什么,是需要通过业务场景进行权衡的(并不是所有情况都是CP好于CA,只能查看信息但不能更新信息有时候还不如直接拒绝服务)

1.3.3 BASE 理论

上面我们讲到CAP 不可能同时满足,而分区容错性是对于分布式系统而言,是必须的。最后,我们说,如果系统能够同时实现 CAP 是再好不过的了,所以出现了 BASE 理论。
什么是BASE理论?
BASE:全称是 Basically Available(基本可用),So state(软状态), Eventually consistent(最终一致性)三个短语的缩写,来自 ebay 的架构师提出。

BASE是对CAP中一致性和可用性权衡的结果,BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性

  1. Basically Available(基本可用)
    基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性——但请注意,这绝不等价于系统不可用。以下就是两个"基本可用"的例子:

    • 响应时间上的损失:正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了1~2秒。
    • 功能上的损失:正常情况下,在一个电子商务网站(比如淘宝)上购物,消费者几乎能够顺利地完成每一笔订单。但在一些节日大促购物高峰的时候(比如双十一、双十二),由于消费者的购物行为激增,为了保护系统的稳定性(或者保证一致性),部分消费者可能会被引导到一个降级页面,如下:
      功能上的损失
  2. So state(软状态)
    什么是软状态呢?相对于一致性,要求多个节点的数据副本都是一致的,这是一种 “硬状态”。

    软状态指的是:允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本之间进行数据同步的过程中存在延迟。

  3. Eventually consistent(最终一致性)
    最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

1.3.4 分布式事务

  1. 数据库事务回顾
    事务的基本特性:我们知道事务有4个非常重要的特性,即我们常说的(ACID)。

    • Atomicity(原子性):是说事务是一个不可分割的整体,所有操作要么全做,要么全不做;只要事务中有一个操作出错,回滚到事务开始前的状态的话,那么之前已经执行的所有操作都是无效的,都应该回滚到开始前的状态。
    • Consistency(一致性):是说事务执行前后,数据从一个状态到另一个状态必须是一致的,比如A向B转账(A、B的总金额就是一个一致性状态),不可能出现A扣了钱,B却没收到的情况发生。
    • Isolation(隔离性):多个并发事务之间相互隔离,不能互相干扰。
      关于事务的隔离性,可能不是特别好理解,这里的并发事务是指两个事务操作了同一份数据的情况;而对于并发事务操作同一份数据的隔离性问题,则是要求不能出现脏读、幻读的情况,即事务A不能读取事务B还没有提交的数据,或者在事务A读取数据进行更新操作时,不允许事务B率先更新掉这条数据。而为了解决这个问题,常用的手段就是加锁了,对于数据库来说就是通过数据库的相关锁机制来保证。
    • Durablity(持久性):事务完成后,对数据库的更改是永久保存的。
  2. 什么是分布式事务?
    其实分布式事务从实质上看与数据库事务的概念是一致的,既然是事务也就需要满足事务的基本特性(ACID),只是分布式事务相对于本地事务而言其表现形式有很大的不同。

1.4 分布式一致性协议

1.4.1 一致性协议 2PC

  1. 什么是 2PC
    2PC ( Two-Phase Commit缩写)即两阶段提交协议是将整个事务流程分为两个阶段,准备阶段(Preparephase)、提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。

    在计算机中部分关系数据库如Oracle、MySQL支持两阶段提交协议。

    2PC
    两个阶段过程:

    1. 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事务,并写本地的Undo/Redo日志,此时事务没有提交。 (Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数 据文件)
    2. 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。

    协议说明:
    顾名思义,二阶段提交就是将事务的提交过程分成了两个阶段来进行处理。

  2. 2PC执行流程
    成功执行事务事务提交流程:

    成功提交事务流程
    阶段一

    1. 事务询问
      协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
    2. 执行事务 (写本地的Undo/Redo日志)
    3. 各参与者向协调者反馈事务询问的响应


    总结: 各个参与者进行投票是否让事务进行。


    阶段二

    1. 发送提交请求:
      协调者向所有参与者发出 commit 请求。
    2. 事务提交:
      参与者收到 commit 请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行期间占用的事务资源。
    3. 反馈事务提交结果:
      参与者在完成事务提交之后,向协调者发送 Ack 信息。
    4. 完成事务:
      协调者接收到所有参与者反馈的 Ack 信息后,完成事务。

    中断事务步骤如下:

    假如任何一个参与者向协调者反馈了No响应,或者在等待超时之后,协调者尚无法接收到所有参与者的反馈响应,那么就会中断事务。
    事务中断流程
    阶段一:

    1. 事务询问
      协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
    2. 执行事务 (写本地的Undo/Redo日志)
    3. 各参与者向协调者反馈事务询问的响应
      总结: 各个参与者进行投票是否让事务进行。


    阶段二:

    1. 发送回滚请求:
      协调者向所有参与者发出 Rollback 请求。
    2. 事务回滚:
      参与者接收到 Rollback 请求后,会利用其在阶段一中记录的 Undo 信息来执行事务回滚操作,并在完成回滚之后释放在整个事务执行期间占用的资源。
    3. 反馈事务回滚结果:
      参与者在完成事务回滚之后,向协调者发送 Ack 信息。
    4. 中断事务:
      协调者接收到所有参与者反馈的 Ack 信息后,完成事务中断。


    从上面的逻辑可以看出,二阶段提交就做了2个事情:投票,执行。

    Tip: 什么是Ack
    ACK 确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。

  3. 2PC 优点缺点

    优点
    原理简单,实现方便

    缺点:同步阻塞,单点问题,数据不一致,过于保守

    • 同步阻塞:
      二阶段提交协议存在最明显也是最大的一个问题就是同步阻塞,在二阶段提交的执行过程中,所有参与该事务操作的逻辑都处于阻塞状态,也就是说,各个参与者在等待其他参与者响应的过程中,无法进行其他操作。这种同步阻塞极大的限制了分布式系统的性能。
    • 单点问题:
      协调者在整个二阶段提交过程中很重要,如果协调者在提交阶段出现问题,那么整个流程将无法运转,更重要的是:其他参与者将会处于一直锁定事务资源的状态中,而无法继续完成事务操作。
    • 数据不一致:
      假设当协调者向所有的参与者发送 commit 请求之后,发生了局部网络异常或者是协调者在尚未发送完所有 commit请求之前自身发生了崩溃,导致最终只有部分参与者收到了 commit 请求。这将导致严重的数据不一致问题。
    • 过于保守:
      如果在二阶段提交的提交询问阶段中,参与者出现故障而导致协调者始终无法获取到所有参与者的响应信息的话,这时协调者只能依靠其自身的超时机制来判断是否需要中断事务,显然,这种策略过于保守。换句话说,二阶段提交协议没有设计较为完善的容错机制,任意一个节点失败都会导致整个事务的失败。

1.4.2 一致性协议 3PC

  1. 什么是三阶段提交
    3PC,全称 “three phase commit”,是 2PC 的改进版,将 2PC 的 “提交事务请求” 过程一分为二,共形成了由CanCommit、PreCommit和doCommit三个阶段组成的事务处理协议。
    3PC

  2. 阶段一:CanCommit

    1. 事务询问
      协调者向所有的参与者发送一个包含事务内容的canCommit请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应。
    2. 各参与者向协调者反馈事务询问的响应
      参与者在接收到来自协调者的包含了事务内容的canCommit请求后,正常情况下,如果自身认为可以顺利执行事务,则反馈Yes响应,并进入预备状态,否则反馈No响应。
  3. 阶段二:PreCommit
    协调者在得到所有参与者的响应之后,会根据结果有2种执行操作的情况:执行事务预提交,或者中断事务。
    假如所有参与反馈的都是Yes,那么就会执行事务预提交。

    a. 执行事务预提交分为 3 个步骤:

    1. 发送预提交请求:
      协调者向所有参与者节点发出preCommit请求,并进入prepared阶段。
    2. 事务预提交:
      参与者接收到preCommit请求后,会执行事务操作,并将Undo和Redo信息记录到事务日志中。
    3. 各参与者向协调者反馈事务执行的结果:
      若参与者成功执行了事务操作,那么反馈Ack
      若任一参与者反馈了No响应,或者在等待超时后,协调者尚无法接收到所有参与者反馈,则中断事务

    b. 中断事务也分为2个步骤:

    1. 发送中断请求:
      协调者向所有参与者发出abort请求。
    2. 中断事务:
      无论是收到来自协调者的abort请求或者等待协调者请求过程中超时,参与者都会中断事务
  4. 阶段三:do Commit
    该阶段做真正的事务提交或者完成事务回滚,所以就会出现两种情况:

    a. 执行事务提交

    1. 发送提交请求:
      进入这一阶段,假设协调者处于正常工作状态,并且它接收到了来自所有参与者的Ack响应,那么他将从预提交状态转化为提交状态,并向所有的参与者发送doCommit请求。
    2. 事务提交:
      参与者接收到doCommit请求后,会正式执行事务提交操作,并在完成提交之后释放整个事务执行过程中占用的事务资源。
    3. 反馈事务提交结果:
      参与者在完成事务提交后,向协调者发送Ack响应。
    4. 完成事务:
      协调者接收到所有参与者反馈的Ack消息后,完成事务。

    b. 中断事务

    1. 发送中断请求:协调者向所有的参与者节点发送abort请求。
    2. 事务回滚:参与者收到abort请求后,会根据记录的Undo信息来执行事务回滚,并在完成回滚之后释放整个事务执行期间占用的资源。
    3. 反馈事务回滚结果:参与者在完成事务回滚后,向协调者发送Ack消息。
    4. 中断事务:协调者接收到所有参与者反馈的Ack消息后,中断事务。


    注意:一旦进入阶段三,可能会出现 2 种故障:

    1. 协调者出现问题
    2. 协调者和参与者之间的网络故障


    如果出现了任一一种情况,最终都会导致参与者无法收到 doCommit 请求或者 abort 请求,针对这种情况,参与者都会在等待超时之后,继续进行事务提交。

    • 若第三阶段执行的是doCommit请求,如果有参与者正常接收到了doCommit请求,那会正常执行提交事务;如果出现以上2种故障,导致参与者没有接收到doCommit请求,那参与者会在超时后自动执行doCommit请求,这个时候数据是一致的
    • 若第三阶段执行的是回滚abort请求,正常接收到请求的会执行回滚abort请求,没有接收到的会在超时后自动执行doCommit请求,这个时候就会导致数据不一致
  5. 2PC对比3PC

    1. 首先对于协调者和参与者都设置了超时机制(在2PC中,只有协调者拥有超时机制,即如果在一定时间内没有收到参与者的消息则默认失败),主要是避免了参与者在长时间无法与协调者节点通讯(协调者挂掉了)的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
    2. 通过CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的 。
    3. PreCommit是一个缓冲,保证了在最后提交阶段之前各参与节点的状态是一致的。

    问题:3PC协议并没有完全解决数据不一致问题。

1.4.3 NWR协议

  1. 什么是NWR协议
    NWR是一种在分布式存储系统中用于控制一致性级别的一种策略。在亚马逊的云存储系统中,就应用NWR来控制一致性。

    • N:node,在分布式存储系统中,有多少份备份数据
    • W:write,代表一次成功的更新操作要求至少有w份数据写入成功
    • R: read,代表一次成功的读数据操作要求至少有R份数据成功读取
  2. 原理
    NWR值的不同组合会产生不同的一致性效果,当W+R>N的时候,整个系统对于客户端来讲能保证强一致性。
    以常见的N=3、W=2、R=2为例:

    • N=3,表示,任何一个对象都必须有三个副本

      在分布式系统中,数据的单点是不允许存在的。即线上正常存在的备份数量N设置1的情况是非常危险的,因为一旦这个备份发生错误,就 可能发生数据的永久性错误。假如我们把N设置成为2,那么,只要有一个存储节点发生损坏,就会有单点的存在。所以N必须大于2。N越高,系统的维护和整体 成本就越高。工业界通常把N设置为3。

    • W=2表示,对数据的修改操作只需要在3个副本中的2个上完成就返回
    • R=2表示,从三个对象中要读取到2个数据对象,才能返回

    1. 当W是2、R是2的时候,W+R>N,这种情况对于客户端就是强一致性的。
      NWR1
      在上图中,如果R+W>N,则读取操作和写入操作成功的数据一定会有交集(如图中的Node2),这样就可以保证一定能够读取到最新版本的更新数据,数据的强一致性得到了保证。在满足数据一致性协议的前提下,R或者W设置的越大,则系统延迟越大,因为这取决于最慢的那份备份数据的响应时间。
    2. 当R+W<=N,无法保证数据的强一致性
      NWR2
      因为成功写和成功读集合可能不存在交集,这样读操作无法读取到最新的更新数值,也就无法保证
      数据的强一致性。

1.4.4 Gossip 协议

  1. 什么是Gossip 协议
    Gossip 协议也叫 Epidemic 协议 (流行病协议)。原本用于分布式数据库中节点同步数据使用,后被广泛用于数据库复制、信息扩散、集群成员身份确认、故障探测等。

    从 gossip 单词就可以看到,其中文意思是八卦、流言等意思,我们可以想象下绯闻的传播(或者流行病的传播);gossip 协议的工作原理就类似于这个。
    gossip 协议利用一种随机的方式将信息传播到整个网络中,并在一定时间内使得系统内的所有节点数据一致。Gossip 其实是一种去中心化思路的分布式协议,解决状态在集群中的传播和状态一致性的保证两个问题。
    Gossip协议

  2. Gossip原理
    Gossip 协议的消息传播方式有两种:反熵传播谣言传播

    • 反熵传播
      是以固定的概率传播所有的数据。所有参与节点只有两种状态:Suspective(病原)、Infective(感染)。过程是种子节点会把所有的数据都跟其他节点共享,以便消除节点之间数据的任何不一致,它可以保证最终、完全的一致。
      缺点是消息数量非常庞大,且无限制;通常只用于新加入节点的数据初始化。
    • 谣言传播
      是以固定的概率仅传播新到达的数据。所有参与节点有三种状态:Suspective(病原)、Infective(感染)、Removed(愈除)。过程是消息只包含最新 update,谣言消息在某个时间点之后会被标记为 removed,并且不再被传播。
      缺点是系统有一定的概率会不一致,通常用于节点间数据增量同步。
  3. 通信方式
    Gossip 协议最终目的是将数据分发到网络中的每一个节点。根据不同的具体应用场景,网络中两个节点之间存在三种通信方式:推送模式、拉取模式、推/拉模式。

    • Push
      节点 A 将数据 (key,value,version) 及对应的版本号推送给 B 节点,B 节点更新 A 中比自己新的数据
      Gossip--Push
    • Pull
      A 仅将数据 key, version 推送给 B,B 将本地比 A 新的数据(Key, value, version)推送给 A,A 更新本地。
      Gossip--Pull
    • Push/Pull
      与 Pull 类似,只是多了一步,A 再将本地比 B 新的数据推送给 B,B 则更新本地
  4. 优缺点
    综上所述,我们可以得出 Gossip 是一种去中心化的分布式协议,数据通过节点像病毒一样逐个传播。因为是指数级传播,整体传播速度非常快。

    优点

    • 扩展性:允许节点的任意增加和减少,新增节点的状态 最终会与其他节点一致
    • 容错:任意节点的宕机和重启都不会影响 Gossip 消息的传播,具有天然的分布式系统容错特性
    • 去中心化:无需中心节点,所有节点都是对等的,任意节点无需知道整个网络状况,只要网络连通,任意节点可把消息散播到全网
    • 最终一致性:Gossip 协议实现信息指数级的快速传播,因此在有新信息需要传播时,消息可以快速地发送到全局节点,在有限的时间内能够做到所有节点都拥有最新的数据。

    缺点

    • 消息延迟:节点随机向少数几个节点发送消息,消息最终是通过多个轮次的散播而到达全网;不可避免的造成消息延迟。
    • 消息冗余:节点定期随机选择周围节点发送消息,而收到消息的节点也会重复该步骤;不可避免的引起同一节点消息多次接收,增加消息处理压力

    Gossip 协议由于以上的优缺点,所以适合于 AP 场景的数据一致性处理,常见应用有:P2P 网络通信、Redis Cluster、Consul。

1.4.5 Paxos协议

  1. 什么是Paxos
    Paxos协议其实说的就是Paxos算法, Paxos算法是基于消息传递且具有高度容错特性一致性算法是目前公认的解决分布式一致性问题最有效的算法之一

  2. Paxos 解决了什么问题
    Paxos解决的问题
    在常见的分布式系统中,总会发生诸如机器宕机或网络异常(包括消息的延迟、丢失、重复、乱序,还有网络分区)等情况。
    Paxos算法需要解决的问题就是如何在一个可能发生上述异常的分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。

    注:这里某个数据的值并不只是狭义上的某个数,它可以是一条日志,也可以是一条命(command)。。。根据应用场景不同,某个数据的值有不同的含义。

    在之前讲解2PC 和 3PC的时候在一定程度上是可以解决数据一致性问题的。 但是并没有完全解决就是协调者宕机的情况。
    Paxos
    如何解决2PC和3PC的存在的问题呢?

    1. 步骤1–引入多个协调者
      引入多个协调者
    2. 步骤–引入主协调者,以他的命令为基准
      引入主协调者
      其实在引入多个协调者之后又引入主协调者,那么这个就是最简单的一种Paxos 算法。
      Paxos的版本有: Basic Paxos , Multi Paxos, Fast-Paxos,具体落地有Raft 和zk的ZAB协议
      Paxos引入实例
  3. Basic Paxos相关概念

    1. 角色介绍

      • Client:客户端
        客户端向分布式系统发出请求,并等待响应。例如,对分布式文件服务器中文件的写请求。
      • Proposer:提案发起者
        提案者提倡客户端请求,试图说服Acceptor对此达成一致,并在发生冲突时充当协调者以推
        动协议向前发展
      • Acceptor: 决策者,可以批准提案
        Acceptor可以接受(accept)提案;并进行投票, 投票结果是否通过以多数派为准, 以如果某
        个提案被选定,那么该提案里的value就被选定了
      • Learner: 最终决策的学习者
        学习者充当该协议的复制因素(不参与投票)
    2. 决策模型

      决策模型

    3. basic paxos流程
      basic paxos流程一共分为4个步骤:

      • Prepare
        Proposer提出一个提案,编号为N,此N大于这个Proposer之前提出所有提出的编号,请求Accpetor的多数人接受这个提案。
      • Promise
        如果编号N大于此Accpetor之前接收的任提案编号则接收,否则拒绝。
      • Accept
        如果达到多数派,Proposer会发出accept请求,此请求包含提案编号和对应的内容。
      • Accepted
        如果此Accpetor在此期间没有接受到任何大于N的提案,则接收此提案内容,否则忽略。
  4. Basic Paxos流程图

    1. 无故障的basic Paxos
     无故障的basic Paxos

    2. Acceptor失败时的basic Paxos
       在下图中,多数派中的一个Acceptor发生故障,因此多数派大小变为2。在这种情况下,Basic Paxos协议仍然成功。
    Acceptor失败时的basic Paxos

    3. Proposer失败时的basic Paxos
       Proposer在提出提案之后但在达成协议之前失败。具体来说,传递到Acceptor的时候失败了,这个时候需要选出新的Proposer(提案人),那么 Basic Paxos协议仍然成功。
    Proposer失败时的basic Paxos

    4. 当多个提议者发生冲突时的basic Paxos
       最复杂的情况是多个Proposer都进行提案,导致Paxos的活锁问题。
    当多个提议者发生冲突时的basic Paxos

    针对活锁问题解决起来非常简单:只需要在每个Proposer再去提案的时候随机加上一个等待时间即可

  5. Multi-Paxos流程图
    针对basic Paxos是存在一定得问题,首先就是流程复杂,实现及其困难,其次效率低(达成一致性需要2轮RPC调用),针对basic Paxos流程进行拆分为选举和复制的过程。

    1. 第一次流程-确定Leader
    在这里插入图片描述

    2. 第二次流程-直接由Leader确认
    在这里插入图片描述

  6. Multi-Paxos角色重叠流程图
    Multi-Paxos在实施的时候会将Proposer,Acceptor和Learner的角色合并统称为“服务器”。因此,最后只有“客户端”和“服务器”。
    在这里插入图片描述

1.4.6 Raft协议

  1. 什么是Raft协议
    Paxos 是论证了一致性协议的可行性,但是论证的过程据说晦涩难懂,缺少必要的实现细节,而且工程实现难度比较高,广为人知实现只有 zk 的实现 zab 协议。

    Paxos协议的出现为分布式强一致性提供了很好的理论基础,但是Paxos协议理解起来较为困难,实现比较复杂。

    然后斯坦福大学RamCloud项目中提出了易实现,易理解的分布式一致性复制协议 Raft。Java,C++,Go 等都有其对应的实现之后出现的Raft相对要简洁很多。引入主节点,通过竞选确定主节点。节点类型:Follower、Candidate 和 Leader

    Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间(每个Follower设定的超时时间是随机生成的,一般是不同的),一般为 150ms~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段, 通过竞选阶段的投票多的人成为Leader。
    Raft协议

  2. Raft相关概念

    1. 节点状态

    • Leader(主节点):接受 client 更新请求,写入本地后,然后同步到其他副本中。
    • Follower(从节点):从 Leader 中接受更新请求,然后写入本地日志文件。对客户端提供读请求。
    • Candidate(候选节点):如果 follower 在一段时间内未收到 leader 心跳。则判断 leader可能故障,发起选主提议。节点状态从 Follower 变为 Candidate 状态,直到选主结束。


    2. termId

    • 任期号,时间被划分成一个个任期,每次选举后都会产生一个新的 termId,一个任期内只有一个 leader。


    3. RequestVote

    • 请求投票,candidate 在选举过程中发起,收到多数派响应后,成为 leader。
  3. 竞选阶段流程
    单节点是不存在数据不一致问题的。一个节点就很容易就该值达成一致性。
    如果是多个节点如何达成一致性。Raft是用于实施分布式数据一致性协议的。

    接下来开始完成整个竞选阶段流程:

    1. 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
      在这里插入图片描述
    2. 此时 A 发送投票请求给其它所有节点。
      在这里插入图片描述
    3. 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成Leader。
      在这里插入图片描述
    4. 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。
      在这里插入图片描述
  4. Leader节点宕机
    如图,当前Node B为主节点,当Node B宕机后,Node A 、Node C会重新进行超时选举,谁先超时,谁就会先成为候选者并发起投票请求,由于它本身会先给自己投上一票,在收到另一个的投票后会变成多数派,即成为最新选举出来的leader
    在这里插入图片描述

  5. 多个 Candidate 竞选
    正常情况下Leader会定期向Follower发送心跳检测包,Follower 接收到心跳包,它的随机竞选时间会重新开始计时。
    如果Follower收不到Leader发送过来的心跳检测包,那等待一个随机的竞选超时时间之后,最先超时的就会变成Candidate。

    1. 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。
      在这里插入图片描述
    2. 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个Candidate 并获得同样票数的概率很低。
      在这里插入图片描述
  6. 日志复制

    1. 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。
      在这里插入图片描述
    2. Leader 会把修改复制到所有 Follower。
      在这里插入图片描述
    3. Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。
      在这里插入图片描述
    4. 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
      在这里插入图片描述
    5. 多次日志复制情况
      在这里插入图片描述
  7. 面对网络分区,Raft甚至可以保持一致。

    1. 最初始正常情况下状态,B节点会对其他4个节点发送心跳。
      在这里插入图片描述
    2. 当出现网络分区情况,但是出现网络分区的请求后,只能对A发送心跳,同时其他三个节点会再次选出一个leader节点。
      上面3个节重新选出一个Leader,由于又进行了一轮投票选举,所以上面的分区任期term大些,所以在后面分区合并时会以term大的为准。
      在这里插入图片描述
  8. 网络分区情况日志复制
    网络分区情况日志复制工作也可以完成数据一致性。

    1. 不同分区写入数据不同。
      上面的分区有3个节点,下面的分区只有2个节点,在同时向2个分区更新数据时,遵从过半原则,由于上面的节点数超为3个是多数派,所以上面的分区更新数据会成功,下面分区的数据永远不会提交成功,最终也更新不了数据。
      在这里插入图片描述
    2. 最终E节点Termid最大成为Leader节点,同步节点数据,达成数据一致性。
      在这里插入图片描述
      分区合并后,会检查term任期,任期大的会去同步数据到编号小的

1.4.7 Lease机制

  1. 什么是Lease机制
    Lease机制,翻译过来即是租约机制,是一种在分布式系统常用的协议,是维护分布式系统数据一致性的一种常用工具。
    Lease机制有以下几个特点:

    • Lease是颁发者对一段时间内数据一致性的承诺;
    • 颁发者发出Lease后,不管是否被接收,只要Lease不过期,颁发者都会按照协议遵守承诺;
    • Lease的持有者只能在Lease的有效期内使用承诺,一旦Lease超时,持有者需要放弃执行,重新申请Lease。

    以租车为例:
    Lease机制示例

  2. Lease机制解决了什么问题
    分布式系统中,如何确认一个节点是否工作正常?如果有5副本1-5。其中1号为主副本。
    在这里插入图片描述
    在分布式中最直观的处理方法是在每个副本与主副本维护一个心跳,期望通过心跳是否存在而判断对方是否依旧存活。

    心跳方法其实根本无法解决分布式下节点是否正常的这个的这个问题。考虑如下场景:

    1. 在某个时刻Node1主节点突然出现网络抖动或者网络中断情况(注意:不是宕机),导致从节点无法接受到心跳。
      在这里插入图片描述
    2. 接下来会在剩下的副节点中选取一当主节点。
      在这里插入图片描述

    解决上述主节点网络抖动导致的主节点重新选举,有2个主节点的问题
    主要解决思路有四种:

    • 设计能容忍双主的分布式协议
    • Raft协议-通过Term版本高的同步低的.
    • 用lease机制
    • 涉及去中心化-Gossip协议
  3. Lease的原理

    1. 引入中心节点负责下发Lease
      在这里插入图片描述
      由于最开始Node1是主节点,它会向中心节点去申请一个Lease,然后中心节点会下发一个Lease,同时带上这个Lease的有效期。比如当前时间是01:00,下发的Lease的到期时间设置为01:10,即10分钟后。在这个有效期内,中心节点会一直承认Node1是主节点,不承认其它节点。
    2. 出现网络问题
      在这里插入图片描述
      在01:05期间如果出现网络抖动导致其他节点申请Lease会申请失败,因为中心节点在01:10之前都会承认有主节点,不允许其他节点在申请Lease
    3. 如果网络恢复
      在这里插入图片描述
      如果Node1在有效期内网络恢复,会重新给其它节点发送心跳检测
    4. 如果到01:10时间,主节点会进行续约操作,然后在下发新的Lease
      在这里插入图片描述
    5. 如果主节点宕机,其他节点申请Lease也会失败,承认主节点存在。
      在这里插入图片描述
    6. 副节点申请Lease,申请成功。 因为Lease过期
      在这里插入图片描述
      在01:10Node5去中心节点申请Lease成功,因为Node1申请的Lease已经过期了
  4. lease的容错

    1. 主节点宕机
      lease机制天生即可容忍网络、lease接收方的出错,时间即Lease剩余过期时长
    2. 中心节点异常
      颁发者宕机可能使得全部节点没有lease,系统处于不可用状态,解决的方法就是使用一个小集群而不是单一节点作为颁发者。
    3. 时差问题
      中心节点与主节点之间的时钟可能也存在误差,只需要中心节点考虑时钟误差即可。

    lease时间长短一般取经验值1-10秒即可。太短网络压力大,太长则收回承诺时间过长影响可用性。

  5. 应用

    1. GFS(Google 文件系统)中,Master通过lease机制决定哪个是主副本,lease在给各节点的心跳响应消息中携带。收不到心跳时,则等待lease过期,再颁发给其他节点。
    2. chubby中,paxos选主后,从节点会给主颁发lease,在期限内不选其他节点为主。另一方面,主节点给每个client节点发送lease,用于判断client死活。

1.5 分布式系统设计策略

在分布式环境下,有几个问题是普遍关心的.

  • 如何检测当前节点还活着?
  • 如何保障高可用?
  • 容错处理
  • 负载均衡

1.5.1 心跳检测

在分布式环境中,我们提及过存在非常多的节点(Node)。那么就有一个非常重要的问题,如何检测一个节点出现了故障乃至无法工作了?
通常解决这一问题是采用心跳检测的手段,如同通过仪器对病人进行一些检测诊断一样。

心跳,顾名思义,就是以固定的频率向其他节点汇报当前节点状态的方式。收到心跳,一般可以认为一个节点和现在的网络是良好的。当然,心跳汇报时,一般也会携带一些附加的状态、元数据信息,以便管理。

心跳检测
若Server没有收到Node3的心跳时,Server认为Node3失联。

但是失联是失去联系,并不确定是否是Node3故障,有可能是Node3处于繁忙状态,导致调用检测超时;也有可能是Server与Node3之间链路出现故障或闪断。所以心跳不是万能的,收到心跳可以确认节点正常,但是收不到心跳也不能认为该节点就已经宣告“死亡”。

此时,可以通过一些方法帮助Server做决定: 周期检测心跳机制累计失效检测机制

  1. 周期检测心跳机制
    Server端每间隔 t 秒向Node集群发起监测请求,设定超时时间,如果超过超时时间,则判断“死亡”。可以把该节点踢出集群。
    周期心跳检测
  2. 累计失效检测机制
    在周期检测心跳机制的基础上(即会设置检测超时时间),统计一定周期内节点的返回情况(包括超时及正确返回),以此计算节点的“死亡”概率。另外,对于宣告“濒临死亡”的节点可以发起有限次数的重试,以作进一步判断。如果超过次数则可以把该节点踢出集群。
    累计失效检测

1.5.2 高可用

  1. 高可用HA设计
    高可用(High Availability)是系统架构设计中必须考虑的因素之一,通常是指,经过设计来减少系统不能提供服务的时间 。

    可用性一年中可故障时长一天中可故障时长
    90%36.5天144分钟
    99%3.6天14.4分钟
    99.9%8.8小时86.4秒
    99.99%52.6分钟8.6秒
    99.999%5.3分钟860毫秒
    99.9999%31.5秒86毫秒

    系统高可用性的常用设计模式包括三种:主备(Master-SLave)、互备(Active-Active)和集群(Cluster)模式。

    1. 主备模式
      主备模式就是Active-Standby模式,当主机宕机时,备机接管主机的一切工作,待主机恢复正常后,按使用者的设定以自动(热备)或手动(冷备)方式将服务切换到主机上运行。在数据库部分,习惯称之为MS模式。MS模式即Master/Slave模式,这在数据库高可用性方案中比较常用,如MySQL、Redis等就采用MS模式实现主从复制。保证高可用,如图所示。
      主备模式
    2. 互备模式
      互备模式指两台主机同时运行各自的服务工作且相互监测情况。在数据库高可用部分,常见的互备是MM模式。MM模式即Multi-Master模式,指一个系统存在多个master,每个master都具有read-write能力,会根据时间戳或业务逻辑合并版本。
      互备模式
    3. 集群模式
      集群模式是指有多个节点在运行,同时可以通过主控节点分担服务请求。集群模式需要解决主控节点本身的高可用问题,一般采用主备模式。
  2. 高可用HA下"脑裂问题"
    a. 什么是脑裂?
    在高可用(HA)系统中,当联系两个节点的"心跳线"断开时(即两个节点断开联系时),本来为一个整体、动作协调的HA系统,就分裂成为两个独立的节点(即两个独立的个体)。由于相互失去了联系,都以为是对方出了故障,两个节点上的HA软件像"裂脑人"一样,“本能"地争抢"共享资源”、争起"应用服务"。

    脑裂导致的严重后果:

    • 共享资源被瓜分、两边"服务"都起不来了;
    • 两边"服务"都起来了,但同时读写"共享存储",导致数据损坏(常见如数据库轮询着的联机日志出错)。
      两个节点相互争抢共享资源,结果会导致系统混乱,数据损坏。对于无状态服务的HA,无所谓脑裂不脑裂,但对有状态服务(比如MySQL)的HA,必须要严格防止脑裂

    b. 脑裂出现的原因是什么?

    一般来说,裂脑的发生,有以下几种原因:

    • 高可用服务器各节点之间心跳线链路发生故障,导致无法正常通信。
    • 因网卡及相关驱动坏了,ip配置及冲突问题(网卡直连)。
    • 因心跳线间连接的设备故障(网卡及交换机)。
    • 因仲裁的机器出问题(采用仲裁的方案)。
    • 高可用服务器上开启了iptables防火墙阻挡了心跳消息传输。
    • 高可用服务器上心跳网卡地址等信息配置不正确,导致发送心跳失败。
    • 其他服务配置不当等原因,如心跳方式不同,心跳广插冲突、软件Bug等。

    c. 脑裂预防方案?

    • 添加冗余的心跳线 [即冗余通信的方法]
      同时用两条心跳线路 (即心跳线也HA),这样一条线路坏了,另一个还是好的,依然能传送心跳消息,尽量减少"脑裂"现象的发生几率。
    • 仲裁机制
      当两个节点出现分歧时,由第3方的仲裁者决定听谁的。这个仲裁者,可能是一个锁服务,一个共享盘或者其它什么东西。
    • Lease机制
      下发Lease,在没过期之前主节点一直不变,到期后才能重新下发。
    • 隔离(Fencing)机制
      a. 共享存储fencing:确保只有一个Master往共享存储中写数据。
      b. 客户端fencing:确保只有一个Master可以响应客户端的请求。
      c. Slave fencing:确保只有一个Master可以向Slave下发命令

1.5.3 容错性

容错顾名思义,就是IT系统对于错误包容的能力。

容错的处理是保障分布式环境下相应系统的高可用或者健壮性,一个典型的案例就是对于缓存穿透 问题的解决方案。我们来具体看一下这个例子,如图所示:
在这里插入图片描述
问题描述:
我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,或者有人恶意攻击。
如频繁发起为id为“-1”的条件进行查询,可能DB就挂掉了。

那这种问题有什么好办法解决吗?

  1. 临时存放null值
  2. 使用布隆过滤器


在这里插入图片描述

1.5.4负载均衡

负载均衡:其关键在于使用多台集群服务器共同分担计算任务,把网络请求及计算分配到集群可用的不同服务器节点上,从而达到高可用性及较好的用户操作体验。

如图,不同的用户client1、client2、client3访问应用,通过负载均衡器分配到不同的节点。
在这里插入图片描述
负载均衡器有硬件解决方案,也有软件解决方案。硬件解决方案有著名的F5,软件有LVS、HAProxy、Nginx等。

以Nginx为例,负载均衡有以下6种策略:

方式说明
轮询默认方式,每个请求会按时间顺序逐一分配到不同的后端服务器
weight权重方式,在轮询策略的基础上指定轮询的几率,权重越大,接受请求越多
ip_hash依据ip分配方式,相同的客户端的请求一直发送到相同的服务器,以保证session会话
least_conn最少连接方式,把请求转发给连接数较少的后端服务器
fair(第三方)响应时间方式,按照服务器端的响应时间来分配请求,响应时间短的优先分配
url_hash(第三方)依据URL分配方式,按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器

1.6 分布式架构服务调用

1.6.1 服务调用

和传统的单体架构相比,分布式多了一个远程服务之间的通信,不管是 soa 还是微服务,他们本质上都是对于业务服务的提炼和复用。那么远程服务之间的调用才是实现分布式的关键因素。
远程服务调用

1.6.2 实现方式

  1. HTTP 应用协议的通信框架

    1. HttpURLConnection
      java 原生 HttpURLConnection是基于http协议的,支持get,post,put,delete等各种请求方式,最常用的就是get和post。
    2. Apache Common HttpClient
      HttpClient 是Apache Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本。
      - a. 实现了所有 HTTP 的方法(GET,POST,PUT,HEAD 等)
      - b. 支持 HTTPS 协议
      - c. 支持代理服务器等
    3. OKhttp3
      OKHttp是一个当前主流的网络请求的开源框架, 用于替代HttpUrlConnection和Apache HttpClient
      - a. 支持http2.0,对一台机器的请求共享一个socket。
      - b. 采用连接池技术,可以有效的减少Http连接数量。
      - c. 无缝集成GZIP压缩技术。
      - d. 支持Response Cache,避免重复请求
      - e. 域名多IP支持
    4. RestTemplate
      Spring RestTemplate 是 Spring 提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率,所以很多客户端比如 Android或者第三方服务商都是使用 RestTemplate 请求 restful 服务。
      - a. 面向 URL 组件,必须依赖于主机 + 端口 + URI
      - b. RestTemplate 不依赖于服务接口,仅关注 REST 响应内容
      - c. Spring Cloud Feign
  2. RPC 框架
    RPC全称为remote procedure call,即远程过程调用。借助RPC可以做到像本地调用一样调用远程服务,是一种进程间的通信方式。
    常见的RPC框架有一下几种:

    1. Java RMI
      Java RMI(Romote Method Invocation)是一种基于Java的远程方法调用技术,是Java特有的一种RPC实现。
      Java MRI
    1. Hessian
      Hessian是一个轻量级的remoting onhttp工具,使用简单的方法提供了RMI的功能。相比WebService,Hessian更简单、快捷。采用的是二进制RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。
      Hessian
    1. Dubbo
      Dubbo是阿里巴巴公司开源的一个高性能优秀的服务框架,使得应用可通过高性能的 RPC 实现服务的输出和输入功能,可以和Spring框架无缝集成。Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现
      Dubbo
    1. gRPC
      gRPC是由Google公司开源的一款高性能的远程过程调用(RPC)框架,可以在任何环境下运行。该框架提供了负载均衡,跟踪,智能监控,身份验证等功能,可以实现系统间的高效连接。
      gRPC

1.6.3 跨域调用

  1. 跨域
    在分布式系统中,会有调用其他业务系统,导致出现跨域问题,跨域实质上是浏览器的一种保护处理。如果产生了跨域,服务器在返回结果时就会被浏览器拦截(注意:此时请求是可以正常发起的,只是浏览器对其进行了拦截),导致响应的内容不可用。

    产生跨域的几种情况有以下:

    当前页面URL被请求页面URL是否跨域原因
    http://www.lala.com/http://www.lala.com/index.html同源(协议, 域名, 端口号相同)
    http://www.lala.com/https://www.lala.com/index.html跨域协议不同(http / https)
    http://www.lala.com/http://www.baidu.com/跨域主域名不同(lala / baidu)
    http://www.lala.com/http://kaiwu.lala.com/跨域子域名不同(www / kaiwu)
    http://www.lala.com:8080http://www.lala.com:8090跨域端口号不同(8080 / 8090)
  2. 常见的解决方案

    1. 使用jsonp解决网站跨域
      缺点:不支持post请求,代码书写比较复杂
    2. 使用HttpClient内部转发
    3. 使用设置响应头允许跨域
      response.setHeader(“Access-Control-Allow-Origin”, “*”); 设置响应头允许跨域。
    4. 基于Nginx搭建企业级API接口网关
    5. 使用Zuul搭建微服务API接口网关
      Zuul是spring cloud中的微服务网关。网关: 是一个网络整体系统中的前置门户入口。请求首先通过网关,进行路径的路由,定位到具体的服务节点上。可以使用zuul的过滤器的请求转发去解决跨域问题

1.7 分布式服务治理

1.7.1 服务协调

分布式协调技术主要用来解决分布式环境当中多个进程之间的同步控制,让他们有序的去访问某种临界资源,防止造成"脏数据"的后果。
服务协调
分布式锁是我们分布式协调技术实现的核心内容
分布式锁两种实现方式:

  1. 基于缓存(Redis等)实现分布式锁

    • 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,释放锁的时候进行判断。
    • 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
    • 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
    SETNX :set一个key为value的字符串,返回1;若key存在,则什么都不做,返回0。
    expire: 为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
    delete :删除key
    
  2. 基于ZooKeeper实现分布式锁
    ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。

    基于ZooKeeper实现分布式锁的步骤如下:

    • 创建一个目录mylock
    • 线程A想获取锁就在mylock目录下创建临时顺序节点
    • 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁
    • 线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点
    • 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

1.7.2 服务削峰

  1. 为什么要削峰?
    主要是还是来自于互联网的业务场景,例如,春节火车票抢购,大量的用户需要同一时间去抢购;以及大家熟知的阿里双11秒杀, 短时间上亿的用户涌入,瞬间流量巨大(高并发)。

  2. 流量削峰方案
    削峰从本质上来说就是更多地延缓用户请求,以及层层过滤用户的访问需求,遵从“最后落地到数据库的请求数要尽量少”的原则。

    1. 消息队列解决削峰
     
       要对流量进行削峰,最容易想到的解决方案就是用消息队列来缓冲瞬时流量,把同步的直接调用转换成异步的间接推送,中间通过一个队列在一端承接瞬时的流量洪峰,在另一端平滑地将消息推送出去。
    在这里插入图片描述
    消息队列中间件主要解决应用耦合,异步消息, 流量削锋等问题。常用消息队列系统:目前在生产环境,使用较多的消息队列有 ActiveMQ、RabbitMQ、 ZeroMQ、Kafka、RocketMQ 等。

    在这里,消息队列就像“水库”一样,拦截上游的洪水,削减进入下游河道的洪峰流量,从而达到减免洪水灾害的目的。

    2. 流量削峰漏斗:层层削峰
     
       分层过滤其实就是采用“漏斗”式设计来处理请求的,这样就像漏斗一样,尽量把数据量和请求量一层一层地过滤和减少了。如下图所示:
    削峰漏斗

    • 分层过滤的核心思想
      通过在不同的层次尽可能地过滤掉无效请求。
      通过CDN过滤掉大量的图片,静态资源的请求。
      再通过类似Redis这样的分布式缓存过滤请求
    • 分层过滤的基本原则
      对写数据进行基于时间的合理分片,过滤掉过期的失效请求。
      对写请求做限流保护,将超出系统承载能力的请求过滤掉。
      涉及到的读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题。
      对写数据进行强一致性校验,只保留最后有效的数据。

1.7.3 服务降级

  1. 什么是服务降级?
    当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心服务正常运作或高效运作。
    服务降级
    整个架构整体的负载超出了预设的上限阈值或即将到来的流量预计将会超过预设的阈值时,为了保证重要或基本的服务能正常运行,我们可以将一些 不重要 或 不紧急 的服务或任务进行服务的 延迟使用或 暂停使用。

  2. 降级策略
    当触发服务降级后,新的交易再次到达时,我们该如何来处理这些请求呢?

    从分布式,微服务架构全局的视角来看,降级处理方案:

    • 页面降级 —— 可视化界面禁用点击按钮、调整静态页面
    • 延迟服务 —— 如定时任务延迟处理、消息入MQ后延迟处理
    • 写降级 —— 直接禁止相关写操作的服务请求
    • 读降级 —— 直接禁止相关读的服务请求
    • 缓存降级 —— 使用缓存方式来降级部分读频繁的服务接口

    针对后端代码层面的降级处理策略,我们通常使用以下几种处理措施进行降级处理:

    • 抛异常
    • 返回NULL
    • 调用Mock数据
    • 调用Fallback处理逻辑
  3. 分级降级
    结合服务能否降级的优先原则,并根据台风预警(都属于风暴预警)的等级进行参考设计,可将分布式服务架构的所有服务进行故障风暴等级划分为以下四种:
    分级降级

1.7.4 服务限流

  1. 什么是服务限流?
    限流并非新鲜事,在生活中亦无处不在,下面例举一二:

    • 博物馆:限制每天参观总人数以保护文物
    • 地铁安检:有若干安检口,乘客依次排队,工作人员根据安检快慢决定是否放人进去。遇到节假日,可以增加安检口来提高处理能力,同时增加排队等待区长度。
    • 水坝泄洪:水坝可以通过闸门控制泄洪速度。

    以上"限流"例子,可以让服务提供者稳定的服务客户。
    限流的目的是通过对并发访问请求进行限速,或者一个时间窗口内的的请求数量进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待。
    服务限流

  2. 多维度限流
    在请求到达目标服务接口的时候,可以使用多维度的限流策略,这样就可以让系统平稳度过瞬间来临的并发。
    多维度限流

  3. 限流算法

    1. 限流算法—计数器(固定窗口)
     
    计数器限制每一分钟或者每一秒钟内请求不能超过一定的次数,在下一秒钟计数器清零重新计算。
    计数器限流
    存在问题:
    客户端在第一分钟的59秒请求100次,在第二分钟的第1秒又请求了100次, 2秒内后端会受到200次请求的压力,形成了流量突刺。

    2. 限流算法—计数器(滑动窗口)
     
    滑动窗口其实是细分后的计数器,它将每个时间窗口又细分成若干个时间片段,每过一个时间片段,整个时间窗口就会往右移动一格。
    滑动窗口
    时间窗口向右滑动一格,这时这个时间窗口其实已经打满了100次,客户端将被拒绝访问。时间窗口划分的越细,滑动窗口的滚动就越平滑,限流的效果就会越精确。

    3. 限流算法—漏桶
     
    漏桶算法类似一个限制出水速度的水桶,通过一个固定大小FIFO队列+定时取队列元素的方式实现,请求进入队列后会被匀速的取出处理(桶底部开口匀速出水),当队列被占满后后来的请求会直接拒绝(水倒的太快从桶中溢出来)
    漏桶
    优点是可以削峰填谷,不论请求多大多快,都只会匀速发给后端,不会出现突刺现象,保证下游服务正常运行。
    缺点就是在桶队列中的请求会排队,响应时间拉长

    4. 限流算法—令牌桶
     
    令牌桶算法是以一个恒定的速度往桶里放置令牌(如果桶里的令牌满了就废弃),每进来一个请求去桶里找令牌,有的话就拿走令牌继续处理,没有就拒绝请求。
    令牌桶
    令牌桶的优点是可以应对突发流量,当桶里有令牌时请求可以快速的响应,也不会产生漏桶队列中的等待时间。
    缺点就是相对漏桶一定程度上减小了对下游服务的保护。

1.7.5 服务熔断

  1. 什么是服务熔断?
    熔断这一概念来源于电子工程中的断路器(Circuit Breaker)。在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断

    如果不采取熔断措施,我们的系统会怎样呢?
    举例说明:

    当前系统中有A,B,C三个服务,服务A是上游,服务B是中游,服务C是下游。它们的调用链如下:
    在这里插入图片描述
    一旦下游服务C因某些原因变得不可用,积压了大量请求,服务B的请求线程也随之阻塞。线程资源逐渐耗尽,使得服务B也变得不可用。紧接着,服务 A也变为不可用,整个调用链路被拖垮。
    在这里插入图片描述
    像这种调用链路的连锁故障,叫做雪崩

  2. 熔断机制
    在这种时候,就需要我们的熔断机制来挽救整个系统。
    熔断机制
    这里需要解释两点:

    1. 开启熔断
      在固定时间窗口内,接口调用超时比率达到一个阈值,会开启熔断。
      进入熔断状态后,后续对该服务接口的调用不再经过网络,直接执行本地的默认方法,达到服务降级的效果。
    2. 熔断恢复
      熔断不可能是永久的。当经过了规定时间之后,服务将从熔断状态回复过来,再次接受调用方的远程调用。
  3. 熔断机制实现

    1. Spring Cloud Hystrix
     
    Spring Cloud Hystrix是基于Netflix的开源框架Hystrix实现,该框架实现了服务熔断、线程隔离等一系列服务保护功能。
    对于熔断机制的实现,Hystrix设计了三种状态:

    • 熔断关闭状态(Closed)
      服务没有故障时,熔断器所处的状态,对调用方的调用不做任何限制。
    • 熔断开启状态(Open)
      在固定时间内(Hystrix默认是10秒),接口调用出错比率达到一个阈值(Hystrix默认为50%),会进入熔断开启状态。进入熔断状态后, 后续对该服务接口的调用不再经过网络,直接执行本地的fallback方法。
    • 半熔断状态(Half-Open)
      在进入熔断开启状态一段时间之后(Hystrix默认是5秒),熔断器会进入半熔断状态。所谓半熔断就是尝试恢复服务调用,允许有限的流量调用该服务,并监控调用成功率。如果成功率达到预期,则说明服务已恢复,进入熔断关闭状态;如果成功率仍旧很低,则重新进入熔断开启状态。


    三个状态的转化关系如下图:
    熔断状态转换图
    Hystrix是利用线程池对依赖的资源进行隔离。

    • 这样做的好处是:资源和资源之间可以做到彻底的隔离。
    • 缺点是:增加了线程切换的成本,还需要预先给各个资源做线程池大小的分配。

    2. Sentinel
    Sentinel 和 Hystrix 的原则是一致的:当调用链路中某个资源出现不稳定,例如,表现为timeout,异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,防止避免影响到其它的资源,最终产生雪崩的效果。


    Sentinel 熔断手段:

    • 通过并发线程数进行限制,减少不稳定资源对其它资源的影响
    • 通过响应时间对资源进行降级
    • 系统负载保护

1.7.6 服务链路追踪

  1. 什么是链路追踪?
    分布式微服务架构上通过业务来划分服务的,通过REST调用对外暴露的一个接口,可能需要很多个服务协同才能完成这个接口功能,如果链路上任何一个服务出现问题或者网络超时,都会形成导致接口调用失败。随着业务的不断扩张,服务之间互相调用会越来越复杂。
    服务调用
    随着服务的越来越多,对调用链的分析会越来越复杂。

    分布式链路追踪(Distributed Tracing),也叫 分布式链路跟踪,分布式跟踪,分布式追踪 等等。其实就是将一次分布式请求还原成调用链路。显示的在后端查看一次分布式请求的调用情况,比如各个节点上的耗时、请求具体打到了哪台机器上、每个服务节点的请求状态等等。

  2. 链路跟踪具备的功能

    1. 故障快速定位
    通过调用链跟踪,一次请求的逻辑轨迹可以用完整清晰的展示出来。开发中可以在业务日志中添加调用链ID,可以通过调用链结合业务日志快速定位错误信息。
    在这里插入图片描述

    2. 各个调用环节的性能分析
    在调用链的各个环节分别添加调用时延,可以分析系统的性能瓶颈,可以进行针对性的优化。通过分析各个环节的平均时延,QPS等信息,可以找到系统的薄弱环节,对一些模块做调整。
    平均延时QPS

    3.数据分析
    调用链绑定业务后查看具体每条业务数据对应的链路问题,可以得到用户的行为路径,经过了哪些服务器上的哪个服务,汇总分析应用在很多业务场景。

    4. 生成服务调用拓扑图
    通过可视化分布式系统的模块和他们之间的相互联系来理解系统拓扑。点击某个节点会展示这个模块的详情,比如它当前的状态和请求数量。

  3. 链路跟踪设计原则

    1. 设计目标
      低侵入性,应用透明
      低损耗
      大范围部署,扩展性
    2. 埋点和生成日志
      埋点即系统在当前节点的上下文信息,可以分为客户端埋点、服务端埋点,以及客户端和服务端双向型埋点。埋点日志通常要包含以下内容:
      TraceId、RPCId、调用的开始时间,调用类型,协议类型,调用方ip和端口,请求的服务名等信息;调用耗时,调用结果,异常信息,消息报文等。
    3. 抓取和存储日志
      日志的采集和存储有许多开源的工具可以选择,一般来说,会使用离线+实时的方式去存储日志,主要是分布式日志采集的方式。典型的解决方案如Flume结合Kafka。
    4. 分析和统计调用链数据
      一条调用链的日志散落在调用经过的各个服务器上,首先需要按 TraceId 汇总日志,然后按照RpcId 对调用链进行顺序整理。调用链数据不要求百分之百准确,可以允许中间的部分日志丢失。
    5. 计算和展示
      汇总得到各个应用节点的调用链日志后,可以针对性的对各个业务线进行分析。需要对具体日志进行整理,进一步储存在HBase或者关系型数据库中,可以进行可视化的查询。
  4. 链路跟踪Trace模型
    Trace调用模型,主要有以下概念:

    术语解释
    Trace一次完整的分布式调用跟踪链路
    Span跟踪服务调用基本结构,表示跨服务的一次调用; 多span形成树形结构,组合成一次Trace追踪记录
    Annotation在span中的标注点,记录整个span时间段内发生的事件:

    Cs CLIENT_SEND,客户端发起请求
    Cr CLIENT_RECIEVE,客户端收到响应
    Sr SERVER_RECIEVE,服务端收到请求
    Ss SERVER_SEND,服务端发送结果
    BinaryAnnotation可以认为是特殊的Annotation,用户自定义事件:
    Event 记录普通事件
    Exception 记录异常事件

    Client && Server:对于跨服务的一次调用,请求发起方为client,服务提供方为Server各术语在一次分布式调用中,关系如下图所示:
    链路追踪Trace模型
    链路跟踪系统实现:
    大的互联网公司都有自己的分布式跟踪系统,比如Google的Dapper,Twitter的zipkin,淘宝的鹰
    眼,新浪的Watchman,京东的Hydra等等。

1.8 架构设计基本原则

架构最重要的就是编程思想:

  1. 利于开发者
  2. 利于公司
  3. 利于客户

1.8.1 开闭原则

  1. 开闭原则的定义
    开闭原则:软件实体应当对扩展开放,对修改关闭,这就是开闭原则的经典定义。

    这里的软件实体包括以下几个部分:

    • 项目中划分出的模块
    • 类与接口
    • 方法

    开闭原则的含义是:当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。

  2. 开闭原则的作用
    开闭原则是面向对象程序设计的终极目标,它使软件实体拥有一定的适应性和灵活性的同时具备稳定性和延续性。

    具体来说,其作用如下:

    1. 对软件测试的影响
    2. 可以提高代码的可复用性
    3. 可以提高软件的可维护性
  3. 开闭原则的实现方法
    可以通过“抽象约束、封装变化”来实现开闭原则,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变因素封装在相同的具体实现类中。

    举例:

    1.学校有许多的课程,其中有一门课程为java课程,我们需要打印出该课程的id、名称以及售价,为此新建一个接口类ICourse和实现类JavaCourse,JavaCourse实现接口ICours
    开闭原则1

    1. 此时,正好赶上节日,我们有一个促销活动,JAVA课程我们打六折
    • 第一种选择:
      在ICours新增打折方法getDiscountPrice,这种会将所有实现ICours接口的实现类都得做改变
    • 第二种选择
      每次打折我们都修改JavaCourse类,这种代码显然是不合理的,这样破坏了原本稳定的代码
    • 第三种
      新建一个JavaDiscountCourse类继承JavaCourse,并在JavaDiscountCourse新增加一个打折即可
      开闭原则2

1.8.2 单一职责原则

  1. 单一职责原则的定义
    单一职责原则又称单一功能原则,这里的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分

    该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:

    • 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
    • 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
  2. 单一职责原则的优点
    单一职责原则的核心就是控制类的粒度大小将对象解耦提高其内聚性

    如果遵循单一职责原则将有以下优点:

    1. 降低类的复杂度
    2. 提高类的可读性
    3. 提高系统的可维护性
    4. 变更引起的风险降低
  3. 单一职责原则的实现方法
    单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中。而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。
    下面以大学学生工作管理程序为例介绍单一职责原则的应用。
    大学学生工作管理程序:
    单一职责原则

1.8.3 接口隔离原则

  1. 接口隔离原则的定义
    接口隔离原则要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法。

    接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:

    1. 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
    2. 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
  2. 接口隔离原则的优点
    接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。

    1. 提高系统的灵活性和可维护性
    2. 降低系统的耦合性。
    3. 保证系统的稳定性
    4. 使用多个专门的接口还能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
    5. 能减少项目工程中的代码冗余
  3. 接口隔离原则的实现方法
    在具体应用接口隔离原则时,应该根据以下几个规则来衡量。

    1. 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
    2. 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
    3. 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同,深入了解业务逻辑。
    4. 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

    举例:
    学生成绩管理程序,学生成绩管理程序一般包含插入成绩、删除成绩、修改成绩、计算总分、计算均分、打印成绩信息、査询成绩信息等功能:

    1. 如果将这些功能全部放到一个接口中显然不太合理
      在这里插入图片描述
    1. 正确的做法是将它们分别放在输入模块、统计模块和打印模块等 3 个模块中
      接口隔离原则

1.8.4 里氏替换原则

  1. 里氏替换原则的定义
    里氏替换原则主要阐述了有关继承的一些原则。里氏替换原则是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。总结:子类可以扩展父类的功能,但不能改变父类原有的功能

  2. 里氏替换原则的作用

    里氏替换原则的主要作用如下:

    1. 里氏替换原则是实现开闭原则的重要方式之一。
    2. 它克服了继承中重写父类造成的可复用性变差的缺点。
    3. 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
    4. 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。
  3. 里氏替换原则的实现方法
    根据上述理解,对里氏替换原则的定义可以总结如下:

    1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
    在这里插入图片描述

    public class C {
    	public int func(int a, int b){
    		return a+b;
    	}
    }
    
    public class C1 extends C{
    	@Override
    	public int func(int a, int b) {
    		return a-b;
    	}
    }
    
    public class Client{
    	public static void main(String[] args) {
    		C c = new C1();
    		System.out.println("2+1=" + c.func(2, 1));     // 这里调用c的func方法,实际是调用的子类里重写的方法
    	}
    }
    

    运行结果:2+1=1
    这里C1继承了C,C1需要增加一个减法功能,但是C1并没有新写一个方法,而是直接重写了父类的func方法,这样就违背了里氏替换原则,导致运行结果错误。

    2. 子类中可以增加自己特有的方法
    在这里插入图片描述

    public class C {
    	public int func(int a, int b){
    		return a+b;
    	}
    }
    
    public class C2 extends C{
    	// 自定义了一个func2方法
    	public int func2(int a, int b) {
    		return a-b;
    	}
    }
    
    public class Client{
    	public static void main(String[] args) {
    		C2 c = new C2();
    		System.out.println("2+1=" + c.func(2, 1));    // 调用父类里的func方法
    		System.out.println("2-1=" + c.func2(2, 1));   // 调用C2里自定义的func2方法
    	}
    }
    

    运行结果:2+1=3 , 2-1=1
    这里C2新增了一个func2方法

1.8.5 依赖倒置原则

  1. 依赖倒置原则定义
    依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程

  2. 依赖倒置原则的作用

    依赖倒置原则的主要作用如下。

    1. 可以降低类间的耦合性。
    2. 可以提高系统的稳定性。
    3. 可以减少并行开发引起的风险。
    4. 可以提高代码的可读性和可维护性。
  3. 依赖倒置原则的实现方法
    依赖倒置原则在“顾客购物程序”中的应用:

    • 本程序反映了 “顾客类”与“商店类”的关系。商店类中有 sell() 方法,顾客类通过该方法购物以下代码定义了顾客类通过韶关网店 ShaoguanShop 购物:
      class Customer {
      	public void shopping(ShaoguanShop shop) {
      		//购物
      		System.out.println(shop.sell());
      	}
      }
      
    • 但是,这种设计存在缺点,如果该顾客想从另外一家商店(如婺源网店 WuyuanShop)购物,就要将该顾客的代码修改如下:
      class Customer {
      	public void shopping(WuyuanShop shop) {
      		//购物
      		System.out.println(shop.sell());
      	}
      }
      
    • 顾客每更换一家商店,都要修改一次代码,这明显违背了开闭原则。存在以上缺点的原因是:顾客类设计时同具体的商店类绑定了,这违背了依赖倒置原则。
      class Customer {
      	public void shopping(Shop shop) {
      		//购物
      		System.out.println(shop.sell());
      	}
      }
      
      依赖倒置原则

1.8.6 迪米特法则

  1. 迪米特法则的定义
    迪米特法则又叫作最少知识原则,迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

  2. 迪米特法则的优点
    迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点。

    • 降低了类之间的耦合度,提高了模块的相对独立性。
    • 由于亲合度降低,从而提高了类的可复用性和系统的扩展性
  3. 迪米特法则的实现方法
    从迪米特法则的定义和特点可知,它强调以下两点:

    • 从依赖者的角度来说,只依赖应该依赖的对象。
    • 从被依赖者的角度说,只暴露应该暴露的方法。

    案例:明星与经纪人的关系实例。

    分析:明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则
    迪米特法则

1.8.7 合成复用原则

  1. 合成复用原则的定义
    合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

    如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。

  2. 合成复用原则的重要性
    通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。

    缺点:

    1. 继承复用破坏了类的封装性
    2. 子类与父类的耦合度高
    3. 它限制了复用的灵活性

    采用合成复用原则时,他可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调已用有对象的功能,它有以下优点。

    优点:

    1. 它维持了类的封装性
    2. 新旧类之间的耦合度低
    3. 复用的灵活性高
  3. 合成复用原则的实现方法
    合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用。

    下面以汽车分类管理程序为例来介绍合成复用原则的应用。
    分析:汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。 所示是用继承关系实现的汽车分类的图。
    合成复用原则
    从上图 可以看出用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。但如果改用组合关系实现就能很好地解决以上问题。
    合成复用原则

2. RPC框架设计

2.1 Socket回顾与I/0模型

2.1.1 Socket网络编程回顾

  1. Socket概述
    Socket,套接字,就是两台主机之间逻辑连接的端点。
    TCP/IP协议是传输层协议,主要解决数据如何在网络中传输,而HTTP是应用层协议,主要解决如何包装数据。
    Socket是通信的基石,是支持TCP/IP协议的网络通信的基本操作单元。它是网络通信过程中端点的抽象表示,包含进行网络通信必须的五种信息:连接使用的协议、本地主机的IP地址、本地进程的协议端口、远程主机的IP地址、远程进程的协议端口。

  2. Socket整体流程
    Socket编程主要涉及到客户端和服务端两个方面,首先是在服务器端创建一个服务器套接字(ServerSocket),并把它附加到一个端口上,服务器从这个端口监听连接。端口号的范围是0到65536,但是0到1024是为特权服务保留的端口号,可以选择任意一个当前没有被其他进程使用的端口。

    客户端请求与服务器进行连接的时候,根据服务器的域名或者IP地址,加上端口号,打开一个套接字。当服务器接受连接后,服务器和客户端之间的通信就像输入输出流一样进行操作。
    Socket通信流程

2.1.2 I/O模型

  1. I/O模型说明

    • I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
    • Java 共支持 3 种网络编程模型/IO 模式:BIO(同步并阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)

    1. 阻塞与非阻塞:

    • 主要指的是访问IO的线程是否会阻塞(或处于等待)
    • 线程访问资源,该资源是否准备就绪的一种处理方式


    阻塞与非阻塞

    2. 同步和异步

    • 主要是指的数据的请求方式
    • 同步和异步是指访问数据的一种机制


    同步和异步

  2. BIO(同步并阻塞)
    Java BIO就是传统的 socket编程。

    BIO(blocking I/O) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。

    工作机制:
    BIO工作机制
    生活中的例子:
    BIO生活实例

    BIO问题分析:

    1. 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write
    2. 并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
    3. 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费
  3. NIO(同步非阻塞)
    同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求就进行处理。
    NIO模型
    生活中的例子:
    NIO生活实例

  4. AIO(异步非阻塞)
    AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

    Proactor 模式是一个消息异步通知的设计模式,Proactor 通知的不是就绪事件,而是操作完成事件,
    这也就是操作系统异步 IO 的主要模型。
    

    生活中的例子:
    AIO生活实例

  5. BIO、NIO、AIO 适用场景分析

    1. BIO(同步并阻塞) 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
    2. NIO(同步非阻塞) 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4 开始支持。
    3. AIO(异步非阻塞) 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作, 编程比较复杂,JDK7 开始支持。

2.2 NIO编程

2.2.1 NIO介绍

Java NIO 全称java non-blocking IO ,是指 JDK 提供的新 API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的。

  1. NIO 有三大核心部分:Channel(通道)Buffer(缓冲区)Selector(选择器)
  2. NIO是 面向缓冲区编程的。数据读取到一个缓冲区中,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。
  3. Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。
    非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入, 这个线程同时可以去做别的事情。
    通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 10000 个请求过来,根据实际情况,可以分配50 或者 100 个线程来处理。不像之前的阻塞 IO 那样,非得分配 10000 个。

2.2.2 NIO和 BIO的比较

  1. BIO 以流的方式处理数据,而 NIO 以缓冲区的方式处理数据,缓冲区 I/O 的效率比流 I/O 高很多
  2. BIO 是阻塞的,NIO则是非阻塞的。
  3. BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求, 数据到达等),因此使用单个线程就可以监听多个客户端通道。

2.2.3 NIO 三大核心原理示意图

一张图描述 NIO 的 Selector 、 Channel 和 Buffer 的关系。

NIO模型关系示意图

  1. 每个 channel 都会对应一个 Buffer
  2. Selector 对应一个线程, 一个线程对应多个 channel(连接)
  3. 每个 channel 都注册到 Selector选择器上
  4. Selector不断轮询查看Channel上的事件,事件是通道Channel非常重要的概念
  5. Selector 会根据不同的事件,完成不同的处理操作
  6. Buffer 就是一个内存块 , 底层是有一个数组
  7. 数据的读取写入是通过 Buffer,这个和BIO 中要么是输入流,或者是输出流,不能双向,但是NIO 的 Buffer 是可以读也可以写,channel 是双向的。

2.2.4 缓冲区(Buffer)

2.2.4.1 基本介绍

缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个数组,该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer。
Buffer

2.2.4.2bBuffer常用API介绍
  1. Buffer 类及其子类
    Buffer类及子类
    在 NIO 中,Buffer是一个顶层父类,它是一个抽象类, 类的层级关系图,常用的缓冲区分别对应byte,short,int,long,float,double,char 7种。

  2. 缓冲区对象创建

    方法名说明
    static ByteBuffer allocate(长度)创建byte类型的指定长度的缓冲区
    static ByteBuffer wrap(byte[] array)创建一个有内容的byte类型缓冲区

    示例代码:

    package com.lagou.buffer;
    import java.nio.ByteBuffer;
    /**
    * 创建缓冲区
    */
    public class CreateBufferDemo {
    	public static void main(String[] args) {
    		//1.创建一个指定长度的缓冲区, 以ByteBuffer为例
    		ByteBuffer byteBuffer = ByteBuffer.allocate(5);
    		for (int i = 0; i < 5; i++) {
    			System.out.println(byteBuffer.get());
    		}
    		
    		//在此调用会报错--后续再读缓冲区时着重讲解
    		//System.out.println(byteBuffer.get());
    		//2.创建一个有内容的缓冲区
    		ByteBuffer wrap = ByteBuffer.wrap("lagou".getBytes());
    		for (int i = 0; i < 5; i++) {
    			System.out.println(wrap.get());
    		}
    	}
    }
    
  3. 缓冲区对象添加数据

    方法名说明
    int position() / position(int newPosition)获得当前要操作的索引/修改当前要操作的索引位置
    int limit() / limit(int newLimit)最多能操作到哪个索引/修改最多能操作的索引位置
    int capacity()返回缓冲区的总长度
    int remaining() / boolean hasRemaining()还有多少能操作索引个数/是否还有能操作
    put(byte b) / put(byte[] src)添加一个字节/添加字节数组

    图解:
    缓冲区对象添加图解

  4. 缓冲区对象读取数据

    方法名介绍
    flip()写切换读模式 limit设置position位置, position设置0
    get()读一个字节
    get(byte[] dst)读多个字节
    get(int index)读指定索引的字节
    rewind()将position设置为0,可以重复读
    clear()切换写模式 position设置为0 , limit 设置为 capacity
    array()将缓冲区转换成字节数组返回

    图解:flip()方法
     
    flip()方法图解

    图解:clear()方法
     clear()方法图解

    注意事项:

    1. capacity:容量(长度),limit: 界限(最多能读/写到哪里),posotion:位置(读/写哪个索引)
    2. 获取缓冲区里面数据之前,需要调用flip方法
    3. 再次写数据之前,需要调用clear方法,但是数据还未消失,等再次写入数据,被覆盖了才会消失。

2.2.5 通道(Channel)

  1. 基本介绍
    通常来说NIO中的所有IO都是从 Channel(通道) 开始的。NIO 的通道类似于流,但有些区别如下:

    1. 通道可以读也可以写,流一般来说是单向的(只能读或者写,所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流)
    2. 通道可以异步读写
    3. 通道总是基于缓冲区Buffer来读写

    通道

  2. Channel常用类介绍

    1. Channel接口
    常用 的Channel实现类有 :FileChannel,DatagramChannel ,ServerSocketChannel和SocketChannel 。
    FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写 ServerSocketChannel 和SocketChannel 用于 TCP 的数据读写。【ServerSocketChanne类似 ServerSocket , SocketChannel 类似 Socket
    Channel接口

    2. SocketChannel 与ServerSocketChannel
    类似 Socke和ServerSocket,可以完成客户端与服务端数据的通信工作。

  3. ServerSocketChannel

    服务端实现步骤:

    1. 打开一个服务端通道
    2. 绑定对应的端口号
    3. 通道默认是阻塞的,需要设置为非阻塞
    4. 检查是否有客户端连接,有客户端连接会返回对应的通道
    5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
    6. 给客户端回写数据
    7. 释放资源

    代码实现:

    package com.lagou.channel;
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.nio.charset.StandardCharsets;
    
    /**
    * 服务端
    */
    public class NIOServer {
    	public static void main(String[] args) throws IOException, InterruptedException {
    		//1. 打开一个服务端通道
    		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    		//2. 绑定对应的端口号
    		serverSocketChannel.bind(new InetSocketAddress(9999));
    		//3. 通道默认是阻塞的,需要设置为非阻塞
    		// true 为通道阻塞 false 为非阻塞
    		serverSocketChannel.configureBlocking(false);
    		System.out.println("服务端启动成功..........");
    		while (true) {
    			//4. 检查是否有客户端连接 有客户端连接会返回对应的通道 , 否则返回null
    			SocketChannel socketChannel = serverSocketChannel.accept();
    			if (socketChannel == null) {
    				System.out.println("没有客户端连接...我去做别的事情");
    				Thread.sleep(2000);
    				continue;
    			}
    			
    			//5. 获取客户端传递过来的数据,并把数据放在byteBuffer这个缓冲区中
    			ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    			//返回值:
    			//正数: 表示本次读到的有效字节个数.
    			//0 : 表示本次没有读到有效字节.
    			//-1 : 表示读到了末尾
    			int read = socketChannel.read(byteBuffer);
    			System.out.println("客户端消息:" + new String(byteBuffer.array(), 0, read, StandardCharsets.UTF_8));
    		
    			//6. 给客户端回写数据
    			socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));
    			//7. 释放资源
    			socketChannel.close();
    		}
    	}
    }
    
  4. SocketChannel

    实现步骤

    1. 打开通道
    2. 设置连接IP和端口号
    3. 写出数据
    4. 读取服务器写回的数据
    5. 释放资源

    代码实现:

    package com.lagou.channel;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SocketChannel;
    import java.nio.charset.StandardCharsets;
    
    /**
    * 客户端
    */
    public class NIOClient {
    	public static void main(String[] args) throws IOException {
    		//1.打开通道
    		SocketChannel socketChannel = SocketChannel.open();
    		//2.设置连接IP和端口号
    		socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999));
    		//3.写出数据
    		socketChannel.write(ByteBuffer.wrap("老板, 该还钱拉!".getBytes(StandardCharsets.UTF_8)));
    		
    		//4.读取服务器写回的数据
    		ByteBuffer readBuffer = ByteBuffer.allocate(1024);
    		int read=socketChannel.read(readBuffer);
    		System.out.println("服务端消息:" + new String(readBuffer.array(), 0, read, StandardCharsets.UTF_8));
    		//5.释放资源
    		socketChannel.close();
    	}
    }
    

2.2.6 6 Selector (选择器)

  1. 基本介绍
    可以用一个线程,处理多个的客户端连接,就会使用到NIO的Selector(选择器)。Selector 能够检测多个注册的服务端通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

    线程处理
    在这种没有选择器的情况下,对应每个连接对应一个处理线程。但是连接并不能马上就会发送信息,所以还会产生资源浪费。

    Selector选择器
    只有在通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,避免了多线程之间的上下文切换导致的开销。

  2. 常用API介绍

    1. Selector 类是一个抽象类
    Selector抽象类

    常用方法:

    • Selector.open() :得到一个选择器对象。
    • selector.select() :阻塞,监控所有注册的通道,当有对应的事件操作时,会将SelectionKey放入集合内部并返回事件数量。
    • selector.select(1000):阻塞 1000 毫秒,监控所有注册的通道,当有对应的事件操作时, 会将SelectionKey放入集合内部并返回。
    • selector.selectedKeys() :返回存有SelectionKey的集合。

    2. SelectionKey
    SelectionKey

    常用方法

    • SelectionKey.isAcceptable():是否是连接继续事件
    • SelectionKey.isConnectable():是否是连接就绪事件
    • SelectionKey.isReadable():是否是读就绪事件
    • SelectionKey.isWritable():是否是写就绪事件


    SelectionKey中定义的4种事件

    • SelectionKey.OP_ACCEPT : 接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
    • SelectionKey.OP_CONNECT :连接就绪事件,表示客户端与服务器的连接已经建立成功
    • SelectionKey.OP_READ : 读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
    • SelectionKey.OP_WRITE : 写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)
  3. Selector 编码
    服务端实现步骤:

    1. 打开一个服务端通道
    2. 绑定对应的端口号
    3. 通道默认是阻塞的,需要设置为非阻塞
    4. 创建选择器
    5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
    6. 检查选择器是否有事件
    7. 获取事件集合
    8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()
    9. 得到客户端通道, 并将通道注册到选择器上,并指定监听事件为OP_READ
    10. 判断是否是客户端读就绪事件SelectionKey.isReadable()
    11. 得到客户端通道,读取数据到缓冲区
    12. 给客户端回写数据
    13. 从集合中删除对应的事件,因为防止二次处理。

    代码实现:

    package com.lagou.selector;
    
    import java.io.IOException;
    import java.net.InetSocketAddress;
    import java.nio.ByteBuffer;
    import java.nio.channels.SelectionKey;
    import java.nio.channels.Selector;
    import java.nio.channels.ServerSocketChannel;
    import java.nio.channels.SocketChannel;
    import java.nio.charset.StandardCharsets;
    import java.util.Iterator;
    import java.util.Set;
    
    /**
    * 服务端-选择器
    */
    public class NIOSelectorServer {
    	public static void main(String[] args) throws IOException, InterruptedException {
    		//1. 打开一个服务端通道
    		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    		//2. 绑定对应的端口号
    		serverSocketChannel.bind(new InetSocketAddress(9999));
    		//3. 通道默认是阻塞的,需要设置为非阻塞
    		serverSocketChannel.configureBlocking(false);
    		//4. 创建选择器
    		Selector selector = Selector.open();
    		
    		//5. 将服务端通道注册到选择器上,并指定注册监听的事件为OP_ACCEPT
    		serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    		System.out.println("服务端启动成功...");
    		while (true) {
    			//6. 检查选择器是否有事件
    			int select = selector.select(2000);
    			if (select == 0) {
    				continue;
    			}
    			
    			//7. 获取事件集合
    			Set<SelectionKey> selectionKeys = selector.selectedKeys();
    			Iterator<SelectionKey> iterator = selectionKeys.iterator();
    		
    			while (iterator.hasNext()) {
    			//8. 判断事件是否是客户端连接事件SelectionKey.isAcceptable()
    			SelectionKey key = iterator.next();
    		
    			//9. 得到客户端通道,并将通道注册到选择器上, 并指定监听事件为OP_READ
    			if (key.isAcceptable()) {
    				SocketChannel socketChannel = serverSocketChannel.accept();
    				System.out.println("客户端已连接......" + socketChannel);
    				//必须设置通道为非阻塞, 因为selector需要轮询监听每个通道的事件
    				socketChannel.configureBlocking(false);
    				//并指定监听事件为OP_READ
    				socketChannel.register(selector, SelectionKey.OP_READ);
    			}
    		
    			//10. 判断是否是客户端读就绪事件SelectionKey.isReadable()
    			if (key.isReadable()) {
    				//11.得到客户端通道,读取数据到缓冲区
    				SocketChannel socketChannel = (SocketChannel) key.channel();
    				ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    				int read = socketChannel.read(byteBuffer);
    				if (read > 0) {
    					System.out.println("客户端消息:" + new String(byteBuffer.array(), 0, read, StandardCharsets.UTF_8));
    					//12.给客户端回写数据
    					socketChannel.write(ByteBuffer.wrap("没钱".getBytes(StandardCharsets.UTF_8)));
    					socketChannel.close();
    				}
    			}
    			//13.从集合中删除对应的事件, 因为防止二次处理.
    			iterator.remove();
    			}
    		}
    	}
    }
    

2.3 Netty核心原理

2.3.1 Netty 介绍

  1. 原生 NIO 存在的问题

    • NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
    • 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
    • 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。
    • JDK NIO 的 Bug:臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。直到JDK 1.7版本该问题仍旧存在,没有被根本解决。

      在NIO中通过Selector的轮询当前是否有IO事件,根据JDK NIO api描述,Selector的select方法会一直阻塞,直到IO事件达到或超时,但是在Linux平台上这里有时会出现问题,在某些场景下select方法会直接返回,即使没有超时并且也没有IO事件到达,这就是著名的epoll bug,这是一个比较严重的bug,它会导致线程陷入死循环,会让CPU飙到100%,极大地影响系统的可靠性,到目前为止,JDK都没有完全解决这个问题。

  2. Netty概述
    Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序。 Netty 是一个基于 NIO 的网络编程框架,使用Netty 可以帮助你快速、简单的开发出一 个网络应用,相当于简化和流程化了 NIO 的开发过程。 作为当前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、 通信行业等获得了广泛的应用,知名的 Elasticsearch 、Dubbo 框架内部都采用了 Netty。
    Netty框架
    从图中就能看出 Netty 的强大之处:零拷贝、可拓展事件模型;支持 TCP、UDP、HTTP、
    WebSocket 等协议;提供安全传输、压缩、大文件传输、编解码支持等等。

    Netty具备如下优点:

    1. 设计优雅,提供阻塞和非阻塞的 Socket;提供灵活可拓展的事件模型;提供高度可定制的线程模型。
    2. 具备更高的性能和更大的吞吐量,使用零拷贝技术最小化不必要的内存复制,减少资源的消耗。
    3. 提供安全传输特性。
    4. 支持多种主流协议;预置多种编解码功能,支持用户开发私有协议

2.3.2 线程模型

2.3.2.1 线程模型基本介绍

不同的线程模式,对程序的性能有很大影响,在学习Netty线程模式之前,首先讲解下 各个线程模式, 最后看看 Netty 线程模型有什么优越性。

目前存在的线程模型有:

  • 传统阻塞 I/O 服务模型
  • Reactor 模型
    根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现
    - 单 Reactor 单线程
    - 单 Reactor 多线程
    - 主从 Reactor 多线程
2.3.2.2 传统阻塞 I/O 服务模型

采用阻塞 IO 模式获取输入的数据, 每个连接都需要独立的线程完成数据的输入 , 业务处理和数据返回工作。

阻塞I/O模型
存在的问题:

  1. 当并发数很大,就会创建大量的线程,占用很大系统资源
  2. 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在 read 操作,造成线程资源浪费
2.2.2.3 Reactor 模型

Reactor 模式,通过一个或多个输入同时传递给服务处理器的模式 , 服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程, 因此 Reactor 模式也叫 Dispatcher模式。
Reactor 模式使用IO 复用监听事件,收到事件后,分发给某个线程(进程),这点就是网络服务器高并发处理关键

  1. 单 Reactor 单线程

    单Reactor单线程
    处理流程

    • Selector是可以实现应用程序通过一个阻塞对象监听多路连接请求
    • Reactor 对象通过 Selector监控客户端请求事件,收到事件后通过 Dispatch 进行分发
    • 是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理
    • Handler 会完成 Read→业务处理→Send 的完整业务流程


    优点:

    • 模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成


    缺点

    • 性能问题:
      只有一个线程,无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈。
    • 可靠性问题:
      线程意外终止或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
  2. 单 Reactor多线程

    单Reactor多线程
    处理流程

    • Reactor 对象通过 selector 监控客户端请求事件,收到事件后,通过 dispatch 进行分发;
    • 如果建立连接请求,则由Acceptor 通过accept 处理连接请求;
    • 如果不是连接请求,则由 reactor 分发调用连接对应的 handler 来处理;
    • handler 只负责响应事件,不做具体的业务处理,通过 read 读取数据后,会分发给后面的worker 线程池的某个线程处理业务;
    • worker 线程池会分配独立线程完成真正的业务,并将结果返回给 handler;
    • handler 收到响应后,通过 send 将结果返回给 client。


    优点:

    • 可以充分的利用多核 cpu 的处理能力


    缺点:

    • 多线程数据共享和访问比较复杂, reactor 处理所有的事件的监听和响应,在单线程运行, 在高并发场景容易出现性能瓶颈。
  3. 主从 Reactor 多线程

    主从Reactor多线程
    处理流程:

    • Reactor 主线程 MainReactor 对象通过 select 监听客户端连接事件,收到事件后,通过Acceptor 处理客户端连接事件;
    • 当 Acceptor 处理完客户端连接事件之后(与客户端建立好 Socket 连接),MainReactor 将连接分配给 SubReactor。(即:MainReactor 只负责监听客户端连接请求,和客户端建立连接之后将连接交由 SubReactor 监听后面的 IO 事件);
    • SubReactor 将连接加入到自己的连接队列进行监听,并创建 Handler 对各种事件进行处理;
    • 当连接上有新事件发生的时候,SubReactor 就会调用对应的 Handler 处理;
    • Handler 通过 read 从连接上读取请求数据,将请求数据分发给 Worker 线程池进行业务处理;
    • Worker 线程池会分配独立线程来完成真正的业务处理,并将处理结果返回给 Handler。Handler 通过 send 向客户端发送响应数据;
    • 一个 MainReactor 可以对应多个 SubReactor,即一个 MainReactor 线程可以对应多个SubReactor 线程。


    优点:

    1. MainReactor 线程与 SubReactor 线程的数据交互简单职责明确,MainReactor 线程只需要接收新连接,SubReactor 线程完成后续的业务处理
    2. MainReactor 线程与 SubReactor 线程的数据交互简单, MainReactor 线程只需要把新连接传给 SubReactor 线程,SubReactor 线程无需返回数据
    3. 多个 SubReactor 线程能够应对更高的并发请求


    缺点:

    • 这种模式的缺点是编程复杂度较高。但是由于其优点明显,在许多项目中被广泛使用,包括Nginx、Memcached、Netty 等。
      这种模式也被叫做服务器的 1+M+N 线程模式,即使用该模式开发的服务器包含一个(或多个,1 只是表示相对较少)连接建立线程+M 个 IO 线程+N 个业务处理线程。这是业界成熟的服务器程序设计模式。
2.3.2.4 Netty线程模型

Netty 的设计主要基于主从 Reactor 多线程模式,并做了一定的改进。

  1. 简单版Netty模型

    简单版Netty模型

    • BossGroup 线程维护 Selector,ServerSocketChannel 注册到这个 Selector 上,只关注连接建立请求事件(主 Reactor)
    • 当接收到来自客户端的连接建立请求事件的时候,通过 ServerSocketChannel.accept 方法获得对应的 SocketChannel,并封装成 NioSocketChannel 注册到 WorkerGroup 线程中的Selector,每个 Selector 运行在一个线程中(从 Reactor)
    • 当 WorkerGroup 线程中的 Selector 监听到自己感兴趣的 IO 事件后,就调用 Handler 进行处理
  2. 进阶版Netty模型

    进阶版Netty模型

    • 有两组线程池:BossGroup 和 WorkerGroup,BossGroup 中的线程专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写。
    • BossGroup 和 WorkerGroup 含有多个不断循环的执行事件处理的线程,每个线程都包含一个Selector,用于监听注册在其上的 Channel。
    • 每个 BossGroup 中的线程循环执行以下三个步骤:
      • 轮询注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件);
      • 处理 accept 事件,与客户端建立连接,生成一个 NioSocketChannel,并将其注册到WorkerGroup 中某个线程上的 Selector 上;
      • 再去以此循环处理任务队列中的下一个事件。
    • 每个 WorkerGroup 中的线程循环执行以下三个步骤:
      • 轮询注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件)
      • 在对应的 NioSocketChannel 上处理 read/write 事件
      • 再去以此循环处理任务队列中的下一个事件
  3. 详细版Netty模型

    详细版Netty

    • Netty 抽象出两组线程池:BossGroup 和 WorkerGroup,也可以叫做BossNioEventLoopGroup 和 WorkerNioEventLoopGroup。每个线程池中都有NioEventLoop 线程。BossGroup 中的线程专门负责和客户端建立连接,WorkerGroup 中的线程专门负责处理连接上的读写。BossGroup 和 WorkerGroup 的类型都是NioEventLoopGroup。
    • NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环就是一个 NioEventLoop。
    • NioEventLoop 表示一个不断循环的执行事件处理的线程,每个 NioEventLoop 都包含一个Selector,用于监听注册在其上的 Socket 网络连接(Channel)。
    • NioEventLoopGroup 可以含有多个线程,即可以含有多个 NioEventLoop。
    • 每个 BossNioEventLoop 中循环执行以下三个步骤:
      • select:轮训注册在其上的 ServerSocketChannel 的 accept 事件(OP_ACCEPT 事件)
      • processSelectedKeys:处理 accept 事件,与客户端建立连接,生成一个NioSocketChannel,并将其注册到某个 WorkerNioEventLoop 上的 Selector 上
      • runAllTasks:再去以此循环处理任务队列中的其他任务
    • 每个 WorkerNioEventLoop 中循环执行以下三个步骤:
      • select:轮训注册在其上的 NioSocketChannel 的 read/write 事件(OP_READ/OP_WRITE 事件);
      • processSelectedKeys:在对应的 NioSocketChannel 上处理 read/write 事件;
      • runAllTasks:再去以此循环处理任务队列中的其他任务。
    • 在以上两个processSelectedKeys步骤中,会使用 Pipeline(管道),Pipeline 中引用了Channel,即通过 Pipeline 可以获取到对应的 Channel,Pipeline 中维护了很多的处理器(拦截处理器、过滤处理器、自定义处理器等)。

2.3.3 核心API介绍

  1. ChannelHandler及其实现类
    ChannelHandler 接口定义了许多事件处理的方法,我们可以通过重写这些方法去实现具 体的业务逻辑。API 关系如下图所示:
    ChannelHandler
    Netty开发中需要自定义一个 Handler 类去实现 ChannelHandle接口或其子接口或其实现类,然后通过重写相应方法实现业务逻辑,

    一般都需要重写哪些方法:

    • public void channelActive(ChannelHandlerContext ctx),通道就绪事件
    • public void channelRead(ChannelHandlerContext ctx, Object msg),通道读取数据事件
    • public void channelReadComplete(ChannelHandlerContext ctx) ,数据读取完毕事件
    • public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause),通道发生异常事件
  2. ChannelPipeline
    ChannelPipeline 是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和
    操作,相当于一个贯穿 Netty 的责任链。
     ChannelPipeline
    如果客户端和服务器的Handler是一样的,消息从客户端到服务端或者反过来,每个Inbound类型或Outbound类型的Handler只会经过一次,混合类型的Handler(实现了Inbound和Outbound的Handler)会经过两次。准确的说ChannelPipeline中是一个ChannelHandlerContext,每个上下文对象中有ChannelHandler。 InboundHandler是按照Pipleline的加载顺序的顺序执行, OutboundHandler是按照Pipeline的加载顺序,逆序执行

  3. ChannelHandlerContext
    这是事件处理器上下文对 象 ,Pipeline 链中的实际处理节点 。 每个处理节点ChannelHandlerContext 中包含一个具体的事件处理器ChannelHandler ,同时ChannelHandlerContext 中也绑定了对应的 ChannelPipeline和 Channel 的信息,方便对ChannelHandler 进行调用。

    常用方法如下所示:

    • ChannelFuture close(),关闭通道
    • ChannelOutboundInvoker flush(),刷新
    • ChannelFuture writeAndFlush(Object msg) , 将数据写到ChannelPipeline中当前
      ChannelHandler的下一个 ChannelHandler 开始处理(出站)
  4. ChannelOption
    Netty 在创建 Channel 实例后,一般都需要设置 ChannelOption 参数。ChannelOption 是 Socket 的标准参数,而非 Netty 独创的。

    常用的参数配置有:

    • ChannelOption.SO_BACKLOG
      对应 TCP/IP 协议 listen 函数中的 backlog 参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户 端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog 参数指定 了队列的大小。
    • ChannelOption.SO_KEEPALIVE
    • 一直保持连接活动状态。该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
  5. ChannelFuture
    表示 Channel 中异步 I/O 操作的结果,在 Netty 中所有的 I/O 操作都是异步的,I/O 的调用会直接返回,调用者并不能立刻获得结果,但是可以通过 ChannelFuture 来获取 I/O 操作 的处理状态。

    常用方法如下所示:

    • Channel channel(),返回当前正在进行 IO 操作的通道
    • ChannelFuture sync(),等待异步操作执行完毕,将异步改为同步
  6. EventLoopGroup和实现类NioEventLoopGroup
    EventLoopGroup 是一组 EventLoop 的抽象,Netty 为了更好的利用多核 CPU 资源,一般 会有多个EventLoop 同时工作,每个 EventLoop 维护着一个 Selector 实例。

    EventLoopGroup 提供 next 接口,可以从组里面按照一定规则获取其中一个 EventLoop 来处理任务。在 Netty 服务器端编程中,我们一般都需要提供两个 EventLoopGroup,例如:BossEventLoopGroup 和 WorkerEventLoopGroup。
    通常一个服务端口即一个 ServerSocketChannel对应一个Selector 和一个EventLoop线程。 BossEventLoop 负责接收客户端的连接并将SocketChannel 交给 WorkerEventLoopGroup 来进 行 IO 处理。
    如下图所示:
    在这里插入图片描述BossEventLoopGroup 通常是一个单线程的 EventLoop,EventLoop 维护着一个注册了ServerSocketChannel 的 Selector 实例,BossEventLoop 不断轮询 Selector 将连接事件分离出来, 通常是 OP_ACCEPT 事件,然后将接收到的 SocketChannel 交给 WorkerEventLoopGroup,WorkerEventLoopGroup 会由 next 选择其中一个 EventLoopGroup 来将这个 SocketChannel 注册到其维护的 Selector 并对其后续的 IO 事件进行处理。

    一般情况下我们都是用实现类NioEventLoopGroup。

    常用方法如下所示:

    • public NioEventLoopGroup(),构造方法,创建线程组
    • public Future<?> shutdownGracefully(),断开连接,关闭线程
  7. ServerBootstrap和Bootstrap
    ServerBootstrap 是 Netty 中的服务器端启动助手,通过它可以完成服务器端的各种配置;
    Bootstrap 是 Netty 中的客户端启动助手,通过它可以完成客户端的各种配置。

    常用方法如下 所示:

    • public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup), 该方法用于服务器端,用来设置两个 EventLoop
    • public B group(EventLoopGroup group) ,该方法用于客户端,用来设置一个 EventLoop
    • public B channel(Class<? extends C> channelClass),该方法用来设置一个服务器端的通道 实现
    • public B option(ChannelOption option, T value),用来给 ServerChannel 添加配置
    • public ServerBootstrap childOption(ChannelOption childOption, T value),用来给接收到的通道添加配置
    • public ServerBootstrap childHandler(ChannelHandler childHandler),该方法用来设置业务处理类(自定义的 handler)
    • public ChannelFuture bind(int inetPort) ,该方法用于服务器端,用来设置占用的端口号
    • public ChannelFuture connect(String inetHost, int inetPort) ,该方法用于客户端,用来连接服务器端。
  8. Unpooled类
    这是 Netty 提供的一个专门用来操作缓冲区的工具类。

    常用方法如下所示:

    • public static ByteBuf copiedBuffer(CharSequence string, Charset charset),通过给定的数据和字符编码返回一个 ByteBuf 对象(类似于 NIO 中的 ByteBuffer 对象)
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 西电分布式计算课程的作业中,我们研究了使用RPC(Remote Procedure Call)进行分布式计算。RPC是一种通信机制,它允许在不同的计算机上调用远程过程或方法。 在作业中,我们首先学习了RPC的工作原理和基本概念。RPC通过网络连接客户端和服务器,使客户端能够像调用本地函数一样调用远程函数。客户端发送请求消息到服务器端,服务器端执行相应的函数,并将结果返回给客户端。这种机制使得分布式计算变得更加简便和高效。 我们进一步实践了RPC的应用,使用Java语言和相关的框架来实现一个简单的RPC系统。我们首先定义了接口,包含了客户端和服务器端共享的方法。然后我们使用RPC框架提供的工具生成客户端和服务器端的代理类,使得客户端可以调用远程方法,而服务器端可以接收请求并处理相应的方法。 在作业中,我们还探讨了RPC的一些特性和优势。RPC具有高度的透明性,使得分布式计算变得更加灵活和简单,客户端无需关注具体的网络通信细节。此外,RPC能够提供数据的传输和序列化,确保数据在网络传输中的有效性和安全性。 通过完成这个作业,我们更深入地理解了分布式计算和RPC的相关概念和原理RPC作为一种重要的分布式计算通信机制,为我们在分布式环境下实现高效的计算和协作提供了有力的支持。在今后的学习和实践中,我们将更加灵活和熟练地应用RPC技术,为解决分布式计算问题提供更好的解决方案。 ### 回答2: 西安电子科技大学的分布式计算作业中,我们使用RPC(远程过程调用)技术来实现分布式计算。 RPC是一种基于网络通信的技术,它可以实现在不同的计算机上调用远程服务。在分布式计算中,RPC可以帮助我们实现跨计算节点的函数调用和数据传输。 首先,我们需要定义分布式计算任务的接口,即要远程调用的方法。比如,我们可以定义一个计算两个数之和的方法。然后,我们使用RPC框架,如Apache Thrift或gRPC,生成客户端和服务器端的代码。 在服务器端,我们实现接口方法的具体逻辑。当客户端调用这个方法时,通过RPC协议,客户端将请求发送给服务器端。服务器端接收到请求后,执行相应的方法,并将结果返回给客户端。 在分布式环境中,客户端和服务器端可以部署在不同的计算节点上。通过RPC技术,客户端不需要知道调用的方法实际在哪个节点上执行,只需要发送请求即可。这样,我们可以实现跨节点的分布式计算。 需要注意的是,在分布式计算中,我们还需要考虑任务的划分和调度。我们可以将一个大的计算任务划分成多个子任务,并将这些子任务分配给不同的计算节点进行处理。通过RPC技术,各个计算节点可以并行地执行任务,最终将结果汇总起来。 综上所述,西安电子科技大学的分布式计算作业中,我们使用RPC技术来实现跨计算节点的函数调用和数据传输。通过合理的任务划分和调度,我们可以实现高效的分布式计算。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值