# 面试题丨SpringCloud

# 1. 什么是 SpringCloud?

SpringBoot 是由 Pivotal团队提供的新框架,它设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。它的核心思想就是约定大于配置,它使用了特定的方式来进行配置,简化开发人员的工作。其实:SpringBoot 并不是什么新框架,它其实整合了众多框架,像Maven 整合了很多jar一样,方便开发人员初始化工程和开发过程。

SpringCloud 在 SpringBoot 的基础上提供了一系列针对分布式场景的基础设施。SpringCloud是一系列框架的有序集合,它利用 SpringBoot 的开发便利性巧妙地简化了分布式系统基础设施的开发,它包含了服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等内容,它用 SpringBoot 的开发风格做到一键启动和部署。Spring 其实并没有重复制造轮子,它主要就是将各家开发的比较成熟服务框架组合起来,通过 SpringBoot 风格进行再封装、屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。大家可以理解springcloud是个类似“全家桶”套餐,大家想吃鸡腿吃鸡腿,想吃汉堡吃汉堡。

# 2. SpringCloud 的优势

参考:http://c.biancheng.net/view/5310.html

Nginx

我们先从 Nginx 说起,了解为什么需要微服务。最初的服务化解决方案是给相同服务提供一个统一的域名,然后服务调用者向这个域发送 HTTP 请求,由 Nginx 负责请求的分发和跳转。

这种架构存在很多问题:Nginx 作为中间层,在配置文件中耦合了服务调用的逻辑,这削弱了微服务的完整性,也使得 Nginx 在一定程度上变成了一个重量级的 ESB。下图标识出了 Nginx 的转发信息流走向。

Nginx转发的信息流

服务的信息分散在各个系统,无法统一管理和维护。每一次的服务调用都是一次尝试,服务消费方并不知道有哪些实例在给他们提供服务。这带来了一些问题:

  • 无法直观地看到服务提供方和服务消费方当前的运行状况与通信频率;
  • 消费方的失败重发、负载均衡等都没有统一策略,这加大了开发每个服务的难度,不利于快速演化。

Dubbo

为了解决上面的问题,我们需要一个现成的中心组件对服务进行整合,将每个服务的信息汇总,包括服务的组件名称、地址、数量等。

服务的调用方在请求某项服务时首先通过中心组件获取提供服务的实例信息(IP、端口等),再通过默认或自定义的策略选择该服务的某一提供方直接进行访问,所以考虑引入 Dubbo。

Dubbo 是阿里开源的一个 SOA 服务治理解决方案,文档丰富,在国内的使用度非常高。下图为 Dubbo 的基本架构图,使用 Dubbo 构建的微服务已经可以较好地解决上面提到的问题。

Dubbo的基本架构图

从上图中,可以看出以下几点:

  • 调用中间层变成了可选组件,消费方可以直接访问服务提供方;
  • 服务信息被集中到 Registry 中,形成了服务治理的中心组件;
  • 通过 Monitor 监控系统,可以直观地展示服务调用的统计信息;
  • 服务消费者可以进行负载均衡、服务降级的选择。

但是对于微服务架构而言,Dubbo 并不是十全十美的,也有一些缺陷,比如:

  • Registry 严重依赖第三方组件(ZooKeeper 或者 Redis),当这些组件出现问题时,服务调用很快就会中断。
  • Dubbo 只支持 RPC 调用。这使得服务提供方与调用方在代码上产生了强依赖,服务提供方需要不断将包含公共代码的 Jar 包打包出来供消费方使用。一旦打包出现问题,就会导致服务调用出错。

Dubbo 和 Spring Cloud 并不是完全的竞争关系,两者所解决的问题域并不一样。

Dubbo 的定位始终是一款 RPC 框架,而 SpringCloud 的目标是微服务架构下的一站式解决方案。如果非要比较的话,Dubbo 可以类比到 Netflix OSS 技术栈,而 Spring Cloud 集成了 Netflix OSS 作为分布式服务治理解决方案,但除此之外 Spring Cloud 还提供了配置、消息、安全、调用链跟踪等分布式问题解决方案。

