Spring Cloud Nacos 项目面试宝典
💪🏻 1. Python基础专栏,基础知识一网打尽,9.9元买不了吃亏,买不了上当。 Python从入门到精通
💪🏻 2. AI编程变现手册,从学会AI编程到实现变现都可以
😁 3. 毕业设计专栏,毕业季咱们不慌忙,几千款毕业设计等你选。
❤️ 4. DeepSeek+RPA提效从入门到变现实战❤️5. 欢迎访问我的免费工具站
❤️ 6. Java高并发编程入门,打卡学习Java高并发。 Java高并发编程入门
文章目录
考点全景图
| # | 考点 | 项目载体 | 实现状态 |
|---|---|---|---|
| 1 | 服务注册与发现 | @EnableDiscoveryClient + Nacos discovery |
✅ 完整 |
| 2 | 配置中心 + @RefreshScope 热更新 | DiscountConfig / bootstrap.yml |
✅ 完整 |
| 3 | OpenFeign 声明式调用 + 契约共享 | ProductFeignClient / common-api |
✅ 完整 |
| 4 | 负载均衡 | @LoadBalanced RestTemplate(默认轮询) |
⚠️ 默认轮询,同集群优先未实现 |
| 5 | 服务容错:Feign 重试 + 超时 + 幂等 | FeignConfig / ProductService.deductStock |
⚠️ 关重试✅、幂等内存版 |
| 6 | 优雅停机 | GracefulShutdownConfig |
✅ 完整 |
| 7 | 服务发现实时性(push/pull) | NacosInstanceChangeListener |
✅ 完整 |
| 8 | 微服务拆分 / 契约共享 | common-api 模块 |
✅ 完整 |
| 9 | 防超卖:乐观锁 | ProductRepository 乐观锁 SQL |
✅ 完整 |
考点 1:服务注册与发现(Nacos)
① 原理讲透
- 服务启动时,
spring-cloud-starter-alibaba-nacos-discovery把实例信息(ip、port、服务名、metadata)注册到 Nacos,之后靠心跳维持健康状态(Nacos 2.x 用 gRPC 长连接,1.x 是 HTTP + 定时心跳)。 - consumer 从 Nacos 拉取某服务的健康实例列表,缓存在本地,调用时按负载均衡策略选一个。
- Nacos 是 CP 还是 AP? 默认 AP(临时实例,基于 Distro 协议,保可用性,短暂容忍数据不一致);如果注册为持久化实例(
ephemeral=false),走 Raft 协议是 CP。这是高频考点,一定记住"默认 AP、可切 CP"。
② 结合本项目怎么答
“两个服务
product-service和order-service启动类都打了@EnableDiscoveryClient,application.yml配spring.cloud.nacos.discovery.server-addr: 127.0.0.1:8848。启动后在 Nacos 控制台服务列表能看到两个服务注册上来,product 我起了两个实例演示。order 通过服务名product-service发现并调用它,不写死 IP。”
③ 追问链
- Q:Nacos 注册表数据结构? → 三层 Map:
namespace → group → service,service 下按 cluster 分组存实例列表。 - Q:CP 还是 AP? → 见原理层,默认 AP,可配 CP。
- Q:临时实例和持久实例区别? → 临时实例靠心跳,心跳停就摘除(AP/Distro);持久实例即使宕机也保留在注册表,健康状态标记为不健康(CP/Raft)。
- Q:和 Eureka / Zookeeper 比? → Eureka 纯 AP;Zookeeper 纯 CP;Nacos 可切换,且注册中心+配置中心二合一。
④ 话术钩子
主动说 “Nacos 默认是 AP 的临时实例,我们也可以配成持久化的 CP 实例……” → 勾出 CP/AP 深挖。
⑤ 自测
Nacos 默认 CP 还是 AP?临时 vs 持久实例的核心区别?注册表三层结构?
考点 2:配置中心 + @RefreshScope 热更新 ⭐核心
① 原理讲透
- 普通单例 Bean 启动时
@Value注入的值就固化了,Nacos 配置变了 Bean 里的字段不会自动变。 @RefreshScope= CGLIB 代理 + 懒重建:把 Bean 放进自定义RefreshScope。配置变更 → 客户端收到通知 → 发布RefreshEvent→RefreshScope.refreshAll()销毁缓存里的旧目标对象 → 下次调用该 Bean 时用新配置重建。- 本质一句话:销毁旧 Bean,下次访问时用新配置重建——不是原地改字段。
- 为什么配置放
bootstrap.yml:bootstrap 上下文先于 application 上下文加载。Nacos 地址、要拉哪个 dataId 必须在应用初始化之前就知道,否则@Value注入时配置还没拉下来。(Spring Cloud 2020+ 默认移除 bootstrap,需引spring-cloud-starter-bootstrap启用——你项目是 2021.0.x。)
② 结合本项目怎么答
“订单折扣率从 Nacos 动态下发。
DiscountConfig打了@RefreshScope,@Value(\"${order.discount.rate:1.0}\")注入折扣率,下单时OrderService算金额 = 价格 × 数量 × 折扣率。在 Nacos 改order-service.yaml的order.discount.rate,不重启,调GET /order/config就看到新值,下一笔订单立刻按新折扣算。原理是 @RefreshScope 给 Bean 套 CGLIB 代理放进 refresh scope,配置变更时销毁旧 Bean、下次调用用新配置重建。”
③ 追问链
- Q:@RefreshScope 底层? → 自定义 Scope + CGLIB 代理 + 懒重建。
- Q:销毁重建,如果 Bean 有连接池会怎样?(= 坑4)→ 会出问题,销毁时连接池被关、活跃连接中断。所以配置分两类:业务参数(折扣率、开关)热更;基础设施参数(连接池、线程池)重启生效。主动暴露这个认知 = 高分点。
- Q:客户端怎么感知配置变更?推还是拉? → Nacos 2.x gRPC 长连接推送为主(1.x 是 HTTP 长轮询 ~30s)。收到后发
RefreshEvent触发刷新。 - Q:
@ConfigurationProperties需要 @RefreshScope 吗?(陷阱)→ 不需要!它由ConfigurationPropertiesRebinder监听EnvironmentChangeEvent自动重绑定。@Value才依赖 @RefreshScope。你项目OrderProperties正好是 @ConfigurationProperties 方式,可对比:“两种我都写了”——瞬间区分于背题的人。 - Q:多实例怎么都生效? → Nacos 广播给所有订阅该 dataId 的实例,各自刷新。
④ 话术钩子
- “热更新不是银弹,有状态的 Bean 不能乱用 @RefreshScope……” → 勾 Q2。
- “@Value 和 @ConfigurationProperties 的刷新机制其实不一样……” → 勾 Q4。
⑤ 自测
@RefreshScope 原地改还是销毁重建?为何连接池不能热更?配置为何放 bootstrap?@ConfigurationProperties 靠什么刷新?
考点 3:OpenFeign 声明式调用 + 契约共享 ⭐核心
① 原理讲透
- Feign 是声明式 HTTP 客户端:你定义一个接口 + 注解,Feign 用 JDK 动态代理生成实现类,把方法调用翻译成 HTTP 请求。
- 调用链:
@FeignClient接口方法 → 动态代理 →RequestTemplate组装请求 → 集成 LoadBalancer 选实例 → 编码器序列化 → HTTP 发出 → 解码器反序列化响应。 - 底层默认用 JDK
HttpURLConnection,可换 OkHttp / Apache HttpClient(配连接池提性能)。
② 结合本项目怎么答
“订单服务调商品服务用 OpenFeign。我把 Feign 接口
ProductFeignClient和 DTO 抽到独立的common-api模块,@FeignClient(name=\"product-service\", path=\"/product\"),定义了getProduct(id)和deductStock(id, req)。provider(商品服务)和 consumer(订单服务)都依赖 common-api,共享同一份契约,避免两边接口定义漂移。下单时直接productFeignClient.getProduct(productId),像调本地方法。”
③ 追问链
- Q:Feign 底层原理? → 动态代理 + RequestTemplate + 编解码器 + 集成 LoadBalancer。
- Q:为什么把 Feign 接口抽独立模块? → 契约单一来源,provider 实现该接口、consumer 引用该接口,改接口两边编译期就能发现不一致;缺点是模块耦合,需版本管理。
- Q:Feign 和 RestTemplate 区别? → Feign 声明式(接口+注解,可读性高);RestTemplate 命令式(手动拼 URL/参数)。你项目两者都有:Feign 调商品,
RestTemplateConfig里也配了@LoadBalanced RestTemplate。 - Q:Feign 怎么做降级/熔断? → 配
fallback或fallbackFactory(需引 Sentinel/Resilience4j)。你项目没做熔断降级,被问到如实说"这个 Demo 没接 Sentinel,生产会加"。
④ 话术钩子
主动说 “我特意把 Feign 契约抽成 common-api 共享模块,就是为了 provider 和 consumer 不会各写一份 DTO 对不上……” → 展示工程思维。
⑤ 自测
Feign 靠什么生成实现类?契约共享模块的利弊?你项目做熔断降级了吗(诚实答:没有)?
考点 4:负载均衡 ⚠️(含坑5诚实说明)
① 原理讲透
- Spring Cloud 2020+ 用 Spring Cloud LoadBalancer 取代了 Ribbon(Ribbon 已废弃)。
@LoadBalanced给 RestTemplate/Feign 装一个拦截器,把http://service-name/xxx里的服务名替换成从注册中心选出的真实实例 ip:port。- 默认策略:轮询(RoundRobin);也可换随机(Random)或自定义。
- 自定义:实现
ReactorServiceInstanceLoadBalancer,重写实例筛选逻辑。
② 结合本项目怎么答(诚实版)
“负载均衡用 Spring Cloud LoadBalancer。
RestTemplateConfig里@LoadBalanced修饰 RestTemplate,Feign 也走它。product 起了两个实例(8071/8072),下单时观察返回体里的servicePort字段能看到请求在两个实例间轮询。
我原本想做’同集群优先路由’——就是 consumer 优先调同机房的实例减少跨网络延迟。目前我把集群元数据打通了(实例注册时带 cluster-name),但自定义 LoadBalancer 的路由过滤逻辑还没落地,实际还是默认轮询。这块我正在补,核心是实现ReactorServiceInstanceLoadBalancer,按实例 metadata 里的 cluster 和本机 cluster 匹配做优先过滤,全不可用再回退全量。”
⚠️ 红线:坑5 README 原本吹"已实现 ClusterPriorityLoadBalancer",实际没有这个类。product 侧
ClusterPriorityConfiguration只打集群元数据日志。面试务必按上面诚实版讲。
③ 追问链
- Q:LoadBalancer 和 Ribbon 区别? → Ribbon 是 Netflix 旧组件已停更;LoadBalancer 是 Spring 官方替代,基于 Reactor,更轻量。
- Q:轮询怎么实现的? → 内部维护原子计数器
position,每次请求position++ % 实例数。 - Q:怎么实现同集群优先/灰度路由? → 自定义
ReactorServiceInstanceLoadBalancer,从ServiceInstanceListSupplier拿实例列表后按 metadata 过滤。(这正是你要补的,讲清思路即可)
④ 话术钩子
“负载均衡默认轮询,但生产上跨机房调用延迟高,理想是同集群优先——这需要自定义 LoadBalancer,我正在做……” → 主动亮出"知道怎么优化",把"未实现"转成"有规划"。
⑤ 自测
LoadBalancer 取代了谁?默认策略?自定义同集群优先要实现哪个接口?(诚实:你项目这块实现了吗?——没有,元数据打通了、路由没做)
考点 5:服务容错 —— Feign 重试 + 超时 + 幂等 ⭐核心 ⚠️(含坑1诚实说明)
① 原理讲透
- Feign 默认重试的坑:Feign 有默认
Retryer,请求失败/超时会自动重试。危险场景:扣库存接口处理慢但其实成功了,Feign 超时后又发一次 → 库存被扣两次。 - 超时配置:
connectTimeout(建连超时)+readTimeout(读响应超时)。超时时间要 > 下游正常处理时间,否则误判。 - 幂等:同一操作执行多次和执行一次效果相同。实现手段:唯一标识(requestId)+ 去重存储(DB 唯一索引 / Redis SETNX / 内存 Map)。
② 结合本项目怎么答(诚实版)
“我遇到过 Feign 重试导致库存超扣。根因是 Feign 默认超时会重试,商品服务处理慢但实际成功,重试又扣一次。解法两步:
一是关重试:FeignConfig里@Bean Retryer feignRetryer(){ return Retryer.NEVER_RETRY; },改成业务层自己控重试。同时配了Request.Options和application.yml的 connect/read timeout。
二是扣减做幂等:deductStock用requestId判重,同一 requestId 重复请求直接返回成功不重复扣。
这里我要诚实说明:Demo 里幂等判重用的是进程内ConcurrentHashMap,只在单实例有效,多实例和重启会失效。我预留了t_deduct_record唯一索引表演示正确方向,但扣减逻辑还没接进去。生产上会换成 DB 唯一索引或 Redis SETNX。”
⚠️ 红线:README 原本吹"数据库唯一索引",实际是内存 Map。按上面诚实版讲,"内存演示思路、生产换 DB/Redis"反而是加分——说明你知道内存版的局限。
③ 追问链
- Q:为什么要关 Feign 重试? → 重试 + 非幂等接口 = 重复副作用(超扣、重复下单)。要么关重试,要么接口幂等,最好两者都做。
- Q:内存 Map 幂等有什么问题? → ①多实例不共享 ②重启丢失 ③无过期清理会内存泄漏。生产用 Redis(带 TTL)或 DB 唯一索引(靠约束冲突拦截)。
- Q:DB 唯一索引怎么做幂等? → 扣减前插一条
request_id唯一的记录,插入成功才扣,冲突(DuplicateKey)说明重复请求直接返回。 - Q:重试和幂等的关系? → 重试是"提高成功率"的手段,幂等是"保证重试安全"的前提。没有幂等就不能安全重试。
④ 话术钩子
“库存超扣这个坑,表面是重试,本质是非幂等接口被重复调用。我先关了 Feign 重试兜底,再从根上做幂等……我 Demo 里幂等先用内存演示,生产会上 DB 唯一索引……” → 主动暴露局限 = 诚实且有深度。
⑤ 自测
Feign 默认重试为什么危险?关重试的代码?内存 Map 幂等的三个问题?DB 唯一索引怎么保证幂等?
考点 6:优雅停机
① 原理讲透
- 问题:
kill -9(SIGKILL)直接杀进程,Nacos 心跳超时前(默认约 15s)consumer 还往死实例路由 → 发布时几秒请求失败。 - 优雅停机:
server.shutdown: graceful让容器停止时先拒绝新请求、等存量请求处理完再关。配合主动注销:JVM 关闭前主动通知 Nacos 摘除自己,不等心跳超时。 - 用
kill(SIGTERM)而非kill -9,SIGTERM 才会触发 Spring 的关闭钩子。
② 结合本项目怎么答
“发布时出现过几秒请求失败,原因是强杀进程,Nacos 心跳超时前还在往死实例路由。我做了优雅停机:
application.yml配server.shutdown: graceful;GracefulShutdownConfig实现ApplicationListener<ContextClosedEvent>,容器关闭时主动调NamingService.deregisterInstance()从 Nacos 注销当前实例,再 sleep 等注销传播,然后才真正关闭。这样发布时先摘流量再停服,不丢请求。”
③ 追问链
- Q:为什么 kill -9 不行? → SIGKILL 不给进程善后机会,Spring 关闭钩子不执行,来不及注销。要用 SIGTERM。
- Q:主动注销和心跳摘除的区别? → 主动注销是"我告诉 Nacos 我要走了",即时;心跳摘除是"Nacos 发现我没心跳了",有延迟(~15s)。主动注销把这 15s 消掉。
- Q:graceful shutdown 等多久? →
spring.lifecycle.timeout-per-shutdown-phase配置最大等待时间,超时强制关。 - Q:K8s 场景怎么配合? → preStop hook 先摘注册再发 SIGTERM,配合 readiness 探针。
④ 话术钩子
“发布抖动的根因是’先停服务后摘流量’,我改成’先摘流量后停服务’——ContextClosedEvent 里主动注销 Nacos……” → 讲清因果顺序。
⑤ 自测
kill -9 为什么丢流量?主动注销 vs 心跳摘除?触发优雅停机该用什么信号?
考点 7:服务发现的实时性(push/pull)
① 原理讲透
- consumer 本地缓存实例列表,不是实时的。Nacos 用 push + pull 混合:
- push:Nacos 2.x 通过 gRPC 长连接,实例变更时主动推给订阅的 consumer。
- pull:consumer 定时拉取兜底(防推送丢失)。
- 所以新实例上线到被 consumer 感知有延迟窗口,灰度/扩容时要注意。
② 结合本项目怎么答
“Nacos 服务发现不是实时的。我写了
NacosInstanceChangeListener,实现ApplicationRunner,启动后用NamingService.subscribe(\"product-service\", ...)订阅商品服务的实例变更。当 product 实例上下线,回调里打印当前健康实例列表和变更时间。我用它观察过:启动第二个 product 实例后,order 端多久感知到、多久开始往新实例路由。”
③ 追问链
- Q:push 还是 pull? → 2.x 以 gRPC push 为主,pull 兜底;1.x 是 HTTP 长轮询。
- Q:新实例上线为什么不立即被调用? → 本地缓存刷新有延迟 + LoadBalancer 缓存实例列表。
- Q:1.x 和 2.x 通信区别? → 1.x HTTP + 长轮询;2.x 新增 gRPC 长连接,性能和实时性更好。
④ 话术钩子
“服务发现有延迟窗口,我专门写了个监听器订阅实例变更来观测这个延迟……” → 展示你不是黑盒用框架,会验证机制。
⑤ 自测
Nacos 2.x push 还是 pull?为什么新实例不被立即调用?1.x/2.x 通信差异?
考点 8:微服务拆分 / 契约共享(common-api)
① 原理讲透
- 微服务拆分要有清晰边界:商品域、订单域各自独立服务、独立数据。
- 契约共享:Feign 接口 + DTO + 统一响应体抽到
common-api,provider 实现、consumer 引用,单一契约来源。 - 权衡:共享模块降低"接口对不上"风险,但引入模块耦合,需版本管理(改契约影响所有依赖方)。
② 结合本项目怎么答
“我按业务域拆成商品服务和订单服务,各自独立库(Demo 用 H2 内存库)。公共部分抽到
common-api:ProductFeignClient(Feign 契约)、DTO、统一响应体Result<T>(code/message/data/timestamp)、异常体系(BusinessException+ErrorCode枚举)。provider 和 consumer 共享它,保证契约一致、响应风格统一。”
③ 追问链
- Q:为什么统一响应体? → 所有接口返回结构一致(code/message/data),前端和调用方好处理,全局异常处理器统一包装。
- Q:共享模块的缺点? → 耦合。改 common-api 要重新发布所有依赖方;契约膨胀会变成"上帝模块"。折中:只放稳定契约,不放业务逻辑。
- Q:DTO 和 Entity 为什么分开? → Entity 是持久层模型(带 JPA 注解),DTO 是传输模型,解耦内部存储和对外契约,避免暴露表结构。
④ 话术钩子
“我把 Feign 契约和 DTO 抽成 common-api,provider/consumer 共享一份,编译期就能发现接口不一致……不过共享模块要控制边界,只放稳定契约不放业务逻辑,否则会变上帝模块。” → 利弊都讲 = 成熟。
⑤ 自测
契约共享的利弊?为什么要统一响应体?DTO 和 Entity 为何分离?
考点 9:防超卖(乐观锁)
① 原理讲透
- 超卖:并发扣库存,多个请求都读到库存充足,都扣,扣成负数。
- 乐观锁:不加锁,靠条件更新保证。
UPDATE ... SET stock = stock - N WHERE id = ? AND stock >= N,靠影响行数判断成功——影响 0 行说明库存不足或被别人抢先。 - 对比悲观锁:
SELECT ... FOR UPDATE加行锁,串行化,并发低但强一致;乐观锁并发高,靠重试/失败处理冲突。
② 结合本项目怎么答
“扣库存防超卖用乐观锁。
ProductRepository里@Modifying @Query(\"UPDATE Product p SET p.stock = p.stock - :quantity WHERE p.id = :id AND p.stock >= :quantity\"),SQL 里带stock >= quantity条件,返回影响行数。ProductService.deductStock拿到影响行数,>0 成功,=0 抛StockNotEnoughException。这样并发扣减靠数据库行级原子性保证,不会扣成负数,也不用SELECT FOR UPDATE悲观锁。”
③ 追问链
- Q:为什么不用
SELECT FOR UPDATE? → 悲观锁串行化,高并发下锁竞争严重、吞吐低。乐观锁一条 UPDATE 搞定,靠 WHERE 条件 + 影响行数,并发好。 - Q:乐观锁失败了怎么办? → 影响 0 行时业务上返回"库存不足"或重试。本项目是直接抛异常。
- Q:这个能扛超高并发吗(秒杀)? → 不能。DB 乐观锁单行会成热点。秒杀要 Redis 预扣减 + MQ 削峰 + DB 最终落库。Demo 是常规电商级别,秒杀级另一套方案——诚实划清边界。
- Q:version 版本号乐观锁和这个区别? → version 是通用乐观锁(读时取 version,更新时
WHERE version=?并 version+1);这里用stock>=quantity是针对库存场景的特化,更直接。
④ 话术钩子
“防超卖我用乐观锁 WHERE stock >= quantity + 影响行数判断,不用悲观锁……不过这是常规电商量级,真到秒杀级别得上 Redis 预扣减 + MQ 削峰,DB 乐观锁单行会成热点。” → 主动划清适用边界 = 有系统观。
⑤ 自测
乐观锁怎么防超卖?为什么不用 SELECT FOR UPDATE?这套能扛秒杀吗(诚实:不能)?version 乐观锁和 stock 条件更新的区别?
附录 A:一分钟项目自述(面试开场用)
“这是我做的一个 Spring Cloud Alibaba 微服务 Demo,两个业务服务——商品服务和订单服务,加一个共享契约模块 common-api。用 Nacos 做注册中心和配置中心,OpenFeign 做服务调用,Spring Cloud LoadBalancer 做负载均衡。重点不是业务多复杂,而是我在里面复现并解决了几个生产级踩坑场景:Feign 重试导致库存超扣、kill -9 发布丢流量、@RefreshScope 热更新的 Bean 重建陷阱、乐观锁防超卖。每个坑我都有代码演示,也清楚它在生产上还要怎么演进。”
(注意:说"清楚怎么演进"给坑1/坑5的"未实现"留了诚实出口。)
附录 B:诚实清单(面试前必读,防穿帮)
| 场景 | README 别信的旧说法 | 真实代码 | 面试怎么说 |
|---|---|---|---|
| 坑1 幂等 | “数据库唯一索引” | 内存 ConcurrentHashMap | “内存演示思路,生产换 DB/Redis” |
| 坑5 同集群路由 | “ClusterPriorityLoadBalancer 已实现” | 该类不存在,走默认轮询 | “元数据打通了,路由逻辑正在补” |
| 熔断降级 | (README 未提,但面试常问) | 没接 Sentinel/Resilience4j | “Demo 没做,生产会加” |
| 秒杀 | (别主动吹) | 常规乐观锁 | “常规电商级,秒杀另一套方案” |
附录 C:高频八股速记(这个项目串得起来的)
- Nacos CP/AP:默认 AP(临时实例/Distro),可配 CP(持久实例/Raft)。
- 配置放 bootstrap 的原因:先于 application 加载,要先拿到 Nacos 地址。
- @RefreshScope:CGLIB 代理 + 销毁重建,非原地改值;有状态 Bean 慎用。
- @Value vs @ConfigurationProperties 刷新:前者靠 @RefreshScope,后者靠 Rebinder 自动重绑。
- Feign 底层:JDK 动态代理 + RequestTemplate + 编解码 + LoadBalancer。
- LoadBalancer:取代 Ribbon,默认轮询(原子计数器取模)。
- Feign 重试坑:默认重试 + 非幂等 = 重复副作用,关重试 or 幂等。
- 优雅停机:ContextClosedEvent 主动注销 > 等心跳超时;用 SIGTERM 不用 SIGKILL。
- 乐观锁防超卖:UPDATE…WHERE stock>=N,影响行数判断,不用 FOR UPDATE。
更多推荐


所有评论(0)