Java八股文——Spring「SpringCloud 篇」
了解Spring Cloud吗,说一下他和Spring Boot的区别
面试官您好,我非常了解Spring Cloud,并且在项目中也深度使用过。我认为,要理解Spring Boot和Spring Cloud的区别,最好的方式是把它们看作是构建现代应用程序的两个不同阶段的解决方案。
它们之间是 “基础与上层建筑” 的关系,而不是“竞争”关系。
一个核心的比喻:造房子 vs. 建小区
-
Spring Boot:就像一个 “模块化的预制房屋建造工具”。它能让我们以极快的速度,独立地、高质量地建造出一栋栋功能完善的房子(单个微服务)。它关心的是单体建筑的内部结构、装修、水电(自动化配置、内嵌服务器、起步依赖)。
-
Spring Cloud:则是在这些房子都建好之后,用来规划和管理整个“现代化小区”(分布式系统) 的工具集。它不关心单栋房子内部怎么装修,它关心的是:
- 小区的 “物业中心”(服务注册与发现)。
- 小区内部的 “道路交通网络” 和 “智能导航”(负载均衡)。
- 每家每户之间的 “电话线路”(服务间的调用)。
- 小区的 “总大门”和“保安”(API网关)。
- 小区的 “电网保险丝”(熔断器)。
- 小区的 “广播系统”(统一配置中心)。
第一阶段:使用Spring Boot构建微服务
- 做什么:Spring Boot是一个快速开发脚手架。它的核心目标是让我们能够快速、简单地创建出独立的、生产级的、可运行的Spring应用。
- 它解决了:传统Spring框架的配置繁琐、依赖管理复杂、部署困难等问题。
- 结果:我们可以用Spring Boot,轻松地开发出
用户服务
、订单服务
、商品服务
等一个个独立的微服务。
第二阶段:当微服务变多后,问题出现了
当这些用Spring Boot建好的“房子”越来越多时,一个“分布式”的难题就摆在了我们面前:
- “我该如何找到你?” —— 服务A如何知道服务B的IP地址和端口?如果服务B部署了多个实例,又该如何处理?
- “你挂了怎么办?” —— 服务A调用服务B,如果服务B因为高负载而响应缓慢甚至宕机,会不会把服务A也拖垮?
- “这么多服务,配置如何统一管理?” —— 每个服务都有自己的配置文件,如果一个公共配置(比如数据库密码)需要修改,难道要去几十个项目里一个个改吗?
- “外部请求该打到哪里?” —— 客户端(比如手机App)如何知道该调用哪个微服务的哪个实例?权限校验、限流这些通用逻辑难道要在每个服务里都实现一遍吗?
第三阶段:使用Spring Cloud治理微服务
Spring Cloud就是为了解决以上所有分布式系统难题而生的一整套“微服务治理全家桶”。
它不是一个单一的框架,而是一系列子项目的集合,每个子项目都解决一个特定的分布式问题,并且都以spring-cloud-starter-*
的形式与Spring Boot无缝集成。
-
为了解决“如何找到你”:Spring Cloud提供了服务注册与发现的组件,如
Spring Cloud Netflix Eureka
(已不推荐)或更主流的Spring Cloud Alibaba Nacos
。所有服务启动时都去“物业中心”(Nacos)注册,调用时也先去这里查询地址。 -
为了解决“你挂了怎么办”:
- 负载均衡:
Spring Cloud LoadBalancer
(替代了Ribbon)可以配合Nacos,在多个服务实例之间实现智能的负载均衡。 - 服务调用:
Spring Cloud OpenFeign
让我们能像调用一个本地Java接口一样,优雅地发起远程RPC调用。 - 熔断降级:
Spring Cloud Circuit Breaker
(集成了Resilience4J)或Spring Cloud Alibaba Sentinel
提供了强大的熔断、降级、限流能力,防止服务间的“雪崩效应”。
- 负载均衡:
-
为了解决“配置统一管理”:
Spring Cloud Config
或Spring Cloud Alibaba Nacos
提供了分布式配置中心的功能。 -
为了解决“外部请求入口”:
Spring Cloud Gateway
提供了一个强大、可编程的API网关,统一处理路由、鉴权、限流等所有入口流量。
总结一下:
- Spring Boot 是“术”,专注于快速构建单个微服务单元,是微服务的实现技术。
- Spring Cloud 是“道”,专注于治理和协调这些微服务单元,是构建分布式系统的架构思想和解决方案。
在现代微服务开发中,它们是密不可分的黄金搭档:我们用Spring Boot来“建房子”,用Spring Cloud来“建小区”。
用过哪些微服务组件?
面试官您好,是的,在我的项目中,我们采用了非常典型的微服务架构。为了支撑这套架构的稳定、高效运行,我们引入了一系列成熟的微服务组件来解决分布式环境下遇到的各种挑战。
我们当时的技术选型主要是基于Spring Cloud Alibaba生态,因为它提供了一套非常完整、且在国内社区非常活跃的解决方案。
下面,我将结合我们项目的实践,来介绍一下我们用到的核心组件,以及它们分别解决了什么问题:
1. 服务注册与发现:Nacos Discovery
- 解决了什么问题? 解决了在动态、弹性的云环境中,一个服务(消费者)如何准确地找到另一个服务(提供者) 的所有健康实例的IP和端口。
- 我们如何用?
- 我们部署了一个Nacos Server作为注册中心。
- 每一个微服务(如
order-service
,user-service
)在启动时,都会通过spring-cloud-starter-alibaba-nacos-discovery
这个starter,自动地将自己的服务名、IP、端口等信息注册到Nacos Server上,并定时发送心跳来维持在线状态。 - 当
order-service
需要调用user-service
时,它不会硬编码IP地址,而是通过Nacos的客户端,向Nacos Server发起一次服务发现请求:“请告诉我user-service
所有健康的实例列表”。
2. 统一配置管理:Nacos Config
- 解决了什么问题? 解决了在数十个微服务、上百个实例中,如何集中管理、动态更新所有配置的问题。
- 我们如何用?
- 我们同样使用Nacos Server作为配置中心。
- 我们将所有微服务的
application.yml
中的配置项,都迁移到了Nacos的配置管理界面上,并按照服务名、环境(dev/test/prod)进行分组。 - 每个微服务启动时,通过
spring-cloud-starter-alibaba-nacos-config
,会优先从Nacos Server拉取配置。 - 最大的好处:当我们需要修改一个配置(比如数据库密码或一个业务开关)时,只需要在Nacos界面上修改并发布,所有相关的微服务都能够热更新,无需重启应用,极大地提升了运维效率。
3. 服务间通信与负载均衡:OpenFeign + Spring Cloud LoadBalancer
- 解决了什么问题?
- 服务通信:解决了如何优雅地、像调用本地方法一样发起远程RPC调用的问题。
- 负载均衡:在拿到服务实例列表后,如何以一种智能的策略(如轮询、随机、权重)来选择一个实例进行调用的问题。
- 我们如何用?
- 我们使用OpenFeign。只需要定义一个Java接口,并用
@FeignClient("user-service")
注解标记,Spring Cloud就会为我们动态创建一个实现类。调用这个接口的方法,就等于发起了一次对user-service
的HTTP请求。 - Feign内部集成了
Spring Cloud LoadBalancer
,它会自动从Nacos获取到的服务实例列表中,通过默认的轮询策略,选择一个实例来发送请求。
- 我们使用OpenFeign。只需要定义一个Java接口,并用
4. API网关:Spring Cloud Gateway
- 解决了什么问题? 为整个微服务系统提供一个统一的、安全的入口。
- 我们如何用?
- 我们部署了一个独立的
gateway-service
。 - 它负责路由:根据请求的路径(如
/api/user/**
),将外部请求转发到内部的user-service
。 - 它负责统一鉴权:在这里集中进行用户的Token校验、权限验证。
- 它还负责限流、跨域、日志记录等所有通用的入口策略。
- 我们部署了一个独立的
5. 服务保护与熔断降级:Sentinel
- 解决了什么问题? 解决了在分布式系统中,如何防止因某个服务的延迟或故障,而导致整个调用链像多米诺骨牌一样 “雪崩” 的问题。
- 我们如何用?
- 我们通过
spring-cloud-starter-alibaba-sentinel
为核心服务接入了Sentinel。 - 我们配置了流量控制规则(QPS限流),防止服务被突发流量打垮。
- 我们配置了熔断降级规则:当对某个下游服务的调用,在一段时间内的错误率或慢调用比例超过阈值时,Sentinel会自动“熔断”该调用,在接下来的时间窗口内,所有对该服务的调用都会直接失败并快速返回一个降级逻辑(比如返回一个缓存的默认值),从而保护了上游服务,避免了无效等待。
- 我们通过
除了这些,我们也使用了像ELK来进行集中式日志管理,以及正在调研引入SkyWalking来实现分布式链路追踪,以便能更直观地监控和排查复杂的跨服务调用问题。
通过这一整套组件的协同工作,我们才得以构建出一个健壮、高可用、易于管理的微服务体系。
负载均衡有哪些算法?
面试官您好,负载均衡是构建高可用、高可扩展分布式系统的核心技术。它的算法多种多样,我通常会把它们分为两大类:静态负载均衡算法和动态负载均衡算法。
第一类:静态负载均衡算法 (不关心后端状态)
这类算法的特点是,它们在做分发决策时,不考虑后端服务器当前的实时负载和运行状态,决策规则是预先设定好的。
-
轮询 (Round Robin)
- 原理:这是最简单、最经典的算法。它像发扑克牌一样,按顺序将请求依次分发给服务器列表中的每一台服务器。
- 优点:绝对公平,实现极其简单。
- 缺点:完全不考虑服务器的性能差异。如果一台高性能服务器和一台低性能服务器混在一起,低性能服务器可能会因为负载过高而崩溃。
- 适用场景:适用于后端服务器性能基本一致的集群。
-
加权轮询 (Weighted Round Robin)
- 原理:这是对简单轮询的改进。我们可以根据服务器的性能(如CPU、内存)为其配置一个权重值。权重越高的服务器,会被分配到越多的请求。
- 优点:解决了服务器性能异构的问题,能让性能更强的服务器“多劳多得”。
- 缺点:它仍然是一种静态策略,无法应对服务器实时的负载波动。比如,一台权重高的服务器可能因为某个慢查询而突然负载飙升,但加权轮询算法并不知道这一点。
-
随机 (Random)
- 原理:从服务器列表中随机选择一台来处理请求。当请求量足够大时,请求会趋向于平均分配。
- 优点:实现简单。
- 缺点:与轮询类似,无法处理性能异构。
-
加权随机 (Weighted Random)
- 原理:结合了权重和随机。服务器被选中的概率与其权重成正比。
- 优点:解决了随机算法的性能异构问题。
第二类:动态负载均衡算法 (关心后端实时状态)
这类算法更“智能”,它们在分发请求时,会参考后端服务器当前的实时运行状态(如连接数、响应时间),来做出最优的决策。
-
最小连接数 (Least Connections) / 最小活跃数 (Least Active)
- 原理:这是最常用、最有效的动态负载均衡算法之一。负载均衡器会实时地统计每台后端服务器上当前正在处理的活动连接数。当一个新的请求到来时,它会把这个请求发送给当前活动连接数最少的那台服务器。
- 优点:非常智能,能根据服务器的实时负载情况,将新请求导向最空闲的服务器,实现动态的、真正的负载均衡。
- 适用场景:适用于绝大多数场景,特别是当请求的处理时间长短不一、服务器负载波动较大时。
-
最快响应时间 (Fastest Response Time)
- 原理:负载均衡器会持续监控每台服务器的平均响应时间,并将新请求发送给响应最快的那台服务器。
- 优点:能将用户请求导向当前处理能力最强的服务器。
- 缺点:需要额外的机制来持续测量和计算响应时间。
特殊的哈希算法
- IP哈希 (IP Hash) / 一致性哈希 (Consistent Hashing)
- 原理:这类算法不是为了“均衡”,而是为了 “会话保持”(Session Persistence)。它会根据请求的某个固定特征(最常用的是客户端的源IP地址)来进行哈希计算,然后将哈希结果映射到一台固定的服务器上。
- 优点:能够保证来自同一个客户端的所有请求,始终被分发到同一台后端服务器。
- 适用场景:非常适合那些需要在服务端保存会话状态的应用。比如,一个未实现Session共享的Web应用集群,必须使用IP哈希,来确保用户的登录状态不会因为请求被转发到不同服务器而丢失。
实践与选型
在实践中:
- 像Nginx这样的硬件/软件负载均衡器,通常都支持轮询、加权轮询、IP哈希、最小连接数等多种策略。
- 在微服务架构中,像Spring Cloud LoadBalancer(替代了Ribbon)这样的客户端负载均衡器,默认采用的是轮询,但也提供了扩展接口,让我们能方便地替换成其他策略,比如基于Nacos权重的加权随机策略。
我的选型原则是:
- 对于无状态服务,优先考虑最小连接数或加权轮询。
- 对于有状态服务,必须使用IP哈希或一致性哈希。
如何实现一直均衡给一个用户?
面试官您好,要实现“一直将同一个用户的请求分发给同一台服务器”,这种需求通常被称为 “会话保持”(Session Persistence) 或 “会话粘滞”(Session Stickiness)。其核心思想是,根据请求中某个不变的特征,通过哈希算法,来固定地映射到一台服务器。
实现这个需求,主要有两种哈希算法,一种是简单直接的,另一种是更高级、更健壮的。
方案一:简单哈希取模法 (也常被称为IP Hash)
这是最直观的实现方式。
-
工作原理:
- 选择一个固定标识:从用户的请求中,提取一个能够唯一标识该用户的、相对固定的信息。最常用的就是客户端的源IP地址。
- 哈希计算:对这个IP地址字符串进行哈希运算,得到一个整数哈希值。比如
hash("123.45.67.89") -> 12345678
。 - 取模映射:用这个哈希值,对后端服务器的总数量进行取模运算,得到的结果就是目标服务器在列表中的索引。
index = hash(client_ip) % server_count
- 只要用户的IP不变,服务器集群的数量也不变,那么每次计算出的
index
就是固定的,请求也就会被固定地分发到同一台服务器。
-
致命缺点:不抗服务器数量变化
- 这种方法的容错性和扩展性非常差。
- 想象一下:我们有10台服务器,现在因为故障,其中1台下线了,服务器总数
server_count
从10变成了9。 - 这时,对于几乎所有的
client_ip
,hash(client_ip) % 9
的结果,都会与之前的hash(client_ip) % 10
的结果完全不同。 - 后果:这会导致几乎所有用户的会话映射关系,在瞬间全部失效。如果我们在服务器上用Session保存了用户的登录状态或购物车信息,那么在这一刻,几乎所有用户都会被强制“下线”或“清空购物车”,引发一场“雪崩式”的灾难。同样,增加一台服务器也会导致同样的问题。
方案二:一致性哈希算法 (Consistent Hashing) —— 更优越的解决方案
为了解决简单哈希取模的“雪崩”问题,一致性哈希算法被设计了出来。
-
核心思想:它不再是简单地对服务器数量取模,而是将哈希值映射到一个虚拟的、环形的哈希空间上。
-
工作原理(一个生动的比喻):
- 构建哈希环:我们想象一个首尾相连的时钟表盘(比如0到 2^32-1)。这个表盘就是我们的哈希环。
- 服务器上环:我们对每一台服务器的IP或主机名进行哈希计算,得到一个哈希值,然后将这个服务器节点“钉”在时钟表盘上对应刻度的位置。
- 请求上环与映射:当一个用户请求到来时,我们同样对它的客户端IP进行哈希计算,得到一个哈希值。这个值也会落在时钟表盘的某个位置。
- 顺时针寻找:然后,从这个请求的哈希位置开始,沿着表盘顺时针方向“走”,遇到的第一个服务器节点,就是这个请求应该被路由到的目标服务器。
-
为什么能抗服务器数量变化?
- 当一台服务器下线时(比如上图中,移除了Server C):
- 只有那些原本应该路由到Server C的请求,现在会“继续顺时针走”,并最终落到它的下一个节点——Server D上。
- 而原本路由到Server A, B, D的请求,它们的映射关系完全不受影响。
- 结论:服务器的增减,只会影响到哈希环上一小段弧度的请求映射,而不会像简单哈希取模那样,导致“全局洗牌”。这极大地提高了系统的稳定性和可用性。
- 当一台服务器下线时(比如上图中,移除了Server C):
-
优化:虚拟节点 (Virtual Nodes)
- 为了解决当服务器节点太少时,可能导致的“哈希倾斜”(即大量请求集中到少数几台服务器上)问题,一致性哈希通常会引入虚拟节点。
- 我们会为每一个物理服务器,创建出多个“虚拟分身”,并将这些虚拟节点均匀地散布在哈希环上。这样,请求就能更均匀地分布到各个物理服务器上。
总结一下,要实现“一直将同一个用户分发到同一台服务器”,我会选择一致性哈希算法。虽然它比简单的IP哈希取模要复杂一些,但它在处理服务器集群动态伸缩(上线/下线)时,能最大限度地保持会话映射的稳定性,是构建高可用、有状态服务的事实标准。
介绍一下服务熔断
面试官您好,服务熔断是微服务架构中一种至关重要的服务保护机制。
它的作用就像电路中的 “保险丝”,其核心目标是防止在分布式系统中,因某个下游服务的故障或延迟,而导致整个调用链像多米诺骨牌一样接连崩溃,即所谓的“雪崩效应”。
1. 为什么需要服务熔断?—— 雪崩效应的模拟
“A -> B -> C”的例子非常经典。
- 正常情况:客户端请求服务A,A调用B,B再调用C。
- 故障发生:突然,服务C因为数据库慢查询或自身Bug,导致响应极度缓慢甚至不可用。
- 问题蔓延:
- 所有调用C的服务B的线程,都会因为等待C的响应而被阻塞、挂起。
- 随着请求不断涌入A,对B的调用也越来越多,很快,服务B的线程池就会被占满。
- 此时,服务B自身也变得不可用,无法响应来自服务A的任何请求。
- 最终,服务A的线程池也被调用B的请求所占满,服务A也崩溃了。
- 结果:一个下游的、非核心的服务C的故障,最终拖垮了整个上游链路,这就是“雪崩”。
2. 服务熔断器是如何工作的?—— 有限状态机模型
为了解决这个问题,我们在服务调用方(比如在服务A调用B、服务B调用C的地方)引入一个 “熔断器”(Circuit Breaker)。这个熔断器就像一个智能的开关,它有三种状态:
-
关闭状态 (Closed):
- 这是默认的、正常的状态。
- 此时,熔断器是“关闭”的,所有请求都可以正常地通过它,去调用下游服务。
- 熔断器会默默地统计最近一段时间内(比如1分钟)的调用成功率和失败率。
-
打开状态 (Open):
- 触发条件:当熔断器检测到,最近的失败率(或慢调用比例)超过了一个预设的阈值(比如,10秒内有超过50%的请求失败),它就会 “跳闸”,从“关闭”状态切换到“打开”状态。
- 行为:一旦进入“打开”状态,后续所有对该下游服务的调用,将不会再发起真正的网络请求。而是会直接快速失败,立即返回一个错误,或者执行一个预设的降级逻辑(Fallback)。
- 降级逻辑:比如,查询商品详情时,如果商品服务被熔断了,我们可以不返回错误,而是返回一个“商品服务繁忙,请稍后再试”的友好提示,或者返回一个之前缓存的、旧的商品数据。
- 作用:通过这种方式,它保护了调用方(上游服务),使其线程不会因为等待一个已知的、故障的服务而被耗尽,从而阻止了雪崩的蔓延。
-
半开状态 (Half-Open):
- 触发条件:在“打开”状态持续一段时间后(比如1分钟),熔断器会进入“半开”状态。这是一种试探性的恢复状态。
- 行为:它会允许一小部分请求(比如1个)“小心翼翼地”地通过,去调用下游服务。
- 状态转换:
- 如果这次试探性的调用成功了,熔断器就认为下游服务已经恢复,于是它会完全闭合,回到“关闭”状态,恢复正常调用。
- 如果这次试探仍然失败,熔断器就认为下游服务还没好,它会重新回到“打开”状态,继续等待下一个“半开”的计时周期。
3. 主流的技术实现
- Hystrix:这是Netflix开源的、最早被广泛使用的熔断器组件,也是您提到的。但目前,Hystrix已经进入维护状态,官方不再进行新功能开发。
- Resilience4J:这是一个更轻量、更函数式的熔断器库,是目前Spring Cloud官方推荐的Hystrix替代品之一。
- Sentinel:这是阿里巴巴开源的一款面向分布式服务架构的、以流量为切入点的流量控制、熔断降级、系统负载保护的综合性组件。它不仅功能强大,还提供了一个非常直观的实时监控和规则配置控制台。在Spring Cloud Alibaba生态中,Sentinel是实现服务保护的事实标准。
总结一下,服务熔断通过一个 “关闭-打开-半开”的状态机模型,实现了对下游故障服务的智能隔离和自动恢复,是保障微服务系统高可用的、不可或缺的一道防线。
介绍一下服务降级
面试官您好,服务降级是构建高可用、高弹性分布式系统的一种防御性策略。
它的核心思想就是 “舍卒保车”。当系统面临巨大的访问压力,或者某些非核心的下游服务出现问题时,我们有策略地、暂时地放弃一些非核心功能,或者为它们提供一个“有损”的、简化的服务,从而将宝贵的服务器资源集中起来,全力保障核心业务的稳定和可用。
1. 服务降级的触发方式
在实践中,服务降级的触发,可以分为自动触发和手动触发两种。
-
a. 自动降级(通常由熔断或限流触发)
- 超时降级:当对一个下游服务的调用,响应时间超过了预设的阈值(比如1秒),我们就认为它已经不稳定,直接触发降级逻辑,不再等待。
- 失败次数降级/熔断降级:当对一个下游服务的调用,在一段时间内的失败率超过了阈值,服务熔断器会“跳闸”。在接下来的时间里,所有对该服务的调用都会被自动地、快速地导向降级逻辑。这是服务熔断必然带来的一个结果。
- 限流降级:当我们为某个接口设置了QPS(每秒查询率)上限,当请求量超过这个阈值时,超出的请求就会被自动地执行降级逻辑。
-
b. 手动降级(通过配置开关)
- 这是一种主动的、有预案的降级方式。我们会在代码中预埋一个 “降级开关”,这个开关的状态通常由 配置中心(如Nacos) 来控制。
- 典型场景:比如“双十一”大促零点之前,运维人员会通过配置中心,提前手动关闭一些非核心的功能,比如“商品评价的显示”、“用户积分的计算”、“推荐算法的实时更新”等。这样,当零点的流量洪峰到来时,服务器就可以将所有资源都用于保障“下单”、“支付”这两个最核心的功能。大促高峰过后,再通过配置中心,把这些降级的功能重新打开。
2. 服务降级的实现手段
降级的最终表现,就是提供一个 Fallback(兜底) 方案。这个Fallback可以有很多种形式:
- 返回错误码或友好提示:这是最简单的。比如,直接返回一个JSON,内容是
{ "code": 503, "message": "服务繁忙,请稍后再试" }
。 - 返回一个缓存的、默认的或简化的数据:
- 比如,在商品详情页,如果推荐服务被降级了,我们可以不显示推荐商品模块,或者显示一个预先准备好的、所有用户都一样的“通用爆款”列表。
- 如果用户的收货地址列表服务被降级了,我们可以返回
null
,让用户在下单时手动填写。
- 执行一个简化的、本地的计算逻辑:
- 比如,一个复杂的、依赖多个外部服务的推荐算法被降级了,我们可以执行一个非常简单的、只依赖本地数据的“猜你喜欢”逻辑。
- 将请求转移到异步队列:
- 对于一些非实时的写操作,比如“用户发表评论”,如果服务压力大,我们可以不直接写入数据库,而是将评论内容先快速地写入一个 消息队列(MQ) 中,然后立即返回成功。由后台的消费者在系统负载低时,再慢慢地从队列中取出数据进行处理。
3. 与服务熔断的关系
- 服务熔断是一种因,是系统因为下游服务持续故障而采取的一种自动保护措施(“保险丝断了”)。
- 服务降级是一种果,是熔断发生后,或者系统为了主动应对高压而采取的一种处理结果(“提供兜底方案”)。
- 可以说,服务熔断必然会导致服务降级,但服务降级不一定是由熔断引起的,也可能是由限流或手动开关触发的。
总结一下,服务降级是系统在面临极端压力时的一种“丢车保帅”的智慧。通过合理地设计降级预案,并结合自动化的熔断、限流和手动的配置开关,我们就能极大地提升系统的弹性和可用性,确保在最糟糕的情况下,核心功能依然坚如磐石。
参考小林coding和JavaGuide