当前由于 RPC 协议、注册中心元数据不匹配等问题,在面临微服务基础框架选型时 Dubbo 与 Spring Cloud 只能二选一,这也是大家总是拿 Dubbo 和 Spring Cloud 做对比的原因之一。

Dubbo 已经适配到 Spring Cloud 生态,比如作为 Spring Cloud 的二进制通信方案来发挥 Dubbo 的性能优势,Dubbo 通过模块化以及对 HTTP 的支持适配到 Spring Cloud。

SpringCloud

作为新一代的服务框架,Spring Cloud 提出的口号是开发“面向云的应用程序”,它为微服务架构提供了更加全面的技术支持。结合我们一开始提到的微服务的诉求。

image-20210405100949659

Spring Cloud 抛弃了 Dubbo 的 RPC 通信,采用的是基于 HTTP 的 REST 方式。严格来说,这两种方式各有优劣。虽然从一定程度上来说,后者牺牲了服务调用的性能,但也避免了上面提到的原生 RPC 带来的问题。而且 REST 相比 RPC 更为灵活,服务提供方和调用方,不存在代码级别的强依赖,这在强调快速演化的微服务环境下显得更加合适。

很明显,Spring Cloud 的功能比 Dubbo 更加强大,涵盖面更广,而且作为 Spring 的拳头项目,它也能够与 Spring Framework、Spring Boot、Spring Data、Spring Batch 等其他 Spring 项目完美融合,这些对于微服务而言是至关重要的。

前面提到,微服务背后一个重要的理念就是持续集成、快速交付,而在服务内部使用一个统一的技术框架,显然比将分散的技术组合到一起更有效率。

更重要的是,相比于 Dubbo,它是一个正在持续维护的、社区更加火热的开源项目,这就可以保证使用它构建的系统持续地得到开源力量的支持。

下面列举 Spring Cloud 的几个优势。

  • Spring Cloud 来源于 Spring,质量、稳定性、持续性都可以得到保证。
  • Spirng Cloud 天然支持 Spring Boot,更加便于业务落地。
  • Spring Cloud 发展得非常快。
  • Spring Cloud 是 Java 领域最适合做微服务的框架。

相比于其他框架,Spring Cloud 对微服务周边环境的支持力度最大。对于中小企业来讲,使用门槛较低。

总结:

优点:

  1. 服务拆分粒度更细,有利于资源重复利用,有利于提高开发效率
  2. 可以更精准的制定优化服务方案,提高系统的可维护性
  3. 微服务架构采用去中心化思想,服务之间采用 Restful 等轻量级通讯,比ESB更轻量
  4. 适于互联网时代,产品迭代周期更短

缺点:

  1. 微服务过多,治理成本高,不利于维护系统
  2. 分布式系统开发的成本高(容错,分布式事务等)对团队挑战大

# 3. 服务注册和发现是什么意思?Spring Cloud 如何实现?

参考:https://blog.csdn.net/qwe86314/article/details/94552801

服务注册:

  • 服务进程在注册中心注册自己的位置。它通常注册自己的主机和端口号,有时还有身份验证信息,协议,版本号,以及运行环境的详细资料。

服务发现:

  • 客户端应用进程向注册中心发起查询,来获取服务的位置。服务发现的一个重要作用就是提供一个可用的服务列表。
在这里插入图片描述

Eureka Server

注册中心服务端主要对外提供了三个功能:

  1. 服务注册
  • 服务提供者启动时,会通过 Eureka Client 向 Eureka Server 注册信息,Eureka Server 会存储该服务的信息,Eureka Server 内部有二层缓存机制来维护整个注册表。
  1. 提供注册表
  • 服务消费者在调用服务时,如果 Eureka Client 没有缓存注册表的话,会从 Eureka Server 获取最新的注册表。
  1. 同步状态
  • Eureka Client 通过注册、心跳机制和 Eureka Server 同步当前客户端的状态。

Eureka Client

Eureka Client 是一个 Java 客户端,用于简化与 Eureka Server 的交互。Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息。因此当所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者,但是当服务有更改的时候会出现信息不一致。

