05-面试必会-SpringBoot&SpringCloud
阅读原文时间:2023年08月09日阅读:2

1.在 SpringBoot 项目的启动引导类上都有一个注解@SpringBootApplication

这个注解是一个复合注解, 其中有三个注解构成 , 分别是

  • @SpringBootConfiguration : 是@Configuration 的派生注解 , 标注当前类是一个 SpringBoot 的配置类

  • @ComponentScan : 开启组件扫描, 默认扫描的是当前启动引导了所在包以及子包

  • @EnableAutoConfiguration : 开启自动配置(自动配置核心注解)

    2.在@EnableAutoConfiguration 注解的内容使用@Import 注解导入了一个AutoConfigurationImportSelector.class的类

AutoConfigurationImportSelector.class中的selectImports方法内通过一系列的方法调用, 最终需要加载类加载路径下META-INF下面的spring.factories配置文件

3.在META-INF/spring.factories配置文件中, 定义了很多的自动配置类的完全限定路径

这些配置类都会被加载

4.加载配置类之后, 会配置类或者配置方法上的@ConditionalOnXxxx 条件化注解是否满足条件

如果满足条件就会从属性配置类中读取相关配置 , 执行配置类中的配置方法 , 完成自动配置

springboot 项目在启动的时候, 首先会执行启动引导类里面的SpringApplication.run(AdminApplication.class, args)方法

这个 run 方法主要做的事情可以分为三个部分 :

第一部分进行 SpringApplication 的初始化模块,配置一些基本的环境变量、资源、构造器、监听器

第二部分实现了应用具体的启动方案,包括启动流程的监听模块、加载配置环境模块、及核心的创建上下文环境模块

第三部分是自动化配置模块,该模块作为 springboot 自动配置核心,在后面的分析中会详细讨论

1 properties 文件

2 YAML 文件

3 系统环境变量

4 命令行参数

如果有相同的配置参数, 后加载的会覆盖先加载的

  1. 直接使用 jar -jar 运行

  2. 开发过程中运行 main 方法

  3. 可以配置插件 , 将 springboot 项目打 war 包, 部署到 Tomcat 中运行

  4. 直接用 maven 插件运行 maven spring-boot:run

Spring Boot 的核心注解是@SpringBootApplication , 他由几个注解组成 :

SpringMVC 项目中使用@CrossOrigin 注解来解决跨域问题 , 本质是 CORS

@RequestMapping("/hello")
@CrossOrigin(origins = "*")
//@CrossOrigin(value = "http://localhost:8081") //指定具体ip允许跨域
public String hello() {
    return "hello world";
}

SpringBoot 项目采用自动配置的方式来配置 CORS , 可以通过实现 WebMvcConfigurer 接口然后重写 addCorsMappings 方法解决跨域问题。

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                //是否发送Cookie
                .allowCredentials(true)
                //放行哪些原始域
                .allowedOrigins("*")
                .allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"})
                .allowedHeaders("*")
                .exposedHeaders("*");
    }
}

在 SpringCloud 项目中一般都会有网关 , 在网关中可以配置 CORS 跨域, 这样所有通过网关的请求都解决了跨域问题

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          "[/**]": # 匹配所有请求
            allowedOrigins: "*" #跨域处理 允许所有的域
            allowedMethods: # 支持的方法
              - GET
              - POST
              - PUT
              - DELETE
  • SpringBoot : 2.3.4.RELEASE

  • SpringCloud : Hoxton.SR10

  • SpringCloudAlibaba : 2.2.5.RELEASE

早期我们一般认为的 Spring Cloud 五大组件是

  • Eureka : 注册中心
  • Ribbon : 负载均衡
  • Feign : 远程调用
  • Hystrix : 服务熔断
  • Zuul/Gateway : 网关

随着 SpringCloudAlibba 在国内兴起 , 我们项目中使用了一些阿里巴巴的组件

  • 注册中心/配置中心 Nacos

  • 负载均衡 Ribbon

  • 服务调用 Feign

  • 服务保护 sentinel

  • 服务网关 Gateway

微服务就是一个独立的职责单一的服务应用程序,一个模块

1.优点:松耦合,聚焦单一业务功能,无关开发语言,团队规模降低 , 扩展性好, 天然支持分库

2.缺点:随着服务数量增加,管理复杂,部署复杂,服务器需要增多,服务通信和调用压力增大

1.同步通信:通过 Feign 发送 http 请求调用

2.异步:消息队列,如 RabbitMq、KafKa 等

各种注册中心组件的原理和流程其实大体上类似

核心的功能就一下几个 :

  1. 服务注册 : 服务启动的时候会将服务的信息注册到注册中心, 比如: 服务名称 , 服务的 IP , 端口号等
  2. 服务发现 : 服务调用方调用服务的时候, 根据服务名称从注册中心拉取服务列表 , 然后根据负载均衡策略 , 选择一个服务, 获取服务的 IP 和端口号, 发起远程调用
  3. 服务状态监控 : 服务提供者会定时向注册中心发送心跳 , 注册中心也会主动向服务提供者发送心跳探测, 如果长时间没有接收到心跳, 就将服务实例从注册中心下线或者移除

