day11-微服务面试篇
day11-微服务面试篇
常见的面试题
微服务在面试时被问到的内容相对较少,常见的面试题如下:
- SpringCloud 有哪些常用组件?分别是什么作用?
- 服务注册发现的基本流程是怎样的?
- Eureka 和 Nacos 有哪些区别?
- Nacos 的分级存储模型是什么意思?
- Ribbon 和 SpringCloudLoadBalancer 有什么差异
- 什么是服务雪崩,常见的解决方案有哪些?
- Hystix 和 Sentinel 有什么区别和联系?
- 限流的常见算法有哪些?
- 什么是 CAP 理论和 BASE 思想?
- 项目中碰到过分布式事务问题吗?怎么解决的?
- AT 模式如何解决脏读和脏写问题的?
- TCC 模式与 AT 模式对比,有哪些优缺点
可以发现,这些问题都是围绕着 SpringCloud 的相关组件的,其中有些问题我们在课堂上已经介绍过,这里不再赘述。我们重点讲解一些之前没有讲过的,与底层实现有关的部分。
讲解的思路还是基于 SpringCloud 的组件分类来讲的,主要包括:
- 分布式事务
- 注册中心
- 远程调用
- 服务保护
等几个方面
0.分布式事务问题 🍐
0.1.本地事务Vs分布式事务
本地事务Vs分布式事务
本地事务,也就是传统的单机事务。在传统数据库事务中,必须要满足四个原则:

原子性(Atomicity):本地事务是原子的,要么全部成功,要么全部失败。如果在事务执行过程中发生错误,会回滚事务,以确保不会留下不一致的状态。
一致性(Consistency):本地事务保证了数据库或资源的一致性。在事务开始和结束时,数据应该遵循事务的完整性规则,确保数据的状态在事务之前和之后没有不一致。
隔离性(Isolation):事务之间应该是隔离的,即一个事务的执行不应影响其他事务。本地事务通常使用锁和事务隔离级别来确保不同事务的数据互相隔离。
- 读未提交(Read Uncommitted):最低的隔离级别。允许一个事务读取另一个事务尚未提交的数据。可能导致脏读、不可重复读和幻影读。
- 读已提交(Read Committed):允许一个事务只能读取已经提交的数据,而不能读取其他事务尚未提交的数据。防止脏读,但仍可能发生不可重复读和幻影读。
- 可重复读(Repeatable Read):保证一个事务在读取数据时不会被其他事务所修改。防止脏读和不可重复读,但仍可能发生幻影读。
- 串行化(Serializable):最高的隔离级别。保证事务之间的完全隔离,不会发生脏读、不可重复读和幻影读。通常会带来最高的性能开销,因为它需要强制顺序执行事务
持久性(Durability):一旦事务被提交,其结果应该在系统故障或崩溃后仍然保持持久。这通常涉及将事务更改写入持久性存储(如磁盘)。
单一资源:本地事务仅涉及单个资源或数据库,没有涉及分布式事务或多个不同的资源。这意味着所有操作都在一个数据库上执行,而不涉及多个数据库或外部系统。
较低的复杂性:与分布式事务相比,本地事务通常更简单,因为它们不需要协调多个参与者或解决分布式一致性问题。
较高的性能:由于本地事务的较低复杂性和较少的协调开销,它们通常具有更高的性能,因为不需要处理网络通信和分布式系统中的复杂协议。
分布式事务就是指不是
在单个服务或单个数据库架构下,产生的事务,例如:
- 跨数据源的分布式事务
- 跨服务的分布式事务
- 综合情况
在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。例如电商行业中比较常见的下单付款案例,包括下面几个行为:
- 创建新订单
- 扣减商品库存
- 从用户账户余额扣除金额
完成上面的操作需要访问三个不同的微服务和三个不同的数据库。