Register 服务注册

  • 服务的提供者,将自身注册到注册中心,服务提供者也是一个 Eureka Client。当 Eureka Client 向 Eureka Server 注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL,主页等。

Renew 服务续约

  • Eureka Client 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 Eureka Server 该 Eureka Client 运行正常,没有出现问题。 默认情况下,如果 Eureka Server 在 90 秒内没有收到 Eureka Client 的续约,Server 端会将实例从其注册表中删除,此时间可配置,一般情况不建议更改。

Eviction 服务剔除

  • 当 Eureka Client 和 Eureka Server 不再有心跳时,Eureka Server 会将该服务实例从服务注册列表中删除,即服务剔除。

Cancel 服务下线

  • Eureka Client 在程序关闭时向 Eureka Server 发送取消请求。 发送请求后,该客户端实例信息将从 Eureka Server 的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:

    DiscoveryManager.getInstance().shutdownComponent()

GetRegisty 获取注册列表信息

Eureka Client 从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与 Eureka Client 的缓存信息不同,Eureka Client 自动处理。

如果由于某种原因导致注册列表信息不能及时匹配,Eureka Client 则会重新获取整个注册表信息。 Eureka Server 缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka Client 和 Eureka Server 可以使用 JSON/XML 格式进行通讯。在默认情况下 Eureka Client 使用压缩 JSON 格式来获取注册列表的信息。

获取服务是服务消费者的基础,所以必有两个重要参数需要注意:

# 启用服务消费者从注册中心拉取服务列表的功能
eureka.client.fetch-registry=true

# 设置服务消费者从注册中心拉取服务列表的间隔
eureka.client.registry-fetch-interval-seconds=30

Remote Call 远程调用

当 Eureka Client 从注册中心获取到服务提供者信息后,就可以通过 Http 请求调用对应的服务;服务提供者有多个时,Eureka Client 客户端会通过 Ribbon 自动进行负载均衡。

自我保护机制

默认情况下,如果 Eureka Server 在一定的 90s 内没有接收到某个微服务实例的心跳,会注销该实例。但是在微服务架构下服务之间通常都是跨进程调用,网络通信往往会面临着各种问题,比如微服务状态正常,网络分区故障,导致此实例被注销。

固定时间内大量实例被注销,可能会严重威胁整个微服务架构的可用性。为了解决这个问题,Eureka 开发了自我保护机制,那么什么是自我保护机制呢?

Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否高于 85%,如果高于 85%,Eureka Server 即会进入自我保护机制。

Eureka Server 触发自我保护机制后,页面会出现提示:

image-20210405102208013

Eureka Server 进入自我保护机制,会出现以下几种情况:

  1. Eureka 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务。
  2. Eureka 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)。
  3. 当网络稳定时,当前实例新的注册信息会被同步到其它节点中。

Eureka 自我保护机制是为了防止误杀服务而提供的一个机制。当个别客户端出现心跳失联时,则认为是客户端的问题,剔除掉客户端;当 Eureka 捕获到大量的心跳失败时,则认为可能是网络问题,进入自我保护机制;当客户端心跳恢复时,Eureka 会自动退出自我保护机制。

如果在保护期内刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,即会调用失败。对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。

通过在 Eureka Server 配置如下参数,开启或者关闭保护机制,生产环境建议打开:

eureka.server.enable-self-preservation=true

集群原理

再来看看 Eureka 集群的工作原理。我们假设有三台 Eureka Server 组成的集群,第一台 Eureka Server 在北京机房,另外两台 Eureka Server 在深圳和西安机房。这样三台 Eureka Server 就组建成了一个跨区域的高可用集群,只要三个地方的任意一个机房不出现问题,都不会影响整个架构的稳定性。

在这里插入图片描述

从图中可以看出 Eureka Server 集群相互之间通过 Replicate 来同步数据,相互之间不区分主节点和从节点,所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其他节点。

如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点。当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 Eureka Server 当前所知的所有节点中。

另外 Eureka Server 的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。所以,如果存在多个节点,只需要将节点之间两两连接起来形成通路,那么其它注册中心都可以共享信息。每个 Eureka Server 同时也是 Eureka Client,多个 Eureka Server 之间通过 P2P 的方式完成服务注册表的同步。