使用的话, 首先需要部署注册中心服务 , 然后在我们自己的微服务中引入注册中心依赖, 然后再配置文件中配置注册中心地址 就可以了

spring:
  application:
    name: leadnews-admin
  cloud:
    nacos:
      # 注册中心地址
      discovery:
        server-addr: 124.221.75.8:8848
      # 配置中心地址
      config:
        server-addr: 124.221.75.8:8848
        file-extension: yml

我们项目中注册中心用的是 Nacos , 基本上所有的注册中心的核心功能都包括服务注册 , 服务发现, 服务状态监控 , 他的核心原理如下 :

  1. 客户端启动时会将当前服务的信息包含 ip、端口号、服务名、分组名、集群名等信息封装为一个 Instance 对象,准备向 Nacos 服务器注册服务,在注册服务之前,会根据 Instance 中的信息创建一个 BeatInfo 对象,然后创建一个定时任务,每隔一段时间向 Nacos 服务器发送 PUT 请求并携带相关信息,作为定时心跳连接,服务器端在接收到心跳请求后,会去检查当前服务列表中有没有该实例,如果没有的话将当前服务实例重新注册,注册完成后立即开启一个异步任务,更新客户端实例的最后心跳时间,如果当前实例是非健康状态则将其改为健康状态
  2. 心跳定时任务创建完成后,通过 POST 请求将当前服务实例信息注册进 Nacos 服务器,服务器端在接收到注册实例请求后,会将请求携带的数据封装为一个 Instance 对象,然后为这个服务实例创建一个服务 Service,一个 Service 下可能有多个服务实例,服务在 Nacos 保存到一个 ConcurrentHashMap 中,格式为命名空间为 key,value 为 map,分组名和服务名为内层 map 的 key,value 为服务数据,Map(namespace,Map(group::serviceName, Service))
  3. 服务创建完成之后,开启一个定时任务(5s 执行一次),检查当前服务中的各个实例是否在线,如果实例上次心跳时间大于 15s 就将其状态设置为不健康,如果超出 30s,则直接将该实例删除;
  4. 然后将当前实例添加到对应服务列表中,这里会通过 synchronized 锁住当前服务,然后分两种情况向集群中添加实例,如果是持久化数据,则使用 CP 模型,通过 leader 节点将实例数据更新到内存和磁盘文件中,然后同步写入到其他节点 , 必须集群半数以上节点写入成功才会给客户端返回成功;
  5. 如果是非持久话实例数据,使用的是 AP 模型,首先向任务阻塞队列添加一个本地服务实例改变任务,去更新本地服务列表,然后在遍历集群中所有节点,分别创建数据同步任务放进阻塞队列异步进行集群数据同步,不保证集群节点数据同步完成即可返回;
  6. 在将服务实例更新到服务注册表中时,为了防止并发读写冲突,采用的是写时复制的思想,将原注册表数据拷贝一份,添加完成之后再替换回真正的注册表,更新完成之后,通过发布服务变化事件,将服务变动通知给客户端,采用的是 UDP 通信,客户端接收到 UDP 消息后会返回一个 ACK 信号,如果一定时间内服务端没有收到 ACK 信号,还会尝试重发,当超出重发时间后就不在重发,虽然通过 UDP 通信不能保证消息的可靠抵达,但是由于 Nacos 客户端会开启定时任务,每隔一段时间更新客户端缓存的服务列表,通过定时轮询更新服务列表做兜底,所以不用担心数据不会更新的情况,这样既保证了实时性,又保证了数据更新的可靠性;
  7. 服务发现:客户端通过定时任务定时从服务端拉取服务数据保存在本地缓存,服务端在发生心跳检测、服务列表变更或者健康状态改变时会触发推送事件,在推送事件中会基于 UDP 通信将服务列表推送到客户端,同时开启定时任务,每隔 10s 定时推送数据到客户端

服务调用过程中的负载均衡一般使用 SpringCloud 的 Ribbon 组件实现 , Feign 的底层已经自动集成了 Ribbon , 使用起来非常简单

客户端调用的话一般会通过网关, 通过网关实现请求的路由和负载均衡

