💪🏻 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-serviceorder-service 启动类都打了 @EnableDiscoveryClientapplication.ymlspring.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。配置变更 → 客户端收到通知 → 发布 RefreshEventRefreshScope.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.yamlorder.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 的实例,各自刷新。

④ 话术钩子

  1. “热更新不是银弹,有状态的 Bean 不能乱用 @RefreshScope……” → 勾 Q2。
  2. “@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 怎么做降级/熔断? → 配 fallbackfallbackFactory(需引 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.Optionsapplication.yml 的 connect/read timeout。
二是扣减做幂等deductStockrequestId 判重,同一 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.ymlserver.shutdown: gracefulGracefulShutdownConfig 实现 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-apiProductFeignClient(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。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