Eureka Server 集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。

Eureka 保证了 CAP 中的 AP:

Eureka Server 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 Eureka Client 在向某个 Eureka 注册时,如果发现连接失败,则会自动切换至其它节点。只要有一台 Eureka Server 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。

Eureka 工作流程

  1. Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息。
  2. Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务。
  3. Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常。
  4. 当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例。
  5. 单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端。
  6. 当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式。
  7. Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地。
  8. 服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存。
  9. Eureka Client 获取到目标服务器信息,发起服务调用。
  10. Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除。

# 4. 负载均衡的意义是什么?

在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。

# 5. 什么是 Hystrix?它如何实现容错?

参考:https://blog.csdn.net/chengqiuming/article/details/80752781

Hystrix是一个实现了超时机制断路模式的工具类库。

Hystrix 是由 Netflix 开源的一个延迟和容错库,用于隔离访问远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。

Hystrix主要通过以下几点实现延迟和容错:

  1. 包裹请求:使用 HystrixCommand 包裹对依赖的调用逻辑,每个命令在独立线程中执行。这使用了设计模式中的“命令模式”。
  2. 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑由开发人员自行提供,例如返回一个缺省值。
  3. 跳闸机制:当某服务的错误率超过一定的阈值时,Hystrix 可以自动或手动跳闸,停止请求该服务一段时间。
  4. 自我修复:断路器打开一段时间后,会自动进入“半开”状态。
  5. 资源隔离:Hystrix 为每个依赖都维护了一个小型的线程池(或者信号量)。如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等待,从而加速失败判定。
  6. 监控:Hystrix 可以近乎实时地监控运行指标和配置的变化,例如成功、失败、超时、以及被拒绝的请求等。

# 6. 什么是命令模式?

参考:https://yanchen.blog.csdn.net/article/details/79030816

命令模式(Command Pattern)是一种数据驱动的设计模式,它属于行为型模式。请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。

img

在该类图中,我们看到四个角色:

  • Command:请求封装成的对象,该对象是命令模式的主角。也就是说将请求方法封装成一个命令对象,通过操作命令对象来操作请求方法。在命令模式是有若干个请求的,需要将这些请求封装成一个个命令对象,客户端只需要调用不同的命令就可以达到将请求参数化的目的。将一条条请求封装成一个个命定对象之后,客户端发起的就是一个个命令对象了,而不是原来的请求方法。
  • Receiver:有命令,当然有命令的接收者对象:如果有只有命令,没有接受者,那不就是光棍司令了?没有电视机或者电脑主机,你对着电视机遥控器或者电脑键盘狂按有毛用?Receiver 对象的主要作用就是**受到命令后执行对应的操作。**对于点击遥控器发起的命令来说,电视机就是这个 Receiver 对象,比如按了待机键,电视机收到命令后就执行了待机操作,进入待机状态。
  • Client:但是有一个问题摆在眼前,命令对象现在已经有了,但是谁来负责创建命令呢?这里就引出了客户端 Client 对象,再命令模式中命令是有客户端来创建的。打个比方来说,操作遥控器的那个人,就是扮演的客户端的角色。人按下遥控器的不同按键,来创建一条条命令。
  • Invoker:现在创建命令的对象 Client 也已经露脸了,它负责创建一条条命令,那么谁来使用或者调度这个命令呢?命令的使用者就是 Invoker 对象了,还是拿人,遥控器,电视机来做比喻,遥控器就是这个 Invoker 对象,遥控器负责使用客户端创建的命令对象。该 Invoker 对象负责要求命令对象执行请求,通常会持有命令对象,可以持有很多的命令对象。

下面就用看电视的人(Watcher),电视机(Television),遥控器(TeleController)来模拟一下这个命令模式。

其中 Watcher 是 Client 角色,Television 是 Receiver 角色,TeleController 是 Invoker 角色。

首先设计一个简单的电视机的对象:

//电视机对象:提供了播放不同频道的方法
public class Television {

    public void playCctv1() {
        System.out.println("--CCTV1--");
    }

    public void playCctv2() {
        System.out.println("--CCTV2--");
    }