spring:
  cloud:
    gateway:
      routes:
        # 平台管理
        - id: wemedia
          uri: lb://leadnews-wemedia
          predicates:
            - Path=/wemedia/**
          filters:
            - StripPrefix= 1

RIbbon 负载均衡原理 :

SpringCloudRibbon 的底层采用了一个拦截器,拦截了 RestTemplate 发出的请求,对地址做了修改。

基本流程如下:

  • 拦截我们的 RestTemplate 请求
  • RibbonLoadBalancerClient 会从请求 url 中获取服务名称
  • DynamicServerListLoadBalancer 根据服务名称到注册中心拉取服务列表
  • 注册中心返回列表
  • IRule 利用内置负载均衡规则,从列表中选择一个服务实例
  • RibbonLoadBalancerClient 用服务实例的 IP 和端口替换请求路径中的服务名称
  • 向服务实例发起 http 请求

Ribbon 默认的负载均衡策略有七种 :

**内置负载均衡规则类 **

规则描述

RoundRobinRule

简单轮询服务列表来选择服务器。它是 Ribbon 默认的负载均衡规则。

AvailabilityFilteringRule

对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果 3 次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续 30 秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了 AvailabilityFilteringRule 规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit 属性进行配置。

WeightedResponseTimeRule

为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。

ZoneAvoidanceRule

以区域可用的服务器为基础进行服务器的选择。使用 Zone 对服务器进行分类,这个 Zone 可以理解为一个机房、一个机架等。而后再对 Zone 内的多个服务做轮询。

BestAvailableRule

忽略那些短路的服务器,并选择并发数较低的服务器。

RandomRule

随机选择一个可用的服务器。

RetryRule

重试机制的选择逻辑

默认的实现就是 ZoneAvoidanceRule,是一种轮询方案

如果想要自定义负载均衡 , 可以自己创建类实现 IRule 接口 , 然后再通过配置类或者配置文件配置即可 :

通过定义 IRule 实现可以修改负载均衡规则,有两种方式:

  1. 代码方式:在 order-service 中的 OrderApplication 类中,定义一个新的 IRule:

    @Bean
    public IRule randomRule(){
    return new RandomRule();
    }

  2. 配置文件方式:在 order-service 的 application.yml 文件中,添加新的配置也可以修改规则:

    userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
    ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则

大部分的固定的配置文件都放在服务本地 , 一些根据环境不同可能会变化的部分, 放到 Nacos 中

限流一般有二种方式设置 :

第一种 : 网关配置限流

spring:
  application:
    name: api-gateway
  redis:
    host: localhost
    port: 6379
    password:
  cloud:
    gateway:
      routes:
        - id: cloud-gateway
          uri: http://192.168.1.211:8088/
          predicates:
            - Path=/ytb/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1   # 令牌桶每秒填充速率
                redis-rate-limiter.burstCapacity: 2   # 令牌桶总容量
                key-resolver: "#{@pathKeyResolver}"   # 使用 SpEL 表达式按名称引用 bean

在上面的配置文件,配置了 redis 的信息,并配置了 RequestRateLimiter 的限流过滤器,该过滤器需要配置三个参数:

burstCapacity,令牌桶总容量。

replenishRate,令牌桶每秒填充平均速率。

key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据 #{@beanName} 从 Spring 容器中获取 Bean 对象

@Configuration
public class KeyResolverConfiguration {
    @Bean
    public KeyResolver pathKeyResolver(){
        return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
    }
}

常见的限流算法有:计数器算法,漏桶(Leaky Bucket)算法,令牌桶(Token Bucket)算法。

Spring Cloud Gateway 官方提供了 RequestRateLimiterGatewayFilterFactory 过滤器工厂,使用 Redis 和 Lua 脚本实现了 令牌桶 的方式。

令牌桶算法 是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。

放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌。所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置 qps 为 100,那么限流器初始化完成一秒后,桶中就已经有 100 个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的 100 个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。

第二种 : 使用服务保护组件 Sentinel 实现限流

建议回去看看微服务保护课程中的限流配置

我们项目中使用 Hystrix/Sentinel 实现的断路器 , 断路器状态机包括三个状态:

  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到 open 状态
  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open 状态 5 秒后会进入 half-open 状态
  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。
    • 请求成功:则切换到 closed 状态
    • 请求失败:则切换到 open 状态

我们项目中涉及到服务调用得地方都会定义降级, 一般降级逻辑就是返回默认值 , 降级的实现也非常简单 , 就是创建一个类实现FallbackFactory接口 , 然后再对应的 Feign 客户端接口上面 , 通过@FeignClient 指定降级类

@Component
@Slf4j
public class OrderServiceFallbackFactory implements FallbackFactory<OrderService> {
    @Override
    public OrderService create(Throwable throwable) {
        log.error("调用订单服务失败",throwable);

        return new OrderService() {
            @Override
            public String weixinPay(PayVO payVO) {
                return null;
            }

            @Override
            public Pager<OrderVO> search(Integer pageIndex, Integer pageSize, String orderNo, String openId, String startDate, String endDate) {
                return new Pager<>();
            }

            @Override
            public List<Long> getBusinessTop10Skus(Integer businessId) {
                return Lists.newArrayList();
            }
        };
    }
}

我们会根据不同的异常情况定义异常类 , 实现 RuntimeException 接口 , 然后在需要进行异常处理的位置对外抛出对应异常

在项目中使用@ControllerAdvice + @ExceptionHandler 捕获指定异常 , 处理异常

SpringBoot 项目读取配置文件常用的方式有二种 :

  1. 通过@Value 注解通过属性名称读取

  2. 通过@ConfigurationProperties 属性 , 批量读取配置文件配置到属性配置类

实现热更新的方式也有二种 : 首先需要将配置文件配置到配置中心中 , 之后通过

  1. @Value + @RefreshScope 注解实现热更新
  2. 通过@ConfigurationProperties 注解读取的属性, 自动会热更新