订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证ACID原则。
但是当我们把三件事情看做一个"业务",要满足保证“业务”的原子性
,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务了。
此时ACID难以满足,这是分布式事务要解决的问题
电子商务平台:
- 场景:当用户下订单时,需要将订单信息写入订单数据库,同时扣减库存信息。这涉及两个不同的数据库操作,需要确保它们要么同时成功,要么同时失败,以避免库存与订单数据不一致。
- 解决方案:使用分布式事务来确保订单和库存的一致性。
金融交易:
- 场景:在进行跨银行或国际金融交易时,需要从一个账户扣款并将资金存入另一个账户。这牵涉到多个金融机构和多个数据库。
- 解决方案:使用分布式事务来保证交易的一致性和可靠性。
酒店预订系统:
- 场景:当用户预订酒店时,需要从酒店库存中扣除房间,并将订单信息写入预订数据库。这牵涉到多个酒店和订单系统。
- 解决方案:使用分布式事务来确保库存和订单的一致性。
电信运营商:
- 场景:当用户切换手机运营商时,需要将用户的手机号信息从一个运营商转移到另一个运营商。这牵涉到多个运营商的系统和数据库。
- 解决方案:使用分布式事务来保证号码转移的一致性。
供应链管理:
- 场景:在供应链中,生产、库存和物流管理需要协同工作,以确保产品按时交付。这牵涉到多个环节和数据库系统。
- 解决方案:使用分布式事务来协调不同环节的操作,以确保供应链的一致性。
在线支付:
- 场景:当用户进行在线支付时,需要从银行账户扣款,并更新商家的订单状态。这牵涉到用户的银行和商家的支付系统。
- 解决方案:使用分布式事务来确保支付和订单状态的一致性。
医疗信息交换:
- 场景:在医疗领域,患者的医疗信息需要在不同医院和诊所之间交换。这牵涉到不同的医疗信息系统和数据库。
- 解决方案:使用分布式事务来确保医疗信息的安全传输和一致性。
多国际化企业:
- 场景:跨越不同国家和地区的企业需要处理多个国家的税务、法规和货币。这牵涉到多个国家的财务系统和数据库。
- 解决方案:使用分布式事务来确保跨国企业操作的合规性和一致性。
1.理论基础 ❤️ 🍐
解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导。🎯 ❤️
1.1.CAP定理 ❤️ 🍐
CAP定理
1998年,加州大学的计算机科学家 Eric Brewer【埃里克·布鲁尔】 提出,分布式系统有三个指标。
- Consistency(一致性) [kənˈsɪstənsi]
- Availability(可用性) [əˌveɪləˈbɪləti]
- Partition tolerance (分区容错性)[pɑrˈtɪʃ(ə)n] [ˈtɑlərəns]