    public void playCctv3() {
        System.out.println("--CCTV3--");
    }

    public void playCctv4() {
        System.out.println("--CCTV4--");
    }

    public void playCctv5() {
        System.out.println("--CCTV5--");
    }

    public void playCctv6() {
        System.out.println("--CCTV6--");
    }

}

电视机的类创建好了,本文会以“非命令模式“和“命令模式“两种实现看电视的不同之处,加深对命令模式的理解。

非命令模式实现: 如果不用命令模式的话,其实实现看电视的功能很简单,首先设计一个看电视的人的类:Watcher;既然要看电视,所以 Watcher 内部需要持有一个电视机的引用。如此简单的 Watcher 搞定:

//观看电视的死宅类
public class Watcher {
    //持有一个
    public Television tv;

    public Watcher(Television tv) {
        this.tv = tv;
    }

    public void playCctv1() {
        tv.playCctv1();
    }

    public void playCctv2() {
        tv.playCctv2();
    }

    public void playCctv3() {
        tv.playCctv3();
    }

    public void playCctv4() {
        tv.playCctv4();
    }

    public void playCctv5() {
        tv.playCctv5();
    }

    public void playCctv6() {
        tv.playCctv6();
    }
}

所以简单的调用就实现了:

public static void main(String args[]) {
        Watcher watcher = new Watcher(new Television());
        watcher.playCctv1();
        watcher.playCctv2();
        watcher.playCctv3();
        watcher.playCctv4();
        watcher.playCctv5();
        watcher.playCctv6();
    }
}

执行结果:

--CCTV1--
--CCTV2--
--CCTV3--
--CCTV4--
--CCTV5--
--CCTV6--

可以看出 Watcher 类和 Television 完全的耦合在一起了,目前本文的电视机对象只能播放六个电视台,如果需要增添全国所有主流卫视的话,需要做如下改动:

  1. 修改 Television对象,增加若干个的playXXTV()的方法来播放不同的卫视。
  2. 修改 Watcher,也添加若干个对应的 playXXTV() 的方法,调用 Television 的 playXXTV()。

这明显违背了**“对修改关闭,对扩展开放”**的重要设计原则。

别的不说,就拿本看电视来说,比如调用 playXXTV() 的顺序是随即的,也就是你胡乱切换了一通:比如你沿着 cctv1、cctv2、cctv3、cctv4、xxtv、yytv..nntv 的顺序来看电视,也就是发生了如下调用:

    watcher.playCctv1();
    watcher.playCctv2();
    watcher.playCctv3();
    watcher.playCctv4();
    watcher.playCctv5();
    watcher.playCctv6();
    watcher.playXXtv();
    watcher.playYYtv();
    watcher.playNNtv();

当前你在看 nntv,如果你想看上一个看过的台也就是 yytv,这很简单,只要在 playNNtv() 后面,调用watcher.playYYtv() 即可,但是如果你想要一直回退到 cctv1 呢?那么你就话发生如下可怕的调用:

        watcher.playCctv1();
        watcher.playCctv2();
        watcher.playCctv3();
        watcher.playCctv4();
        watcher.playCctv5();
        watcher.playCctv6();
        watcher.playXXtv();
        watcher.playYYtv();
        watcher.playNNtv();


        watcher.playYYtv();
        watcher.playXXtv();
        watcher.playCctv6();
        watcher.playCctv5();
        watcher.playCctv4();
        watcher.playCctv3();
        watcher.playCctv2();
        watcher.playCctv1();

为什么会这样呢?因为对于之前播放的哪个卫视并没有记录功能。是时候让命令模式来出来解决问题了,通过命令模式的实现,对比下就能体会到命令模式的巧妙之处。

命令模式的实现

1、设计一个抽象的命令类:在本系统中,命令的接收者对象就是电视机 Tevevision了:

public abstract class Command {
    //命令接收者:电视机
    protected Television television;

    public Command(Television television) {
        this.television = television;
    }

    //命令执行
    abstract void execute();
}

将播放各个卫视的操作封装成一个一个命令,实现如下:

//播放cctv1的命令
public class CCTV1Command extends Command {
    @Override
    void execute() {
        television.playCctv1();
    }
}

//播放cctv2的命令
public class CCTV6Command extends Command {
    @Override
    void execute() {
        television.playCctv2();
    }
}

//...........

//播放cctv6的命令
public class CCTV1Command extends Command {
    @Override
    void execute() {
        television.playCctv6();
    }
}

抽象类 Command 的几个子类实现也很简单,就是将电视机 TeleVision 对象的 playXxTV() 方法分布于不同的命令对象中,且因为不同的命令对象拥有共同的抽象类,我们很容易将这些名利功能放入一个数据结构(比如数组)中来存储执行过的命令。

命令对象设计好了,那么就引入命令的调用着 Invoker 对象了,在此例子中电视遥控器 TeleController 就是扮演的这个角色:

public class TeleController {
    //播放记录
    List<Command> historyCommand = new ArrayList<Command>();

    //切换卫视
    public void switchCommand(Command command) {
        historyCommand.add(command);
        command.execute();
    }

    //遥控器返回命令
    public void back() {
        if (historyCommand.isEmpty()) {
            return;
        }
        int size = historyCommand.size();
        int preIndex = size-2<=0?0:size-2;

        //获取上一个播放某卫视的命令
        Command preCommand = historyCommand.remove(preIndex);
        preCommand.execute();
    }
}

很简答,遥控器对象持有一个命令集合,用来记录已经执行过的命令。新的命令对像作为 switchCommand 参数来添加到集合中,注意在这里就体现出了让上文所术的请求参数化的目的。且遥控器类也提供了 back() 方法用来模拟真实遥控器的返回功能:所以 main() 方法的实现如下:

       //创建一个电视机
        Television tv = new Television();
        //创建一个遥控器
        TeleController teleController = new TeleController();

        teleController.switchCommand(new CCTV1Command(tv));
        teleController.switchCommand(new CCTV2Command(tv));
        teleController.switchCommand(new CCTV4Command(tv));
        teleController.switchCommand(new CCTV3Command(tv));
        teleController.switchCommand(new CCTV5Command(tv));
        teleController.switchCommand(new CCTV1Command(tv));
        teleController.switchCommand(new CCTV6Command(tv));
        System.out.println("------返回上一个卫视--------");
        //模拟遥控器返回键
        teleController.back();
        teleController.back();

执行结果:

--CCTV1--
--CCTV2--
--CCTV4--
--CCTV3--
--CCTV5--
--CCTV1--
--CCTV6--
----------返回上一个卫视-------------
--CCTV1--
--CCTV5--

从上面的例子我们可以看出,命令模式的主要特点就是将请求封装成一个个命令,以命令为参数来进行切换,达到请求参数化的目的,且还能通过集合这个数据结构来存储已经执行的请求,进行回退操作。而且如果需要添加新的电视频道,只需要添加新的命令类即可。

而非命令模式中,看电视的人和电视耦合在一起;而新的命令模式则使用一个遥控器就将人和电视机解耦。

缺点:

  • 可能会接口过多、类爆炸:命令子类太多了。

# 7. 什么是 SpringCloud OpenFeign?

Feign 的初衷是:feign makes writing java http clients easier ,可以理解为一个Http Client。

只不过这个 http client 对http 请求进行了一个封装。后面我们会讲到它的一个工作方式就是处理注解,封装参数,放入到一个Http请求模板,并能解析返回的结果。

OpenFeign

OpenFeign 是最原始,最早的feign。与Spring 无关。就是一个Java的组件,封装了对http请求和响应的处理。

SpringCloud OpenFeign

Spring Cloud 中的微服务都是以Http 接口的形式向外提供服务。

提供Http 服务的形式有多种:

  • JDK 原生的URLConnction
  • Apache 的HttpClient
  • Spring 的RestTemplate

Spring Cloud 对Feign 也进行了增强,直接支持 Hystrix 和 Ribbon,也支持 SpringMVC 的注解。这样使得 Feign 的使用非常方便。

