微服务面试基础
——————↓↓↓↓↓以下为面试篇↓↓↓↓↓↓——————
面试篇:分布式事务定义、CAP定理、BASE理论、AT模式流程与脏写问题
要清晰讲解分布式事务相关内容,我们可以从分布式事务定义、CAP定理、BASE理论、AT模式流程与脏写问题这几个核心部分逐步展开:
一、分布式事务的定义
分布式事务指:事务操作跨越多个服务或多个数据库的场景,而非单一服务/数据库内的事务。常见场景包括:
跨数据源的分布式事务(操作多个不同数据库);
跨服务的分布式事务(微服务调用中,多个服务的数据库操作需统一事务);
以上两种的综合情况。
二、CAP定理:分布式系统的“三角取舍”
1998年,计算机科学家Eric Brewer提出CAP定理:分布式系统有三个核心指标,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance),但三者无法同时满足。
1. 三个指标的含义
一致性(Consistency):用户访问分布式系统的任意节点,获取的数据必须一致。
比如有两个节点node01
和node02
,初始数据都是data:v0
。若node01
的数据被改为v1
,必须同步到node02
,才能保证用户访问任意节点都得到v1
。
可用性(Availability):用户访问分布式系统时,读、写操作总能成功。
若系统“只能读不能写”“只能写不能读”,甚至两者都不可用,就是“弱可用”或“不可用”。
分区容错性(Partition tolerance):
Partition(分区):分布式节点间因网络故障无法通信,形成“网络分区”(如
node01/node02
能通信,但与node03
断开,node03
成为独立分区)。Tolerance(容错):即使出现网络分区,系统仍要持续对外提供服务。
2. 为什么三者不可兼得?
分布式系统中,网络故障(分区)是必然会发生的(网络无法100%可靠)。因此,分区容错性(P)是分布式系统的“硬性要求”(必须能应对分区,持续服务)。
此时,只能在一致性(C)和可用性(A)中二选一:
选“可用性(A)”:允许用户任意读写,保证服务能访问。但分区存在时,部分节点数据无法同步(如
node03
无法同步node01
的新数据),会出现数据不一致。这种情况满足 AP(Availability + Partition tolerance,牺牲一致性)。选“一致性(C)”:出现分区时,禁止写操作(只能读),直到网络恢复、分区消失,保证数据一致。但此时写操作不可用,满足 CP(Consistency + Partition tolerance,牺牲可用性)。
三、BASE理论:“退而求其次”的最终一致
CAP定理要求在“C”和“A”之间取舍,但如果牺牲了一致性,数据临时不一致怎么办?BASE理论给出了思路:不追求“强一致性”(任何时刻都一致),而是追求“最终一致性”。
BASE包含三个要点:
基本可用(Basically Available):系统故障时,允许损失部分可用性,但核心功能必须可用。
比如电商系统库存服务故障时,“下单”核心流程仍能执行,只是库存显示可能延迟。
软状态(Soft State):允许系统在一定时间内存在中间状态(临时不一致)。
比如数据同步过程中,节点间暂时存在数据差异,但这个“不一致”是临时的。
最终一致性(Eventually Consistent):虽然无法保证“强一致性”,但经过“软状态”后,最终能达到数据一致。
基于BASE理论,解决分布式事务有两个核心思路:
AP思想:各子事务分别执行、提交,不锁定数据,允许临时不一致;之后通过“弥补措施”(如回滚、补偿)实现最终一致。Seata的AT模式就是这种思路。
CP思想:各子事务执行后不提交,等所有子事务结果;之后“同时提交”或“同时回滚”。过程中锁定资源(不允许其他操作),数据暂时不可用,但保证一致性。XA模式属于这种思路。
四、Seata AT模式:流程与“脏写”问题
Seata的AT模式是AP思想的典型实现,分两个阶段处理分布式事务,同时要解决“极端情况下的脏写问题”。
1. AT模式的两阶段流程
AT模式涉及三个角色:
TM(事务管理器):发起并管理全局事务。
RM(资源管理器):管理各数据源的分支事务(执行SQL、记录快照等)。
TC(事务协调器):协调全局事务的提交/回滚,管理全局锁。
(1)第一阶段:记录快照,执行并提交分支事务
流程如下(结合第三张图):
TM开启全局事务。
TM调用各分支事务(如跨服务的数据库操作)。
RM执行分支事务的SQL,并提交本地事务(此时本地数据已修改,系统保持“可用”);同时记录数据更新前的快照(undo log)(用于后续回滚)。
RM向TC注册分支事务,并报告事务状态(成功/失败)。
(2)第二阶段:根据全局结果,提交或回滚
流程如下(结合第四张图):
若所有分支事务都成功:TC通知各RM,删除第一阶段的undo log(因为本地事务已提交,无需回滚)。
若任意分支事务失败:TC通知各RM,根据undo log回滚数据(恢复到更新前的状态),然后删除undo log。
2. 脏写问题:极端并发下的隐患
大多数情况AT模式没问题,但多线程并发操作同一份数据时,可能出现“脏写”(覆盖合法更新)。
举个例子(结合第五张图):
现有account
表,id=1
的money
初始为100,事务1和事务2都要修改它:
事务1流程:
获取数据库锁(DB锁),保存数据快照(
{"id":1, "money":100}
)。执行业务SQL:
money = money - 10
(数据库中money
变为90)。提交本地事务,释放DB锁(此时其他事务可获取DB锁)。
事务2流程:
事务1释放DB锁后,事务2获取DB锁,保存快照(此时数据库中
money
是90,所以快照为{"id":1, "money":90}
)。执行业务SQL:
money = money - 10
(数据库中money
变为80)。提交本地事务,释放DB锁。
事务1的回滚(假设需要回滚):
事务1要根据最初的快照(money:100)回滚数据,但此时数据库中money
已被事务2改成80。若强行回滚到100,会覆盖事务2的合法更新,这就是脏写。
3. 脏写的解决:全局锁
为解决脏写,AT模式引入全局锁(由TC管理),规则是:事务在释放DB锁之前,必须先获取全局锁(保证同一时间只有一个事务能操作同一份数据)。
流程如下(结合第六张图):
事务1流程:
获取DB锁,保存快照(
{"id":1, "money":100}
)。获取全局锁(TC记录:“事务1正在操作
account
表id=1
的数据”)。执行业务SQL(
money
变90)。提交本地事务,释放DB锁(但全局锁仍由事务1持有)。
若事务1需要回滚,再次获取DB锁,根据快照(100)回滚。
事务2流程:
事务1持有全局锁时,事务2尝试获取全局锁(操作同一份数据)会失败。此时事务2会重试(默认30次,间隔10毫秒);若超时仍未获取,事务2会回滚并释放DB锁,不会修改数据。
这样,事务2无法在事务1的全局锁释放前修改数据,避免了“回滚覆盖合法更新”的脏写问题。
在 Seata AT 模式中,全局锁的释放时机与全局事务的最终状态强绑定,而事务 1 的回滚操作只会在全局锁释放之前执行。这一设计从根本上避免了 “事务 1 释放全局锁后,事务 2 修改数据,事务 1 再回滚覆盖” 的问题。
事务 1 释放全局锁的时机(总结:所有分支事务成功通过后,必然要删除undolog日志,必然无法回滚了,所以全局锁也就释放了,此时事务2可以大胆的去操作数据,因为事务1必定不会回滚。)
全局锁由事务协调器(TC)管理,其核心作用是在全局事务未结束前,锁定被操作的数据,防止其他事务修改。事务 1 释放全局锁的时机只有两种:
全局事务成功提交(第二阶段 “提交”)
若所有分支事务都成功,TM 通知 TC “全局事务提交”。
TC 会向所有 RM 发送 “删除 undo log” 的指令(因为分支事务已提交,无需回滚)。
RM 删除 undo log 后,向 TC 确认,TC 才会释放事务 1 持有的全局锁。
此时,全局事务已彻底结束,事务 1 不会再有任何回滚操作(因为 “提交” 是最终状态)。
全局事务回滚完成(第二阶段 “回滚”)
若任意分支事务失败,TM 通知 TC “全局事务回滚”。
TC 向所有 RM 发送 “回滚” 指令,RM 执行以下操作:
重新获取该数据的数据库锁(DB 锁)(防止此时其他事务修改);
根据第一阶段记录的 undo log,将数据恢复到更新前的状态(比如从 90 回滚到 100);
回滚完成后,删除 undo log,并向 TC 确认;
TC 收到所有 RM 的回滚确认后,释放事务 1 持有的全局锁。
此时,全局事务已通过回滚恢复一致,事务 1 的回滚操作已完成,后续不会再触发回滚。
总结
从“分布式事务的场景”出发,通过CAP定理理解分布式系统“C/A二选一”的核心矛盾;再通过BASE理论明确“最终一致”的取舍思路;最后聚焦Seata AT模式,了解其“两阶段提交+快照回滚”的流程,以及极端情况下“脏写问题”的成因与“全局锁”的解决方案。整个逻辑围绕“分布式系统如何在一致性与可用性之间取舍,并保证事务最终正确”展开。
面试篇:TCC模式
要清晰讲解 TCC模式,我们可以从核心方法、流程示例、异常问题、优缺点这几个维度逐步展开:
一、TCC的核心:三个手动编码的方法
TCC模式是分布式事务的一种实现,通过人工编码完成“资源预留→确认/回滚”,包含三个关键方法:
Try:检测并预留资源。比如“扣钱”场景中,先检查余额是否充足,充足则“冻结”要扣的金额(预留资源,不直接扣减可用余额)。
Confirm:完成资源操作的提交。只有所有分支的
Try
都成功,才会执行Confirm
,把“预留的资源”真正提交(比如把冻结的金额扣掉,完成交易)。要求:只要Try
成功,Confirm
必须能成功(逻辑要足够可靠)。Cancel:释放预留的资源(
Try
的反向操作)。若任意分支Try
失败,执行Cancel
,把“预留的资源”释放回去(比如把冻结的金额解冻,恢复可用余额)。
二、TCC流程示例:扣减用户余额
假设账户A初始余额为 100元
,需要扣减 30元
,分三个阶段分析:
阶段1:Try(资源检测与预留)
操作:检查余额是否≥30元(检测);若充足,执行
冻结金额+30
、可用金额-30
。结果:总金额(冻结+可用)仍为
100
(30+70
),但资源已被“预留”(冻结的30元是后续扣减的准备)。特点:
Try
阶段的本地事务直接提交,无需等待其他分支事务,能快速释放数据库资源(性能优势)。
阶段2:Confirm(确认提交)
触发条件:所有分支的
Try
都成功,全局事务需要“提交”。操作:因为
Try
已扣减可用金额、增加冻结金额,所以Confirm
只需执行冻结金额-30
(把冻结的30元“真正扣掉”)。结果:冻结金额
0
,可用金额70
,总金额70
(扣减完成)。
阶段3:Cancel(释放预留资源)
触发条件:任意分支的
Try
失败,全局事务需要“回滚”。操作:执行
Try
的反向操作——冻结金额-30
、可用金额+30
(把冻结的30元“解冻”,恢复可用余额)。结果:冻结金额
0
,可用金额100
,总金额回到初始状态(回滚成功)。
三、TCC的异常问题:事务悬挂与空回滚
结合讲义中的“分支阻塞”场景(一个分布式事务包含两个分支,Try
阶段一个成功、一个阻塞),分析两类典型问题:
场景背景
分布式事务有两个分支,TM(事务管理器)调用分支时:
第一个分支的
Try
成功执行;第二个分支的
Try
长时间阻塞(如网络延迟、资源锁等待)。
1. 空回滚
触发:第二个分支的
Try
阻塞时间过长,导致全局事务超时。此时TC(事务协调器)会触发二阶段的Cancel操作,要求两个分支都执行Cancel
。问题:第二个分支的
Try
根本没执行(还在阻塞),但现在要执行它的Cancel
。如果直接执行Cancel
的“释放预留资源”逻辑,会导致数据错误(因为本来就没预留资源,却要“释放”,相当于“无中生有”地回滚)。处理:在
Cancel
方法中,先判断“当前分支是否执行过Try
”。如果没执行过Try
,则空回滚(不做任何操作),避免数据错误。
2. 事务悬挂
触发:空回滚后,那个“阻塞的
Try
”最终执行完成了(但此时全局事务已经因为超时而结束,执行了Cancel
)。问题:这个
Try
执行的是“预留资源”的操作,但全局事务已经结束(不会再执行Confirm
或Cancel
),导致这个分支的事务“只做了一半”,处于悬挂状态(资源被预留,但没人处理后续的提交/回滚)。处理:在
Try
方法中,先判断“全局事务是否已经结束(比如是否已执行过Cancel
)”。如果全局事务已结束,拒绝执行Try
(避免预留资源后无人处理)。
四、TCC的优缺点总结
优点
性能优秀:一阶段
Try
执行后直接提交本地事务,释放数据库资源,无需像AT模式那样生成数据快照、持有全局锁,性能比AT更强。兼容性广:不依赖数据库事务,靠人工编码的
Confirm/Cancel
补偿操作实现,因此可用于非事务型数据库(如部分NoSQL数据库)。
缺点
代码侵入性强:需要手动编写
Try
、Confirm
、Cancel
三个接口,开发工作量大,对业务代码侵入严重。最终一致性(软状态):事务需经过多阶段,中间是“软状态”(如
Try
后资源预留但未最终提交),只能保证最终一致性,而非强一致性。异常处理复杂:需自行实现幂等性(防止重复操作出错)、事务悬挂、空回滚等问题,开发难度和复杂度高。
通过“核心方法→流程示例→异常问题→优缺点”的逻辑,能清晰理解TCC模式的设计思路与 trade-off(取舍)。
面试篇:Nacos的环境隔离(Namespace)与分级模型(Cluster)
要清晰讲解Nacos的环境隔离(Namespace)与分级模型(Cluster),我们可以从背景需求→功能配置→效果验证→内部逻辑逐步展开:
一、环境隔离:Namespace的作用与实践
企业开发中,存在多环境(开发、测试、生产)或多项目共享Nacos集群的场景。此时需要隔离不同环境/项目的服务注册发现和配置管理,避免互相干扰。
Nacos通过 Namespace
实现环境隔离,隔离层次如图(最外层 Namespace
→ 中间 Group
→ 内层 Service/DataId
):
1. 创建新的Namespace(开发环境dev
)
Nacos默认有一个
public
命名空间,所有服务/配置默认归属它。新建命名空间时,填写表单(如第二张图):
命名空间名:
dev
(代表“开发环境”)。描述:
开发环境
。命名空间ID:不填则自动生成(后续配置需用此ID)。
创建后(第三张图),Nacos会生成唯一ID(如
8c468c63-b650-48da-a632-311c75e6d235
),且dev
命名空间下初始无任何配置(因为之前的配置都在public
下)。
2. 微服务指定Namespace
默认微服务注册到public
,若要让服务归属dev
命名空间,需在配置中指定namespace
(用生成的ID)。
以item-service
为例,修改bootstrap.yml
:
spring: cloud: nacos: discovery: # 服务发现配置 namespace: 8c468c63-b650-48da-a632-311c75e6d235 # 填写dev命名空间的ID
启动item-service
后,查看Nacos服务列表(第五张图):
dev
命名空间下会出现item-service
。public
命名空间下的其他服务(如user-
service
、cart-service
)仍在原位置——证明不同Namespace的服务相互隔离。
3. 验证Namespace的隔离效果
用cart-service
(在public
命名空间)调用item-service
(在dev
命名空间)测试:
访问
cart-service
的Swagger(http://localhost:8082/doc.html
),查询购物车列表(第六张图):
结果中商品的newPrice
为null
——因为跨命名空间的服务调用失败。
查看
cart-service
日志(第七张图):
日志显示No servers available for service: item-service
——Nacos在public
命名空间找不到item-service
的实例,证明Namespace彻底隔离了服务发现。
若将所有服务的namespace
统一(如都配置为dev
),调用会恢复正常。
二、分级模型:Cluster的作用与实践
大型应用中,服务实例可能分布在不同机房(如上海、杭州机房)。跨机房调用会产生网络延迟,因此需要按“机房”对实例分组管理——Nacos用Cluster
(集群)实现这一需求,结构为:服务(Service) → 集群(Cluster) → 实例(Instance)
(第八张图)。
1. Nacos内部注册表结构
Nacos内部用多层Map存储服务实例,与分级模型一一对应(第九张图):
最外层:
Map<Namespace, Map<Group, Service>>
—— 按“命名空间→分组”组织服务。服务层:
Map<String, Cluster>
—— 服务下按“集群名”组织集群。集群层:
Set<Instance>
—— 集群下存储具体实例(IP、端口等)。
2. 配置服务的集群
默认所有服务的集群为default
(第十张图)。若要指定集群(如“北京机房”,集群名BJ
),修改bootstrap.yml
:
spring: cloud: nacos: discovery: cluster-name: BJ # 自定义集群名,代表“北京机房”
修改item-service
的配置后,启动新实例(如端口8084
),查看Nacos(第十一张图):
新实例(
8084
)归属BJ
集群。原有实例(
8081
、8083
)仍在DEFAULT
集群——证明同一服务的实例可按集群分组管理。
总结
Nacos通过 Namespace 实现环境/项目级的隔离(服务、配置互不干扰);通过 Cluster 实现机房级的分组(优化跨机房调用,提升性能)。这种“分级模型”让服务治理更灵活,既保障了不同环境的安全性,又能在大规模集群下精细化管理实例。
面试篇:Eureka
要清晰讲解Eureka及其与Nacos的对比,我们可以从Eureka的基础认知、使用方式、与Nacos的核心差异三个维度展开:
一、Eureka是什么?
Eureka是Netflix公司开源的服务注册中心组件,是早期Spring Cloud生态中核心的注册中心方案。它遵循Spring Cloud通用规范,因此和Nacos的使用流程有诸多相似性,但在“服务治理机制”和“功能范围”上存在明显差异。
二、Eureka的Demo与控制台
讲义中的Eureka Demo包含三类组件:
eureka-server:Eureka的服务端(注册中心)——注意:Eureka的注册中心需要自行创建项目搭建(不像Nacos是独立中间件,直接部署即可)。
order-service:订单服务——作为服务调用者(例如查询订单时,需调用用户服务)。
user-service:用户服务——作为服务提供者(对外暴露“查询用户”的接口)。
启动后,访问 localhost:10086
可打开Eureka控制台(如图):
界面展示“系统状态”(Environment、Data center等)、“注册的服务实例列表”(如
EUREKASERVER
、ORDERSERVICE
、USERSERVICE
及其实例状态)。对比Nacos控制台,Eureka界面更“简陋”,功能展示更简洁。
三、Eureka的使用方式
微服务接入Eureka的流程与Nacos类似,核心两步:
引入
eureka-client
依赖。配置文件中指定Eureka Server的地址。
编写服务调用客户端(如OpenFeign)——流程与Nacos几乎一致。
四、Eureka vs Nacos:核心差异
两者都能实现“服务注册发现”,但在服务治理机制和功能范围上有显著区别。
1. 服务注册发现的“及时性”差异(核心!)
服务注册发现的核心是“快速感知服务上下线,保证调用准确性”。Eureka和Nacos的机制设计不同,导致“故障感知速度”有明显差异。
心跳与健康检测周期:
维度
Eureka
Nacos
心跳间隔
微服务每 30秒 发一次心跳
微服务每 5秒 发一次心跳
疑似故障超时
90秒未收到心跳,认为“疑似故障”
15秒未收到心跳,认为“疑似故障”
服务清理周期
每 60秒 执行一次清理
每 30秒 剔除故障服务;每 5秒 执行检测
→ 结论:Nacos对“服务故障”的感知周期更短、更及时。
服务故障的“容错策略”:
Eureka为避免“误杀”(如网络抖动导致心跳丢失,但服务实际正常),采取保守策略:
即使长时间没收到心跳,也尽量不轻易剔除服务;
若发现“超过85%的服务心跳异常”,会认为是自身(Eureka Server)网络故障,直接暂停“剔除服务”功能——这会导致“真正故障的服务”长期不被剔除,调用者持续请求故障服务,报错风险高。
Nacos策略更“果断”,短周期检测+剔除,能更快隔离故障服务。
服务列表的更新方式:
当服务列表变化时(如服务上线/下线),微服务需拿到最新列表才能正确调用。
Eureka:被动拉取——服务列表变更后,Eureka Server不会主动通知微服务,需微服务每30秒主动去拉取最新列表。
→ 微服务感知“列表变化”的延迟很高。
Nacos:主动推送 + 定时拉取——
→ 微服务能更快拿到最新列表,调用更准确。
微服务会定时拉取(周期短);
Nacos Server在“服务列表变更时”,会主动推送给所有订阅的微服务。
2. 功能范围的差异
Nacos:是“注册中心 + 配置中心”的综合体——不仅能管理服务注册发现,还能统一管理微服务配置(如配置集中存储、动态刷新配置等)。
Eureka:只有“注册中心”功能,无“配置管理”能力。若需配置管理,需额外引入其他组件(如Spring Cloud Config)。
3. 相似点
都支持服务注册与发现(微服务可注册自身、发现其他服务)。
都有基于心跳的健康监测(通过心跳判断服务是否存活)。
都支持集群部署,且集群间数据同步默认采用AP模式(优先保证“可用性(Availability)”,允许临时数据不一致,符合分布式系统“分区容错性(Partition tolerance)”要求)。
总结
Eureka是早期Spring Cloud的经典注册中心,使用简单,但在“服务故障感知的及时性”和“功能丰富度”上不如Nacos:
故障感知:Eureka更“迟钝”,Nacos更“敏锐”;
功能范围:Nacos兼具“注册中心+配置中心”,更全能;
场景:当下微服务生态中,Nacos因更及时的治理机制和丰富功能,使用更广泛;Eureka多见于早期Spring Cloud项目。
面试篇:OpenFeign结合Spring Cloud LoadBalancer实现负载均衡
这张流程图展示了 OpenFeign结合Spring Cloud LoadBalancer实现负载均衡 的完整流程,可按“请求发起→负载均衡选实例→URI重构→发送真实请求”的顺序分层讲解:
一、核心组件与分层职责
流程涉及4类核心组件,各层分工明确:
FeignBlockingLoadBalancerClient
:OpenFeign的负载均衡客户端,是远程调用的“总协调者”。BlockingLoadBalancerClient
:Spring Cloud LoadBalancer提供的负载均衡客户端,负责“选择负载均衡器”。ReactiveLoadBalancer
(如RoundRobinLoadBalancer
):负载均衡器,负责“执行具体的负载均衡算法”。ServiceInstanceListSupplier
+ 注册中心(Nacos):负责“从注册中心拉取服务实例列表”。
二、流程分步解析(必看!!!)
以“调用item-service
的/items/10
接口”为例,流程如下:
(传来初始请求--总调用--选择负载均衡器--具体负载均衡器执行--查看注册中心调用选择后的服务实例--将可用服务实例IP+端口号 替换原初始请求--放行)
1. 步骤1:发起原始请求(FeignBlockingLoadBalancerClient
层)
初始请求:
http://item-service/items/10
(其中item-service
是服务名,而非具体IP)。FeignBlockingLoadBalancerClient
接收请求后,首先从请求中提取serviceId
(即item-service
)。
2. 步骤2:负载均衡选实例(BlockingLoadBalancerClient
层)
FeignBlockingLoadBalancerClient
调用 LoadBalancerClient#choose()
方法,进入 BlockingLoadBalancerClient
的逻辑:
获取负载均衡器:根据
serviceId
(item-service
),获取对应的ReactiveLoadBalancer
(默认是轮询算法的RoundRobinLoadBalancer
)。调用负载均衡算法:调用
ReactiveLoadBalancer#choose()
,进入具体负载均衡器的逻辑。
3. 步骤3:拉取实例并轮询选择(RoundRobinLoadBalancer
层)
RoundRobinLoadBalancer
执行以下操作:
拉取实例列表:通过
ServiceInstanceListSupplier#get()
,从注册中心(Nacos)拉取item-service
的所有实例。假设Nacos中item-service
注册了两个实例:localhost:8081
、localhost:8083
。轮询选实例:调用
getInstanceResponse()
,用轮询算法(“按顺序依次选择”)从实例列表中选一个。假设本次选中localhost:8081
。
4. 步骤4:URI重构与发送请求(FeignBlockingLoadBalancerClient
层)
URI重构:将原始请求的服务名(
item-service
)替换为选中实例的IP+端口,得到新URI:http://localhost:8081/items/10
。(初始请求:http://item-service/items/10
)发送真实请求:向重构后的URI发起HTTP请求,完成远程调用。
三、设计亮点:“解耦与可扩展”
整个流程的核心设计思路是“职责分层、组件解耦”:
OpenFeign只负责“远程调用的模板化”(封装请求、解析响应),不关心“负载均衡怎么选实例”。
Spring Cloud LoadBalancer定义“负载均衡的接口规范”(
ReactiveLoadBalancer
),并提供默认实现(轮询、随机等),方便替换算法(比如想改成“权重负载均衡”,只需自定义ReactiveLoadBalancer
实现类)。注册中心(Nacos)只负责“存储服务实例的元数据”,与负载均衡逻辑解耦。
这种设计让系统扩展性极强,各组件可独立升级、替换。
面试篇:负载均衡流程
要结合核心代码片段讲解OpenFeign + Spring Cloud LoadBalancer的负载均衡流程,我们可以按“请求入口→负载均衡选实例→拉取实例→轮询算法→URI重构”的顺序,逐段分析关键代码与流程的对应关系:
一、请求入口:FeignBlockingLoadBalancerClient#execute
(OpenFeign触发负载均衡的起点)(必看!!!)
当通过OpenFeign的客户端(如ItemClient
)发起远程调用时,请求会先进入FeignBlockingLoadBalancerClient
的execute
方法。这段代码是“负载均衡+远程调用”的总入口:
// FeignBlockingLoadBalancerClient.java @Override public Response execute(Request request, Request.Options options) throws IOException { // 1. 从请求URL中提取服务名(serviceId) URI originalUri = URI.create(request.url()); String serviceId = originalUri.getHost(); Assert.state(serviceId != null, "Request URI需包含有效服务名"); // 2. 负载均衡:选择一个服务实例 ServiceInstance instance = loadBalancerClient.choose(serviceId, buildRequest()); if (instance == null) { throw new IllegalStateException("没有可用实例:" + serviceId); } // 3. 重构URI:把服务名(item-service)替换为实例的IP+端口 URI reconstructedUri = loadBalancerClient.reconstructURI(instance, originalUri); Request newRequest = buildRequest(request, reconstructedUri); // 4. 发起真实请求 return delegate.execute(newRequest, options); }
代码作用:
提取
serviceId
(如item-service
);调用
loadBalancerClient.choose
触发负载均衡选实例;重构URI(如
http://item-service/items/10
→http://localhost:8081/items/10
);发起最终的HTTP请求。
二、负载均衡选实例:BlockingLoadBalancerClient#choose
(选择负载均衡器)
loadBalancerClient.choose
方法实际调用BlockingLoadBalancerClient
的choose
,负责获取“负载均衡器”并执行选择逻辑:
// BlockingLoadBalancerClient.java @Override public <T> ServiceInstance choose(String serviceId, Request<T> request) { // 1. 根据serviceId获取对应的负载均衡器(默认是RoundRobinLoadBalancer) ReactiveLoadBalancer<ServiceInstance> loadBalancer = loadBalancerClientFactory.getInstance(serviceId); if (loadBalancer == null) { return null; } // 2. 调用负载均衡器的choose方法,选实例(响应式编程,block转为同步) Response<ServiceInstance> loadBalancerResponse = Mono.from(loadBalancer.choose(request)).block(); return loadBalancerResponse != null ? loadBalancerResponse.getServer() : null; }
代码作用:
为每个服务(如
item-service
)分配专属的ReactiveLoadBalancer
(负载均衡器接口,默认实现是轮询算法的RoundRobinLoadBalancer
);调用负载均衡器的
choose
方法,执行“选实例”逻辑。
三、轮询选实例:RoundRobinLoadBalancer#choose
(实现轮询算法)
RoundRobinLoadBalancer
是Spring Cloud LoadBalancer提供的默认轮询负载均衡器,核心逻辑在choose
和getInstanceResponse
中:
// RoundRobinLoadBalancer.java @Override public Mono<Response<ServiceInstance>> choose(Request request) { // 1. 获取“服务实例列表提供者”(从注册中心拉取实例) ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider .getInstance(response -> new RequestDataContext(new RequestData(request), response)); // 2. 从注册中心拉取实例列表,然后处理选实例逻辑 return supplier.get(request) .next() .map(serviceInstances -> processInstanceResponse(supplier, serviceInstances)); } private Response<ServiceInstance> processInstanceResponse( ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) { return getInstanceResponse(serviceInstances); } private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) { if (instances.isEmpty()) { return new EmptyResponse(); } // 3. 轮询核心:原子递增位置,取模得到实例索引 int pos = this.position.incrementAndGet() & Integer.MAX_VALUE; ServiceInstance instance = instances.get(pos % instances.size()); return new DefaultResponse(instance); }
代码作用:
supplier.get(request)
:通过ServiceInstanceListSupplier
从注册中心(如Nacos)拉取服务实例列表(如item-service
的localhost:8081
、localhost:8083
);pos % instances.size()
:轮询算法核心——用原子类position
递增计数,对实例数量取模,保证“按顺序依次选择实例”,实现负载均衡。
四、拉取实例:DiscoveryClientServiceInstanceListSupplier#get
(从注册中心拉取实例)
ServiceInstanceListSupplier
的实现类(如DiscoveryClientServiceInstanceListSupplier
)负责从注册中心拉取实例列表。若使用Nacos,底层是NacosDiscoveryClient
:
// DiscoveryClientServiceInstanceListSupplier.java @Override public Flux<List<ServiceInstance>> get(Request request) { return Flux.defer(() -> { // 从注册中心拉取指定serviceId的所有实例 List<ServiceInstance> instances = discoveryClient.getInstances(serviceId); return Flux.just(instances); }); }
代码作用:
discoveryClient.getInstances(serviceId)
:调用注册中心的客户端(如NacosDiscoveryClient
),从Nacos服务器拉取item-service
的所有实例元数据(IP、端口等)。
五、URI重构与真实请求:回到FeignBlockingLoadBalancerClient
当RoundRobinLoadBalancer
选出实例(如localhost:8081
)后,流程回到FeignBlockingLoadBalancerClient
:
// FeignBlockingLoadBalancerClient.java(续) URI reconstructedUri = loadBalancerClient.reconstructURI(instance, originalUri); // 例如:将http://item-service/items/10 → http://localhost:8081/items/10 Request newRequest = buildRequest(request, reconstructedUri); return delegate.execute(newRequest, options); // 发起真实HTTP请求
代码作用:
把“服务名形式的URL”替换为“实例IP+端口的URL”;
通过
delegate.execute
发起最终的HTTP请求,完成远程调用。
总结:代码与流程的对应关系
从“OpenFeign发起请求”到“最终调用成功”,核心代码与流程的对应逻辑是:
FeignBlockingLoadBalancerClient#execute
:触发负载均衡,提取serviceId
;BlockingLoadBalancerClient#choose
:为服务分配负载均衡器;RoundRobinLoadBalancer#choose
:拉取实例+轮询选实例;DiscoveryClientServiceInstanceListSupplier#get
:从注册中心拉取实例;重构URI并发起真实请求。
这种“分层解耦”的设计,让“远程调用”“负载均衡算法”“注册中心交互”职责分离,既保证了流程清晰,又具备极强的扩展性(比如替换负载均衡算法、切换注册中心都很方便)。
面试篇:NacosLoadBalancer的负载均衡策略(集群优先+权重随机)
要清晰讲解 NacosLoadBalancer的负载均衡策略(集群优先+权重随机),我们结合代码、源码逻辑、Nacos控制台操作分三步展开:
一、切换负载均衡策略:从默认轮询到NacosLoadBalancer
Spring Cloud LoadBalancer默认使用 RoundRobinLoadBalancer
(轮询算法),但Nacos提供了更适合生产的 NacosLoadBalancer
(支持集群优先+权重随机)。要切换策略,需自定义配置类并指定生效范围。
1. 自定义负载均衡配置类
编写OpenFeignConfig
类,创建NacosLoadBalancer
的Bean(替换默认的ReactorLoadBalancer
):
package com.hmall.cart.config; import com.alibaba.cloud.nacos.NacosDiscoveryProperties; import com.alibaba.cloud.nacos.loadbalancer.NacosLoadBalancer; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; public class OpenFeignConfig { @Bean public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer( Environment environment, NacosDiscoveryProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) { // 获取服务名(如item-service) String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); // 创建NacosLoadBalancer,传入服务实例列表提供者、服务名、Nacos配置 return new NacosLoadBalancer( loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name, properties); } }
⚠️ 注意:这个类不能加@Configuration
注解(否则会全局强制覆盖,我们要通过@LoadBalancerClients
灵活指定生效范围)。
2. 配置生效范围(全局/局部)
在微服务启动类上,用@LoadBalancerClients
指定配置生效:
全局生效:对所有远程调用的服务生效
@LoadBalancerClients(defaultConfiguration = OpenFeignConfig.class) @EnableFeignClients(clients = {ItemClient.class}) @SpringBootApplication public class CartApplication { public static void main(String[] args) { SpringApplication.run(CartApplication.class, args); } }
局部生效:只对特定服务(如
item-service
)生效@LoadBalancerClients({ @LoadBalancerClient(value = "item-service", configuration = OpenFeignConfig.class) })
3. 验证策略切换
重启服务后,通过Debug模式查看loadBalancer
的类型(如图):
原本默认是
RoundRobinLoadBalancer
,现在变成NacosLoadBalancer
,证明策略切换成功。
二、NacosLoadBalancer的“集群优先”逻辑
NacosLoadBalancer的核心优势是优先选择“同集群”的服务实例,减少跨机房/跨集群调用的网络延迟。
1. 集群的作用与配置
在分布式系统中,服务可能部署在不同机房/集群(如“杭州集群”“北京集群”)。跨集群调用会因网络距离产生额外延迟,因此需优先调用“同集群实例”。
微服务的集群由spring.cloud.nacos.discovery.cluster-name
配置(如cart-service配置为DEFAULT
集群)。
2. 源码中“集群筛选”逻辑
查看NacosLoadBalancer
的源码(如图中代码片段),核心流程:
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) { // 1. 获取当前服务(调用方)的集群名(如cart-service的cluster-name: DEFAULT) String clusterName = this.nacosDiscoveryProperties.getClusterName(); List<ServiceInstance> instancesToChoose = serviceInstances; if (StringUtils.isNotBlank(clusterName)) { // 2. 筛选:从目标服务(如item-service)的所有实例中,找出“集群名匹配”的实例 List<ServiceInstance> sameClusterInstances = serviceInstances.stream() .filter(instance -> { String instanceCluster = instance.getMetadata().get("nacos.cluster"); return StringUtils.equals(instanceCluster, clusterName); }) .collect(Collectors.toList()); // 3. 若有同集群实例,优先在这些实例中选 if (!CollectionUtils.isEmpty(sameClusterInstances)) { instancesToChoose = sameClusterInstances; } } // 后续在筛选后的实例中做负载均衡... }
3. 效果验证
假设:
cart-service的集群是
DEFAULT
;item-service有3个实例:2个在
DEFAULT
集群(8081、8083),1个在BJ
集群(8084)。
当cart-service调用item-service时,NacosLoadBalancer
会先筛选出DEFAULT
集群的2个实例(如图中sameClusterInstances size=2
),优先在这2个实例中做负载均衡,避免跨集群调用。
三、NacosLoadBalancer的“权重随机”算法
同集群内的实例,NacosLoadBalancer采用加权随机算法:实例的“权重”越高,被选中的概率越大(可在Nacos控制台手动配置权重)。
1. Nacos控制台配置权重
进入Nacos控制台→服务详情→实例“编辑”(如图):
找到目标实例(如item-service的8083实例),修改“权重”(比如设为
5
,默认是1
)。
2. 加权随机的源码逻辑
NacosLoadBalancer
中,通过getHostByRandomWeight3
方法实现加权随机(方法名暗示“加权随机”)。核心逻辑是:
为每个实例生成“权重占比”的区间;
生成随机数,落在哪个实例的区间,就选哪个实例;
权重大的实例,区间范围更大,被选中的概率更高。
3. 效果验证
将item-service的8083实例权重设为5
,8081实例权重保持1
:
多次调用购物车接口(触发cart-service调用item-service),会发现8083实例被调用的次数远多于8081(因为权重更高,被选中概率大)。
总结:NacosLoadBalancer的优势
相比Spring Cloud LoadBalancer默认的“轮询/随机”,NacosLoadBalancer
做到了:
集群优先:减少跨集群调用的网络延迟,提升性能;
权重可调:通过Nacos控制台灵活配置实例权重,实现“性能好的实例承担更多流量”,优化资源利用。
这种策略更贴合生产环境中“多机房部署+差异化实例性能”的复杂场景。
面试篇:服务保护(以Sentinel为核心)
要清晰讲解服务保护(以Sentinel为核心)的技术原理,我们可以从线程隔离和四大限流算法(固定窗口、滑动窗口、令牌桶、漏桶)展开,结合类比和示例让逻辑更易懂:
一、线程隔离:限制下游服务的并发风险
线程隔离是为了避免“单个下游服务故障”拖垮整个当前服务,有两种实现思路:
1. 线程池隔离(Hystrix默认方案)
原理:给每个下游服务的调用分配独立线程池。比如调用“服务A”用“服务A线程池”,调用“服务B”用“服务B线程池”。
类比:把每个下游服务的调用比作“不同施工队”,各队有自己的“工人(线程)”。若“服务A”的工人忙到瘫痪,“服务B”的工人仍能正常干活。
优缺点(结合图片):
优点:支持主动超时(线程池可设置超时,到点强制终止请求)、支持异步调用。
缺点:线程有额外开销(创建、销毁、上下文切换耗资源),适合低扇出(调用下游服务少)场景。
2. 信号量隔离(Sentinel采用的方案)
原理:不用线程池,而是用计数器记录当前调用下游服务的线程数。比如给“服务C”设信号量为10,当10个线程都在调用“服务C”时,新请求直接拒绝。
类比:给每个下游服务发“通行卡”,卡数量固定。卡发完后,新请求要么等卡,要么被拒。
优缺点(结合图片):
优点:轻量级,无额外线程开销(不用建线程池),适合高频调用、高扇出(调用下游服务多)场景。
缺点:不支持主动超时(只能等下游自己响应)、不支持异步调用。
二、限流算法:控制流量洪峰,保护服务稳定
限流的核心是限制“单位时间内的请求数(QPS)”,常见4种算法各有特点:
1. 固定窗口计数(基础但有缺陷)
原理:把时间切成固定长度的窗口(如1秒1个窗口),每个窗口内统计请求数。若请求数超过“限流阈值”,拒绝新请求。
示例(结合图片):
窗口1秒,阈值3。第1、2秒请求数<3,正常;第3秒来5个请求,前3个放行,后2个拒绝。
缺陷:临界漏洞。比如“4.5~5秒”来3个请求,“5~5.5秒”又来3个请求——从“4.5~5.5秒”看,总请求6,远超阈值3,但固定窗口只看“第5秒”和“第6秒”各自的计数,都会放行,导致实际流量超限。
2. 滑动窗口计数(解决固定窗口的临界问题)
原理:窗口长度固定(如1秒),但窗口是“滑动”的,且内部细分“小区间”(如把1秒分成2个500ms小区间)。统计时,取“当前时间往前推1秒”的所有小区间请求总和,判断是否超阈值。
示例(结合图片,以1秒窗口、2个500ms小区间、阈值3为例):
1300ms请求:当前时间往前推1秒是300ms,找到“500~1000ms”和“1000~1500ms”小区间,统计总和(假设3个),放行。
1400ms请求:两个小区间总和变4,超阈值3,拒绝。
1600ms请求:窗口滑动到“1000~1500ms”和“1500~2000ms”小区间,继续统计。
优势:通过“滑动+细分小区间”,更精确统计“任意1秒内”的请求数,避免固定窗口的临界漏洞。
3. 令牌桶算法(Sentinel热点参数限流的基础)
原理(结合图片):
系统以固定速率生成“令牌”,存入“令牌桶”(桶有最大容量,满则停止生成)。
请求必须先拿令牌,拿到才会被处理;拿不到则等待或被拒。
特点:允许突发流量。若桶里存了大量令牌(如前几秒无请求,令牌攒着),某刻突发大流量时,能一次性用掉存的令牌,瞬间QPS会超“生成速率”,但长期QPS仍受控。
注意:令牌桶“最大容量”别设为服务QPS上限,要留缓冲,否则突发时仍可能超限。
4. 漏桶算法(Sentinel排队等待的基础)
原理(结合图片,把请求比作“水滴”,漏桶比作“队列”):
请求到达后,先放入“漏桶”(队列)。
漏桶以固定速率“漏水”(处理请求),桶满则新请求直接丢弃。
特点:流量整型。不管上游请求多“陡”(突发大流量),下游收到的请求都是“平滑的”(固定速率),像大坝拦洪水后匀速泄洪,避免下游被冲垮。
三、总结:技术选型与场景匹配
线程隔离:高频/高扇出场景选信号量隔离(Sentinel);需超时/异步场景选线程池隔离(Hystrix)。
限流算法:
固定窗口:简单但有漏洞,少用;滑动窗口:精确统计,Sentinel普通限流常用;
令牌桶:支持突发流量,Sentinel热点参数限流用;
漏桶:流量整型,Sentinel排队等待用。
这些技术共同构成Sentinel的“服务保护”能力,让微服务在流量波动时,既抗住突发,又不因局部故障雪崩。
面试题
1. SpringCloud有哪些常用组件?分别是什么作用?
SpringCloud是微服务开发的“工具集”,常用组件及作用:
服务注册发现:如Nacos、Eureka,负责管理服务的IP、端口等信息,让服务能“找到”彼此。
远程调用:如OpenFeign,基于接口的声明式调用,简化服务间HTTP请求(不用手动写HttpClient)。
负载均衡:如Spring Cloud LoadBalancer(替代Ribbon),帮服务消费者从多个实例中选一个调用,分摊压力。
服务保护:如Sentinel,通过限流、熔断、隔离防止单个服务故障拖垮整个系统。
网关:如Spring Cloud Gateway,统一入口,处理路由、鉴权、限流等(像小区门卫,所有请求先经过它)。
配置中心:如Nacos、Config,集中管理多环境配置,支持动态刷新(改配置不用重启服务)。
分布式事务:如Seata,解决跨服务操作的数据一致性问题(比如下单时扣库存和减余额要同时成功/失败)。
2. 服务注册发现的基本流程是怎样的?
核心是“服务自报家门,消费者按需查找”:
注册:服务启动时,把自己的IP、端口、服务名等信息“上报”给注册中心(如Nacos),注册中心存起来。
心跳:服务定期给注册中心发“心跳”(比如Nacos默认5秒一次),证明自己还活着;注册中心没收到心跳会把服务标记为下线。
发现:服务消费者从注册中心“拉取”目标服务的所有实例列表(比如订单服务找用户服务)。
调用:消费者用负载均衡算法(如轮询)从列表中选一个实例,直接调用。
更新:注册中心若检测到服务上下线,会主动推送新列表给消费者(如Nacos),或消费者定期拉取(如Eureka)。
3. Eureka和Nacos有哪些区别?
两者都是注册中心,但功能和细节差异明显:
功能范围:Nacos是“注册中心+配置中心”二合一;Eureka只有注册中心功能。
健康检测:
心跳间隔:Eureka默认30秒,Nacos默认5秒(Nacos更敏感)。
故障剔除:Eureka90秒未收到心跳才标记故障,60秒清理一次;Nacos15秒超时,30秒剔除(Nacos更快)。
服务列表更新:Nacos支持“主动推送+定期拉取”(服务变了马上通知消费者);Eureka只能消费者30秒自己拉取(延迟高)。
容错策略:Eureka更保守(若85%服务心跳异常,会认为自己网络有问题,暂停剔除服务,避免误杀);Nacos更果断。
4. Nacos的分级存储模型是什么意思?
Nacos用“多层目录”管理服务实例,类似“文件夹分类”,从外到内:
Namespace:环境隔离(如dev、test、prod环境),不同Namespace的服务完全隔离。
Group:同一环境内的服务分组(如按业务线分“支付组”“商品组”),默认DEFAULT_GROUP。
Service:服务名(如item-service、user-service),是服务的唯一标识。
Cluster:集群(如“上海机房”“北京机房”),同一服务的实例按机房分组,减少跨机房调用延迟。
Instance:具体服务实例(如192.168.1.1:8081),包含IP、端口等信息。
这种分级让服务管理更灵活,比如多环境隔离、按机房调度。
5. OpenFeign是如何实现负载均衡的?
OpenFeign本身不做负载均衡,而是“搭便车”整合了Spring Cloud LoadBalancer,流程如下:
代理对象:OpenFeign为接口生成代理对象,当调用接口方法时,代理对象拦截请求。
提取服务名:从请求URL中拿到服务名(如http://item-service/xxx中的item-service)。
选实例:通过LoadBalancerClient,根据服务名找对应的负载均衡器(如RoundRobinLoadBalancer轮询、NacosLoadBalancer集群优先+权重),从注册中心拉取的实例列表中选一个。
重构URL:把服务名替换成选中实例的IP+端口(如http://192.168.1.1:8081/xxx)。
发请求:用重构后的URL发起HTTP请求,完成远程调用。
6. 什么是服务雪崩,常见的解决方案有哪些?
服务雪崩:一个服务故障(如响应慢、宕机),导致依赖它的服务排队等待,资源耗尽,进而引发更多服务故障,像多米诺骨牌倒塌。
解决方案:
熔断:当服务调用失败率过高(如50%),暂时“断开”调用,直接返回兜底数据(如Sentinel的熔断规则),避免持续浪费资源。
限流:限制单位时间内的请求数(如每秒100次),超出的请求拒绝,防止流量洪峰压垮服务。
隔离:给每个下游服务的调用分配独立资源(如线程池隔离、信号量隔离),一个服务故障不影响其他服务。
降级:非核心功能降级(如商品详情页不显示推荐列表),优先保证核心功能可用。
超时控制:设置调用超时时间(如2秒),超时直接返回,避免无限等待。
7. Hystrix和Sentinel有什么区别和联系?
联系:都是服务保护组件,能实现熔断、限流、隔离,防止服务雪崩。
区别:
活跃度:Hystrix已停更,Sentinel是阿里开源,持续维护,功能更丰富。
隔离方式:Hystrix默认用“线程池隔离”(每个服务调用用独立线程池,开销大);Sentinel用“信号量隔离”(计数器控制并发,轻量高效)。
监控与配置:Sentinel有可视化控制台,支持动态配置规则(限流、熔断等);Hystrix配置较繁琐,监控较弱。
适用场景:Sentinel更适合高并发、规则动态变更的场景;Hystrix多见于老项目。
8. 限流的常见算法有哪些?
限流是“控制单位时间内的请求数”,常见算法:
固定窗口:把时间切成固定窗口(如1秒),统计窗口内请求数,超阈值则拒绝。缺点:临界时间可能超限(如4.5~5.5秒内请求超阈值,但两个窗口各自没超)。
滑动窗口:将固定窗口分成多个小区间(如1秒分2个500ms区间),窗口随时间滑动,统计“当前时间往前推1秒”的所有区间请求总和。解决固定窗口的临界问题,Sentinel普通限流用这个。
令牌桶:按固定速率生成令牌(如每秒100个),存到令牌桶(有最大容量);请求必须拿令牌才能处理。支持突发流量(桶里攒的令牌可一次性用),Sentinel热点参数限流用这个。
漏桶:请求先进入“漏桶”(队列),桶以固定速率“漏水”(处理请求),满了则丢弃。能让流出的流量更平滑,Sentinel的排队等待功能用这个。
9. 什么是CAP理论和BASE思想?
CAP理论:分布式系统有三个指标,不可能同时满足:
一致性(C):所有节点数据一致(如改了A节点,B节点马上同步)。
可用性(A):任何时候请求都能成功(读/写不失败)。
分区容错性(P):网络故障(分区)时,系统仍能工作。
分布式系统必须满足P(网络故障不可避免),所以只能二选一:CP(保一致,牺牲可用性)或AP(保可用,牺牲强一致)。
BASE思想:对CAP的妥协,不追求强一致,而是“最终一致”:
基本可用(BA):故障时允许部分功能不可用(如降级),但核心功能可用。
软状态(S):允许短时间数据不一致(如同步延迟)。
最终一致性(E):过一段时间后,数据一定一致(如分布式事务的最终一致)。
10. 项目中碰到过分布式事务问题吗?怎么解决的?
碰到过,比如“下单流程”:扣库存(库存服务)和扣余额(用户服务)必须同时成功或失败,否则会出现“扣了库存但没扣钱”的问题。
项目中用Seata的AT模式解决:
引入Seata,每个服务作为分支事务,由TC(事务协调器)协调全局事务。
第一阶段:每个分支事务执行并提交,同时记录数据快照(undo log),用于回滚。
第二阶段:若所有分支成功,TC通知删除快照;若有失败,TC通知用快照回滚,保证最终数据一致。
11. AT模式如何解决脏读和脏写问题的?
脏写:多个事务同时修改同一数据,后提交的覆盖先提交的。AT模式通过全局锁解决:事务释放数据库锁前,必须先拿全局锁,其他事务没拿到全局锁不能修改,避免并发覆盖。
脏读:一个事务读取到另一个未提交的事务数据。AT模式第一阶段会记录数据快照(更新前的状态),若其他事务修改了数据,回滚时会基于快照恢复,避免读取到中间不一致的数据。
12. TCC模式与AT模式对比,有哪些优缺点?
TCC模式(手动编码补偿)(快,不用等其他事务):
优点:
性能好:一阶段直接提交事务,释放资源,不用等其他服务。
兼容性强:不依赖数据库事务,支持非关系型数据库(如MongoDB)。
缺点:
代码侵入性强:需手动写try(资源预留)、confirm(提交)、cancel(回滚)方法,开发量大。
复杂:要处理空回滚(没执行try却执行cancel)、事务悬挂(try执行时全局事务已结束)等问题。
AT模式(自动快照回滚):
优点:
无侵入:业务代码不用改,框架自动生成快照和回滚逻辑。
简单:开发者不用关心补偿细节。
缺点:
依赖数据库事务:只支持关系型数据库(如MySQL)。
有开销:生成快照、全局锁会消耗资源,性能略低。
13. RabbitMQ是如何确保消息的可靠性的?
从“生产→存储→消费”全链路保障: (确认--持久化--确认)
生产端:用“publisher confirm”机制,消息发出去后,RabbitMQ会回传确认(ack),没收到ack则重试;若消息路由失败,用“publisher return”机制退回,避免消息丢失。
存储端:开启持久化(交换机、队列、消息都设为持久化),RabbitMQ重启后消息不丢失。
消费端:关闭自动ACK,处理完消息后手动发送ACK;若处理失败,不发ACK,消息会重新入队,避免消息没处理完就被删除。
14. RabbitMQ是如何解决消息堆积问题的?
消息堆积是“生产快、消费慢”导致队列满,解决方案:
临时扩容:增加消费者实例,并行消费(前提是队列支持多消费者)。
优化消费速度:消费者批量拉取消息、异步处理、优化业务逻辑(如减少DB操作)。
死信队列+延迟重试:堆积的消息超过一定时间,进入死信队列,后续慢慢重试,避免阻塞正常队列。
监控预警:实时监控队列长度,超过阈值报警,提前扩容或限流。
高效序列化:用Protobuf替代JSON,减少消息大小,加快传输和处理。