它们的第一个字母分别是 C、A、P。
Eric Brewer【埃里克·布鲁尔】 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
CAP 定理解释:👇
Consistency(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。
比如现在包含两个节点,其中的初始数据是一致的:

当我们修改其中一个节点的数据时,两者的数据产生了差异:

要想保住一致性,就必须实现node01 到 node02的数据 同步:

Availability(可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝。
如图,有三个节点的集群,访问任何一个都可以及时得到响应:

当有部分节点因为网络故障或其它原因无法访问时,代表节点不可用:

Partition(分区):因为网络故障或其它原因导致分布式系统中的部分节点与其它节点失去连接,形成独立分区。

Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务
节点并没有宕机,客户端还可以连接

在分布式系统中,系统间的网络不能100%
保证健康,一定会有故障的时候,而服务有必须对外保证服务。因此Partition Tolerance
不可避免。
当节点接收到新的数据变更时,就会出现问题了:

如果此时要保证一致性,就必须等待网络恢复,完成数据同步后,整个集群才对外提供服务,服务处于阻塞状态,不可用。
如果此时要保证可用性,就不能等待网络恢复,那node01、node02与node03之间就会出现数据不一致。
👉 👉也就是说,在P一定会出现的情况下,A和C之间只能实现一个。🎯
总结
- 解释CAP定理是什么?
答案: CAP定理是由计算机科学家Eric Brewer提出的,它指出在分布式系统中,无法同时满足三个属性:一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)。根据CAP定理,分布式系统只能满足其中两个属性中的任意两个,但无法同时满足所有三个。
- 分布式系统节点通过网络连接,一定会出现分区问题(P)
- 当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足
1.2.BASE理论 ❤️ 🍐
BASE理论

BASE理论是对CAP的一种解决思路,包含三个思想:
- Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
- Soft State(软状态) :在一定时间内,允许出现中间状态,比如临时的不一致状态。
- Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
解决分布式事务的思路:🍐

分布式事务最大的问题是各个子事务的一致性问题
,因此可以借鉴CAP定理
和BASE理论
,有两种解决思路:
AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。AT模式
CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。XA模式
思考:为什么P都要存在?
- 不管是哪一种模式,都需要在子系统事务之间互相通讯,协调事务状态,也就是需要一个事务协调者(TC):
这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务。
总结
- 简述BASE理论三个思想:
- 基本可用
- 软状态
- 最终一致
- 解决分布式事务的思想和模型:
- 全局事务:整个分布式事务
- 分支事务:分布式事务中包含的每个子系统的事务
- 最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据
- 强一致思想:各分支事务执行完业务不要提交,等待彼此结果。而后统一提交或回滚
1.3.AT模式
AT模式
AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。
1.Seata的AT模型
基本流程图:

阶段一RM的工作:
- 注册分支事务
- 记录undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态
阶段二提交时RM的工作:
- 删除undo-log即可
阶段二回滚时RM的工作:
- 根据undo-log恢复数据到更新前
2.AT与XA的区别
简述AT模式与XA模式最大的区别是什么?
- XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
- XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
- XA模式强一致;AT模式最终一致
3.脏写问题
在多线程并发访问AT模式的分布式事务时,有可能出现脏写问题 ,如图:

解决思路就是引入了全局锁 的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。

4.优缺点
AT模式的优点:
- 一阶段完成直接提交事务,释放数据库资源,性能比较好
- 利用全局锁实现读写隔离
- 没有代码侵入,框架自动完成回滚和提交
AT模式的缺点:
- 两阶段之间属于软状态,属于最终一致
- 框架的快照功能会影响性能,但比XA模式要好很多
总结
课堂作业
- AT模式优缺点?🎤
- AT 模式如何解决脏读和脏写问题的?🎤
1.4.TCC模式 🍐
TCC模式
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
Try:资源的检测和预留;
Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
Cancel:预留资源释放,可以理解为try的
TCC流程分析 👇
举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。
- 阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30
初识余额:

余额充足,可以冻结:

此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。
- 阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30
确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了:

此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元
- 阶段二(Canncel):如果要回滚(Cancel),则冻结金额扣减30,可用余额增加30
需要回滚,那么就要释放冻结金额,恢复可用金额:

Seata的TCC模型
Seata中的TCC模型依然延续之前的事务架构,如图:

TCC模型优缺点
TCC模式的每个阶段是做什么的?
- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放
TCC的优点是什么?
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 相比AT模型,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点是什么?
- 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
- 软状态,事务是最终一致
- 需要考虑Confirm和Cancel的失败情况,做好幂等处理
事务悬挂和空回滚
1️⃣ 空回滚
当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚。
如图:

执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。
2️⃣ 业务悬挂
对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂。
执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂
TCC模式-代码操作 如果想看代码,点击这里
总结
课堂作业
面试题:TCC 模式的每个阶段是做什么的?🎤
- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放
TCC 的优点是什么?🎤
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 相比 AT 模型,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC 的缺点是什么?🎤
- 有代码侵入,需要人为编写 try、Confirm 和 Cancel 接口,太麻烦
- 软状态,事务是最终一致
- 需要考虑 Confirm 和 Cancel 的失败情况,做好幂等处理、事务悬挂和空回滚处理
TCC 模式与 AT 模式对比,有哪些优缺点

2.注册中心
本章主要学习 Nacos 中的一些特性和原理,以及与 Eureka 的功能对比。
1.1 Eureka
Eureka
Eureka 是 Netflix 公司开源的一个服务注册中心组件,早期版本的 SpringCloud 都是使用 Eureka 作为注册中心。由于 Eureka 和 Nacos 的 starter 中提供的功能都是基于 SpringCloudCommon 规范,因此两者使用起来差别不大。
课前资料中提供了一个 Eureka 的 demo:

也可以点击这里下载cloud-demo.zip初始工程 👈
我们可以用 idea 打开查看一下:

结构说明:
eureka-server
:Eureka 的服务端,也就是注册中心。没错,Eureka 服务端要自己创建项目order-service
:订单服务,是一个服务调用者,查询订单的时候要查询用户user-service
:用户服务,是一个服务提供者,对外暴露查询用户的接口
启动以后,访问 localhost:10086
即可查看到 Eureka 的控制台,相对于 Nacos 来说简陋了很多:


微服务引入 Eureka 的方式也极其简单,分两步:
- 引入
eureka-client
依赖 - 配置
eureka
地址
接下来就是编写 OpenFeign 的客户端了,怎么样?是不是跟 Nacos 用起来基本一致。

任务:将eureka注册中改成nacos注册中心
1)引入依赖
- 停止eureka服务
- 添加依赖