SpringCloud Feign 原理

  1. 通过主类上的 @EnableFeignClients 注解开启 FeignClient。
  2. 根据 Feign 的规则实现接口,并加上 @FeignClient 注解,供调用的地方注入调用。
  3. 程序启动后,会扫描所有 @FeignClient 注解的类,并将这些信息注入到 IoC 容器中。
  4. 当 2 中接口被调用时,通过 JDK 代理,以及反射(Spring处理注解的方式),来生成具体的 RequestTemplate。
  5. RequestTemplate 生成 Reqest。
  6. Request 交给 httpclient 处理,这里的 httpclient 可以是 OkHttp,也可以是 HttpUrlConnection 或者 HttpClient。
  7. 最后 Client 被封装到 LoadBalanceClient 类,这个类结合 Ribbon 实现负载均衡。

OpenFeign

# 8. 什么是 SpringCloud Config?

在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,所以需要分布式配置中心组件。

在 SpringCloud中,有分布式配置中心组件 SpringCloud Config ,它支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git 仓库中。

在 SpringCloud Config 组件中,分两个角色,一是 Config Server,二是 Config Client。

Config Server是一个可横向扩展、集中式的配置服务器,它用于集中管理应用程序各个环境下的配置,默认使用 Git 存储配置文件内容,也可以使用 SVN 存储,或者是本地文件存储。

Config Client 是 Config Server 的客户端,用于操作存储在 Config Server 中的配置内容。

微服务在启动时会请求Config Server获取配置文件的内容,请求到后再启动容器。

image-20210405112419359

它的作用:

  1. 集中管理配置文件。
  2. 不同环境不同配置,动态化的配置更新,分环境部署,比如 dev/test/prod/beta/release。
  3. 运行期间动态调整配置,不再需要在每个服务部署的机器上编写配置文件,服务会向配置中心统一拉取配置自己的信息。但配置发生变动时,服务不需要重启即可感知到配置的变化并应用到新的配置。
  4. 将配置信息以 REST 接口的形式暴露。

# 9. 什么是 SpringCloud Bus?

SpringCloud Bus 是 SpringCloud 中的消息总线。在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息。

基本原理:

  • ConfigClient 实例都监听 MQ 中同一个 topic(默认是 SpringCloudBus)。当一个服务刷新数据的时候,它会把这个消息放入到 Topic 中,这样其他间监听同一个 topic 的服务就能得到通知,然后去更新自身的配置。

image-20210405113014685

image-20210405113023775

# 10. 什么是 SpringCloud Gateway?

image-20210405113825012

Gateway 是在 Spring 生态系统之上构建的API网关服务,基于Spring5,SpringBoot 2 和 Project Reactor 等技术。

Gateway 旨在提供一种简单而有效的方法来对 API 进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等

GateWay 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。

三大核心概念:

  1. Route 路由
    • 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由
  2. Predicate 断言
    • 开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由。
  3. Filter 过滤
    • 指的是 Spring 框架中的 GatewayFilter 的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

总结:

web 请求通过一些匹配条件,定位到真正的服务节地,并在这个转发过去前后,进去一些精细化控制。

predicate 就是我们的匹配条件。

而filter,就可以理解为一个无所不能拦截器。

有了这两个元素,再加上目标uri,就可以实现一个具体的路由了

# 11. 什么是 SpringCloud Stream?

image-20210405114054753

SpringCloud Stream 是一个构件消息驱动微服务的框架。

应用程序通过 inputs 或者 outputs 来与 SpringCloud Stream 中 binder 对象交互,通过我们配置来 binding(绑定),而 SpringCloud Stream 的binder 对象负责与消息中间件交互。所以我们只需要搞清楚如何与 Stream 交互就可以方便使用消息驱动。

通过使用 Spring Integration 来连接消息代理中间件以实现消息事件驱动。

SpringCloud Stream 为一些供应商的消息中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。

目前仅支持 RabbitMQ、Kafka。

设计思想

image-20210405114455119

屏蔽不同消息中间件的具体实现细节,抽象成一整套统一的逻辑,开发者只需要关注业务逻辑的输入和输出相关的逻辑,其他跟 MQ 具体交互的过程交给 Stream 来完成。

上次更新: 8/28/2022, 11:43:26 PM