<!-- nacos注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2个服务都要添加
2)配置nacos地址 在user-service和order-service的application.yml中添加nacos地址:
spring:
cloud:
nacos:
server-addr: nacos的ip:8848
3)重启 重启微服务后,登录nacos管理页面,可以看到微服务信息:

发现也能调用,feign代码没有任何修改:

Eureka 和 Nacos 对比
Eureka 和 Nacos 都能起到注册中心的作用,用法基本类似。但还是有一些区别的,例如:
- Nacos 支持配置管理,而 Eureka 则不支持。
而且服务注册发现上也有区别,我们来做一个实验:
我们停止 user-service
服务,然后观察 Eureka 控制台,你会发现很长一段时间过去后,Eureka 服务依然没有察觉 user-service
的异常状态。

这与 Eureka 的健康检测机制有关。在 Eureka 中,健康检测的原理如下:
- 微服务启动时注册信息到 Eureka,这点与 Nacos 一致。
- 微服务每隔 30 秒向 Eureka 发送心跳请求,报告自己的健康状态。Nacos 中默认是 5 秒一次。
- Eureka 如果 90 秒未收到心跳,则认为服务疑似故障,可能被剔除。Nacos 中则是 15 秒超时,30 秒剔除。
- Eureka 如果发现超过 85% 比例的服务都心跳异常,会认为是自己的网络异常,暂停剔除服务的功能。
- Eureka 每隔 60 秒执行一次服务检测和清理任务;Nacos 是每隔 5 秒执行一次。
综上,你会发现 Eureka 是尽量不剔除服务,避免“误杀”,宁可放过一千,也不错杀一个 。这就导致当服务真的出现故障时,迟迟不会被剔除,给服务的调用者带来困扰。
不仅如此,当 Eureka 发现服务宕机并从服务列表中剔除以后,并不会将服务列表的变更消息推送给所有微服务。而是等待微服务自己来拉取时发现服务列表的变化。而微服务每隔 30 秒才会去 Eureka 更新一次服务列表,进一步推迟了服务宕机时被发现的时间。
而 Nacos 中微服务除了自己定时去 Nacos 中拉取服务列表以外,Nacos 还会在服务列表变更时主动推送最新
的服务列表给所有的订阅者。
综上,Eureka 和 Nacos 的相似点有: 🎯
- 都支持服务注册发现功能
- 都有基于心跳的健康监测功能
- 都支持集群,集群间数据同步默认是 AP 模式,即最全高可用性
Eureka 和 Nacos 的区别有: 🎯
- Eureka 的心跳是 30 秒一次,Nacos 则是 5 秒一次
- Eureka 如果 90 秒未收到心跳,则认为服务疑似故障,可能被剔除。Nacos 中则是 15 秒超时,30 秒剔除。
- Eureka 每隔 60 秒执行一次服务检测和清理任务;Nacos 是每隔 5 秒执行一次。
- Nacos支持服务端
主动检测
提供者状态:临时实例采用心跳模式
,非临时实例采用主动检测模式
临时
实例心跳不正常会被剔除
,非临时
实例则不会被剔除
- Eureka 只能等微服务自己每隔 30 秒更新一次服务列表;Nacos 即有定时更新,也有在服务变更时的广播推送
- Eureka 仅有注册中心功能,而 Nacos 同时支持注册中心、配置管理
- Eureka 和 Nacos 都支持集群,而且默认都是 AP 模式
Nacos集群是持续可用的,也就是舍弃了CAP中的C,保证了AP
Nacos的服务实例分为两种类型: 🎯
临时实例
:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。非临时实例
:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。
配置一个服务实例为永久实例:
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为非临时实例

总结
课堂作业
- SpringCloud 有哪些常用组件?分别是什么作用? 🎤
- 服务注册发现的基本流程是怎样的?🎤
- Eureka 和 Nacos 有哪些区别?🎤
- Nacos的服务临时实例和非临时实例有什么区别?🎤
1.2.服务分级存储模型
服务分级存储模型
一个服务可以有多个实例,例如我们的user-service,可以有:
- 127.0.0.1:8081
- 127.0.0.1:8082
- 127.0.0.1:8083
假如这些实例分布于全国各地的不同机房,例如:
- 127.0.0.1:8081,在上海机房
- 127.0.0.1:8082,在上海机房
- 127.0.0.1:8083,在杭州机房
Nacos就将同一机房内的实例 划分为一个集群。
容灾系统是指在相隔较远的异地,建立两套或多套功能相同的IT系统,互相之间可以进行健康状态监视和功能切换,当一处系统因意外(如火灾、地震等)停止工作时,整个应用系统可以切换到另一处,使得该系统功能可以继续正常工作。容灾技术是系统的高可用性技术
也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:

微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。例如:

杭州机房内的order-service应该优先访问同机房的user-service。 👈
修改user-service的application.yml文件,添加集群配置:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: CS # 集群名称
重启两个user-service实例后,我们可以在nacos控制台看到下面结果:

我们再次复制一个user-service启动配置,添加属性:
-Dserver.port=8083 -Dspring.cloud.nacos.discovery.cluster-name=SH
配置如图所示:

启动UserApplication3后再次查看nacos控制台:

如果停掉一个就会出现不可用现象:

默认的ZoneAvoidanceRule
并不能实现根据同集群优先来实现负载均衡。
1️⃣ 1)给order-service配置集群信息
修改order-service
的application.yml文件,添加集群配置:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: CS # 集群名称

访问:http://localhost:8088/order/101 连接,观察
日志输出情况:
查看源码流程:点击查看debug源码截图👈
- ZoneAvoidanceRule-->ZoneAwareLoadBalancer--->chooseServer()方法
ZoneAwareLoadBalancer类主要是跟Zones时区有关的,提供了跟zone有关的方法。但是在国内基本上用不上。
- eureka提供了region和zone两个概念来进行分区,这两个概念均来自于亚马逊的AWS:
- region:可以简单理解为地理上的分区,比如亚洲地区,或者华北地区,再或者北京等等,没有具体大小的限制。根据项目具体的情况,可以自行合理划分region。
- zone:可以简单理解为region内的具体机房,比如说region划分为北京,然后北京有两个机房,就可以在此region之下划分出zone1,zone2两个zone。
2️⃣ 2)修改负载均衡规则
默认的ZoneAvoidanceRule
并不能实现根据同集群优先来实现负载均衡 .
Nacos中提供了一个NacosRule
的实现,可以优先从同集群中挑选实例。 👈
修改order-service
的OrderApplication类中,修改负载均衡规则阴影部分:
@EnableFeignClients
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
@Bean
public Logger.Level feignLevel(){
return Logger.Level.BASIC;
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Bean
public IRule randomRule(){
return new NacosRule();
}
}


NacosRule的算法核心 ExtendBalancer.getHostByRandomWeight2(instancesToChoose);---->随机效果
课堂作业
- Nacos 的分级存储模型是什么意思?🎤
- NacosRule有什么特点?
1.2.权重配置和环境隔离
权重配置和环境隔离
权重配置
实际部署中会出现这样的场景:
服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。
但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。
因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。
在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:

在弹出的编辑窗口,修改权重:
可以发现,改成0.1的实例被访问的次数剧减!
实际开发中,如果要升级,但是往常要停机,但是现在可以设置权重为0 ,然后升级
注意:如果权重修改为0,则该实例永远不会被访问 ⚠️
1.3.环境隔离
环境隔离
Nacos提供了namespace来实现环境隔离功能。
- nacos中可以有多个namespace
- namespace下可以有group、service等
- 不同namespace之间相互隔离,例如不同namespace的服务互相不可见

5.5.1.创建namespace
默认情况下,所有service、data、group都在同一个namespace,名为public:

我们可以点击页面新增按钮,添加一个namespace:

然后,填写表单:

就能在页面看到一个新的namespace:

5.5.2.给微服务配置namespace
给微服务配置namespace只能通过修改配置来实现。
例如,修改order-service的application.yml文件:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填ID
重启order-service后,访问控制台,可以看到下面的结果:


此时访问order-service,因为namespace不同,会导致找不到userservice,控制台会报错:

3.远程调用
我们知道微服务间远程调用都是有 OpenFeign 帮我们完成的,甚至帮我们实现了服务列表之间的负载均衡。但具体负载均衡的规则是什么呢?何时做的负载均衡呢?
接下来我们一起来分析一下。
3.1.负载均衡原理
负载均衡原理
在 SpringCloud 的早期版本中,负载均衡都是有 Netflix 公司开源的 Ribbon 组件来实现的,甚至 Ribbon 被直接集成到了 Eureka-client 和 Nacos-Discovery 中。
但是自 SpringCloud2020 版本开始,已经弃用 Ribbon,改用 Spring 自己开源的 Spring Cloud LoadBalancer 了,我们使用的 OpenFeign 的也已经与其整合。
接下来我们就通过源码分析,来看看 OpenFeign 底层是如何实现负载均衡功能的。
3.1.1.源码跟踪
要弄清楚 OpenFeign 的负载均衡原理,最佳的办法肯定是从 FeignClient 的请求流程入手。
首先,我们在 com.hmall.cart.service.impl.CartServiceImpl
中的 queryMyCarts
方法中打一个断点。然后在 swagger 页面请求购物车列表接口。
进入断点后,观察 ItemClient
这个接口:

你会发现 ItemClient 是一个代理对象,而代理的处理器则是 SentinelInvocationHandler
。这是因为我们项目中引入了 Sentinel
导致。
我们进入 SentinelInvocationHandler
类中的 invoke
方法看看:

可以看到这里是先获取被代理的方法的处理器 MethodHandler
,接着,Sentinel 就会开启对簇点资源的监控:

开启 Sentinel 的簇点资源监控后,就可以调用处理器了,我们尝试跟入,会发现有两种实现:

这其实就是 OpenFeign 远程调用的处理器了。继续跟入会进入 SynchronousMethodHandler
这个实现类:

在上述方法中,会循环尝试调用 executeAndDecode()
方法,直到成功或者是重试次数达到 Retryer 中配置的上限。
我们继续跟入 executeAndDecode()
方法:

executeAndDecode()
方法最终会利用 client
去调用 execute()
方法,发起远程调用。
这里的 client 的类型是 feign.Client
接口,其下有很多实现类:

由于我们项目中整合了 seata,所以这里 client 对象的类型是 SeataFeignBlockingLoadBalancerClient
,内部实现如下:

这里直接调用了其父类,也就是 FeignBlockingLoadBalancerClient
的 execute
方法,来看一下:

整段代码中核心的有 4 步:
- 从请求的
URI
中找出serviceId
- 利用
loadBalancerClient
,根据serviceId
做负载均衡,选出一个实例ServiceInstance
- 用选中的
ServiceInstance
的ip
和port
替代serviceId
,重构URI
- 向真正的 URI 发送请求
所以负载均衡的关键就是这里的 loadBalancerClient,类型是 org.springframework.cloud.client.loadbalancer.LoadBalancerClient
,这是 Spring-Cloud-Common
模块中定义的接口,只有一个实现类:

而这里的 org.springframework.cloud.client.loadbalancer.BlockingLoadBalancerClient
正是 Spring-Cloud-LoadBalancer
模块下的一个类:

我们继续跟入其 BlockingLoadBalancerClient#choose()
方法:

图中代码的核心逻辑如下:
- 根据 serviceId 找到这个服务采用的负载均衡器(
ReactiveLoadBalancer
),也就是说我们可以给每个服务配不同的负载均衡算法。 - 利用负载均衡器(
ReactiveLoadBalancer
)中的负载均衡算法,选出一个服务实例
ReactiveLoadBalancer
是 Spring-Cloud-Common
组件中定义的负载均衡器接口规范,而 Spring-Cloud-Loadbalancer
组件给出了两个实现:

默认的实现是 RoundRobinLoadBalancer
,即轮询负载均衡器。负载均衡器的核心逻辑如下:

核心流程就是两步:
- 利用
ServiceInstanceListSupplier#get()
方法拉取服务的实例列表,这一步是采用响应式编程 - 利用本类,也就是
RoundRobinLoadBalancer
的getInstanceResponse()
方法挑选一个实例,这里采用了轮询算法来挑选。
这里的 ServiceInstanceListSupplier 有很多实现:

其中 CachingServiceInstanceListSupplier 采用了装饰模式,加了服务实例列表缓存,避免每次都要去注册中心拉取服务实例列表。而其内部是基于 DiscoveryClientServiceInstanceListSupplier
来实现的。
在这个类的构造函数中,就会异步的基于 DiscoveryClient 去拉取服务的实例列表:

3.1.2.流程梳理
根据之前的分析,我们会发现 Spring 在整合 OpenFeign 的时候,实现了 org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient
类,其中定义了 OpenFeign 发起远程调用的核心流程。也就是四步:
- 获取请求中的
serviceId
- 根据
serviceId
负载均衡,找出一个可用的服务实例 - 利用服务实例的
ip
和port
信息重构 url - 向真正的 url 发起请求
而具体的负载均衡则是不是由 OpenFeign
组件负责。而是分成了负载均衡的接口规范,以及负载均衡的具体实现两部分。
负载均衡的接口规范是定义在 Spring-Cloud-Common
模块中,包含下面的接口:
LoadBalancerClient
:负载均衡客户端,职责是根据 serviceId 最终负载均衡,选出一个服务实例ReactiveLoadBalancer
:负载均衡器,负责具体的负载均衡算法
OpenFeign 的负载均衡是基于 Spring-Cloud-Common
模块中的负载均衡规则接口,并没有写死具体实现。这就意味着以后还可以拓展其它各种负载均衡的实现。
不过目前 SpringCloud
中只有 Spring-Cloud-Loadbalancer
这一种实现。
Spring-Cloud-Loadbalancer
模块中,实现了 Spring-Cloud-Common
模块的相关接口,具体如下:
BlockingLoadBalancerClient
:实现了LoadBalancerClient
,会根据 serviceId 选出负载均衡器并调用其算法实现负载均衡。RoundRobinLoadBalancer
:基于轮询算法实现了ReactiveLoadBalancer
RandomLoadBalancer
:基于随机算法实现了ReactiveLoadBalancer
,
这样一来,整体思路就非常清楚了,流程图如下:

总结
课堂作业
- Ribbon 和 SpringCloudLoadBalancer 有什么差异?🎤
4.服务保护
服务保护
在 SpringCloud 的早期版本中采用的服务保护技术叫做 Hystix
,不过后来被淘汰,替换为 Spring Cloud Circuit Breaker
,其底层实现可以是 Spring Retry
和 Resilience4J
。
不过在国内使用较多还是 SpringCloudAlibaba
中的 Sentinel
组件。
接下来,我们就分析一下 Sentinel
组件的一些基本实现原理以及它与 Hystix
的差异。
4.1.线程隔离
首先我们来看下线程隔离功能,无论是 Hystix 还是 Sentinel 都支持线程隔离。不过其实现方式不同。
线程隔离有两种方式实现:
- 线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
- 信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求
如图:

两者的优缺点如下:

Sentinel 的线程隔离就是基于信号量隔离实现的,而 Hystix 两种都支持,但默认是基于线程池隔离。
4.2.滑动窗口算法
在熔断功能中,需要统计异常请求或慢请求比例,也就是计数。在限流的时候,要统计每秒钟的 QPS,同样是计数。可见计数算法在熔断限流中的应用非常多。sentinel 中采用的计数器算法就是滑动窗口计数算法。
4.2.1.固定窗口计数
要了解滑动窗口计数算法,我们必须先知道固定窗口计数算法,其基本原理如图:

说明:
- 将时间划分为多个窗口,窗口时间跨度称为
Interval
,本例中为 1000ms; - 每个窗口维护 1 个计数器,每有 1 次请求就将计数器
+1
。限流就是设置计数器阈值,本例为 3,图中红线标记 - 如果计数器超过了限流阈值,则超出阈值的请求都被丢弃。
示例:

说明:
- 第 1、2 秒,请求数量都小于 3,没问题
- 第 3 秒,请求数量为 5,超过阈值,超出的请求被拒绝
但是我们考虑一种特殊场景,如图:

说明:
- 假如在第 5、6 秒,请求数量都为 3,没有超过阈值,全部放行
- 但是,如果第 5 秒的三次请求都是在 4.5~5 秒之间进来;第 6 秒的请求是在 5~5.5 之间进来。那么从第 4.5~5.之间就有 6 次请求!也就是说每秒的 QPS 达到了 6,远超阈值。
这就是固定窗口计数算法的问题,它只能统计当前某 1 个时间窗的请求数量是否到达阈值,无法结合前后的时间窗的数据做综合统计。
因此,我们就需要滑动时间窗口算法来解决。
4.2.2.滑动窗口计数
固定时间窗口算法中窗口有很多,其跨度和位置是与时间区间绑定,因此是很多固定不动的窗口。而滑动时间窗口算法中只包含 1 个固定跨度的窗口,但窗口是可移动动的,与时间区间无关。
具体规则如下:
- 窗口时间跨度
Interval
大小固定,例如 1 秒 - 时间区间跨度为
Interval / n
,例如 n=2,则时间区间跨度为 500ms - 窗口会随着当前请求所在时间
currentTime
移动,窗口范围从currentTime-Interval
时刻之后的第一个时区开始,到currentTime
所在时区结束。
如图所示:

限流阈值依然为 3,绿色小块就是请求,上面的数字是其 currentTime
值。
- 在第 1300ms 时接收到一个请求,其所在时区就是 1000~1500
- 按照规则,currentTime-Interval 值为 300ms,300ms 之后的第一个时区是 5001000,因此窗口范围包含两个时区:5001000、1000~1500,也就是粉红色方框部分
- 统计窗口内的请求总数,发现是 3,未达到上限。
若第 1400ms 又来一个请求,会落在 1000~1500 时区,虽然该时区请求总数是 3,但滑动窗口内总数已经达到 4,因此该请求会被拒绝:

假如第 1600ms 又来的一个请求,处于 1500~2000 时区,根据算法,滑动窗口位置应该是 1000~1500 和 1500~2000 这两个时区,也就是向后移动:

这就是滑动窗口计数的原理,解决了我们之前所说的问题。而且滑动窗口内划分的时区越多,这种统计就越准确。
4.3.令牌桶算法
限流的另一种常见算法是令牌桶算法。Sentinel 中的热点参数限流正是基于令牌桶算法实现的。其基本思路如图:

说明:
- 以固定的速率生成令牌,存入令牌桶中,如果令牌桶满了以后,多余令牌丢弃
- 请求进入后,必须先尝试从桶中获取令牌,获取到令牌后才可以被处理
- 如果令牌桶中没有令牌,则请求等待或丢弃
基于令牌桶算法,每秒产生的令牌数量基本就是 QPS 上限。
当然也有例外情况,例如:
- 某一秒令牌桶中产生了很多令牌,达到令牌桶上限 N,缓存在令牌桶中,但是这一秒没有请求进入。
- 下一秒的前半秒涌入了超过 2N 个请求,之前缓存的令牌桶的令牌耗尽,同时这一秒又生成了 N 个令牌,于是总共放行了 2N 个请求。超出了我们设定的 QPS 阈值。
因此,在使用令牌桶算法时,尽量不要将令牌上限设定到服务能承受的 QPS 上限。而是预留一定的波动空间,这样我们才能应对突发流量。
4.4.漏桶算法
漏桶算法与令牌桶相似,但在设计上更适合应对并发波动较大的场景,以解决令牌桶中的问题。
简单来说就是请求到达后不是直接处理,而是先放入一个队列。而后以固定的速率从队列中取出并处理请求。之所以叫漏桶算法,就是把请求看做水,队列看做是一个漏了的桶。
如图:

说明:
- 将每个请求视作"水滴"放入"漏桶"进行存储;
- "漏桶"以固定速率向外"漏"出请求来执行,如果"漏桶"空了则停止"漏水”;
- 如果"漏桶"满了则多余的"水滴"会被直接丢弃。
漏桶的优势就是流量整型,桶就像是一个大坝,请求就是水。并发量不断波动,就如图水流时大时小,但都会被大坝拦住。而后大坝按照固定的速度放水,避免下游被洪水淹没。
因此,不管并发量如何波动,经过漏桶处理后的请求一定是相对平滑的曲线:

sentinel 中的限流中的排队等待功能正是基于漏桶算法实现的。
5.作业
尝试用自己的语言回答下列面试题:
- SpringCloud 有哪些常用组件?分别是什么作用?
- 服务注册发现的基本流程是怎样的?
- Eureka 和 Nacos 有哪些区别?
- Nacos 的分级存储模型是什么意思?
- OpenFeign 是如何实现负载均衡的?
- 什么是服务雪崩,常见的解决方案有哪些?
- Hystix 和 Sentinel 有什么区别和联系?
- 限流的常见算法有哪些?
- 什么是 CAP 理论和 BASE 思想?
- 项目中碰到过分布式事务问题吗?怎么解决的?
- AT 模式如何解决脏读和脏写问题的?
- TCC 模式与 AT 模式对比,有哪些优缺点
- RabbitMQ 是如何确保消息的可靠性的?
- RabbitMQ 是如何解决消息堆积问题的?
实战
经过一段时间的学习,是不是感觉很充实?但是心理感觉有点不确定,到底有没有掌握微服务开发技能,不用怀疑和恐慌,接下来用心用力借力,将实战任务完成就证明自己了 加油!!!!!!