面试复习资料 列表全部文章
面试复习资料

面试复习:Spring / Spring Boot 与微服务(初中高级)—— IoC AOP 循环依赖 事务失效 自动装配 熔断限流 灰度

按🌱初级/🌿中级/🌳高级三层整理的 Spring 与微服务复习笔记:IoC/AOP、Bean 生命周期、三级缓存与循环依赖、事务传播与失效场景、Spring Boot 自动装配,以及注册配置中心、网关、熔断限流降级、Feign/Dubbo、链路追踪与灰度发布。

更多

“你们的 @Transactional 为什么没回滚?”——这是我见过最能让一个组会瞬间安静的问题。回滚没生效,钱多扣了、库存少了、对账差了,大家一起对着代码盯了两小时,最后发现是同一个类里 this.save() 调了另一个带事务注解的方法。代理没走,注解等于注释。

Spring 和微服务是后端面试里“最像开放题、其实全是细节”的一块:面试官不想听你背 IoC 的定义,他想听你说出“三级缓存里为什么第三级放的是工厂而不是对象”。怎么用:每个专题分 🌱 初级(是什么/怎么用)→ 🌿 中级(为什么/怎么选/常见坑)→ 🌳 高级(怎么落地/出事怎么办)三层,每层给标准回答要点、追问链、加分项。版本相关内容我对照了 Spring Framework、Spring Boot 与各组件官方文档(文末有出处),文中会标注版本。

一、知识地图

Spring 与微服务 Spring 核心 IoC / DI / 容器 refresh Bean 生命周期与后置处理器 循环依赖:三级缓存 AOP:JDK / CGLIB 代理 事务:传播 / 隔离 / 回滚规则 事务失效八大场景 Spring MVC 请求流程 循环依赖 vs 构造注入取舍 事务 + MQ 的一致性 Spring Boot @SpringBootApplication 三合一 AutoConfiguration.imports @Conditional 条件装配 Starter 与自定义 Starter 配置加载顺序与外部化 Actuator 与优雅停机 启动流程与启动优化 JDK 17+ / Boot 3 迁移 --debug 报告排查 微服务治理 注册中心 / 配置中心 网关:路由 / 鉴权 / 限流 超时 / 重试 / 熔断 / 降级 限流算法:令牌桶 / 漏桶 Feign / Dubbo 远程调用 链路追踪:Trace / Span 灰度发布 / 全链路灰度 分布式事务与幂等 服务拆分与雪崩治理

二、IoC、Bean 生命周期

🌱 初级:IoC 与 DI

标准回答要点:

  • IoC(控制反转):把对象的创建与依赖关系的装配交给容器,而不是自己 new;DI(依赖注入)是实现方式:构造器注入、Setter 注入、字段注入。
  • 官方推荐构造器注入(Spring 文档):得到不可变对象、保证必需依赖非空、组件返回时是完全初始化状态;可选依赖用 Setter 注入。字段注入(@Autowired 打在字段上)便于偷懒,但难以单测、隐藏依赖、无法声明 final,团队规范里往往不推荐。
  • @Autowired vs @Resource:前者 Spring 注解,默认按类型,同类型多个用 @Qualifier 或字段名匹配;后者 JSR-250,默认按名称再回退类型。
  • Bean 作用域:singleton(默认,容器内单例)、prototype(每次获取新建)、request、session 等。单例 Bean 不等于线程安全——它只是唯一,有可变成员变量就要自己保证。
  • BeanFactory vs ApplicationContext:前者是基础容器(延迟加载);后者是它的扩展,加了事件、国际化、资源加载、AOP 集成,默认启动时预实例化所有单例,把配置错误尽早暴露在启动阶段。

🌿 中级:Bean 生命周期

这条链一定要能一口气说完,并知道每一步是谁干的:

① 实例化反射调用构造器 ② 属性填充@Autowired 注入 ③ Aware 回调BeanNameAware 等 ④ BPP 前置含 @PostConstruct ⑤ 初始化afterPropertiesSet ⑥ init-method自定义初始化 ⑦ BPP 后置AOP 代理在这里生成 ⑧ 使用中放入单例池 ⑨ 销毁:@PreDestroy → destroy() → destroy-method容器关闭时;prototype 不受容器销毁管理 同一初始化阶段的三种回调顺序(官方文档):@PostConstruct → afterPropertiesSet → 自定义 init-method 循环依赖时,“早期引用”在 ② 属性填充之前就通过第三级缓存暴露出去

  • BeanPostProcessor(BPP)是容器最重要的扩展点:@Autowired 由 AutowiredAnnotationBeanPostProcessor 处理,@PostConstruct 由 CommonAnnotationBeanPostProcessor 处理,AOP 代理由 AnnotationAwareAspectJAutoProxyCreator(也是 BPP)在初始化后包装原始对象。
  • BeanFactoryPostProcessor:在 Bean 实例化之前修改 BeanDefinition,@Configuration、@ComponentScan、@Bean 的解析(ConfigurationClassPostProcessor)、占位符替换都发生在这里。两者容易混:一个改“图纸”,一个改“成品”。
  • 销毁三种回调顺序:@PreDestroy → DisposableBean.destroy() → 自定义 destroy-method。官方文档另指出:@PostConstruct 里避免去访问其他尚未完全就绪的 Bean,昂贵的初始化后动作用 SmartInitializingSingleton 或监听 ContextRefreshedEvent。
  • 容器启动主线:AbstractApplicationContext.refresh():准备环境 → 创建/获取 BeanFactory 并加载 BeanDefinition → 调用 BeanFactoryPostProcessor → 注册 BeanPostProcessor → 初始化事件与消息源 → onRefresh(Boot 里在这里启动内嵌 Tomcat)→ 注册监听器 → finishBeanFactoryInitialization 预实例化所有非懒加载单例 → finishRefresh 发布刷新完成事件。

🌳 高级:启动性能与生命周期治理

  • 启动慢怎么办:先测量(spring.main.lazy-initialization 是选项但会把问题推迟到首次请求;Actuator 的 startup 端点 + BufferingApplicationStartup 可以看每个 Bean 的耗时);排查大头:过多 @ComponentScan 范围、重的 @PostConstruct(预热缓存、远程调用)、Bean 数量、类路径 jar 太多。K8s 场景配合 readiness 探针,别让未就绪实例接流量。
  • SmartLifecycle 与优雅停机:Boot 支持 server.shutdown=graceful + spring.lifecycle.timeout-per-shutdown-phase;先摘注册中心/停止接收 MQ 消息,再等待在途请求,再关线程池、连接池。SmartLifecycle.getPhase() 决定启动/关闭顺序(数值小的先启动,后关闭)。
  • 加分项:知道 Spring 6 / Boot 3 的 AOT 与 GraalVM Native 把“运行期反射与动态代理”提前到构建期,代价是灵活性下降;冷启动敏感(Serverless)场景才值得。

三、循环依赖与三级缓存

🌱 初级:什么是循环依赖

A 依赖 B,B 又依赖 A。构造器注入下,创建 A 需要先有 B,创建 B 需要先有 A,死局。Spring 官方文档明确说:构造器注入形成的循环依赖会抛 BeanCurrentlyInCreationException;Setter/字段注入下 Spring 可以解决(官方同时说“不推荐”这么配置)。更重要的版本事实:Spring Boot 2.6 起默认禁止循环引用(spring.main.allow-circular-references=false),出现循环依赖启动即失败——最佳实践是重构消除循环,而不是打开开关。

🌿 中级:三级缓存怎么解

三级缓存都在 DefaultSingletonBeanRegistry:

级别名称存什么
一级singletonObjects完整可用的单例 Bean(成品)
二级earlySingletonObjects提前暴露的“早期引用”(半成品,可能是代理)
三级singletonFactoriesObjectFactory:需要时调用 getEarlyBeanReference 产出早期引用

流程(A、B 互相 Setter 依赖):

  1. 创建 A:实例化(构造器执行,还是个空壳)→ 把 A 的 ObjectFactory 放进三级缓存 → 开始属性填充,发现需要 B;
  2. 创建 B:实例化 → B 的工厂放入三级缓存 → 填充属性,需要 A;
  3. B 去找 A:一、二级没有,三级缓存有——调用 A 的工厂拿到早期引用(如果 A 需要被 AOP 代理,这里会提前生成代理对象),放入二级缓存并移除三级,注入到 B;
  4. B 完成初始化,进入一级缓存;回到 A,把 B 注入 A,A 完成初始化;A 最终放入一级缓存。

为什么需要三级,两级不行吗?关键在 AOP。正常情况下代理是在 Bean 初始化完成之后(BPP 后置阶段)才创建的。如果只有两级、且实例化后立刻把“原始对象”放进二级缓存,那么 B 拿到的就是原始 A,而最终放入单例池的却是代理 A——同一个 Bean 出现两个不同对象,语义错误。要提前拿到代理又不想让所有 Bean 都提前生成代理,就用第三级的工厂延迟决定:只有真的发生循环依赖、有人来要早期引用时,才在那一刻创建代理;没人要,则保持“初始化后再代理”的正常流程。第二级则保证多次获取早期引用得到同一个对象。

无法解决的场景:① 构造器注入循环;② prototype 作用域循环;③ 一个 Bean 被 @Async 代理等“在初始化后阶段才包装”的处理,而它的原始版本已经被别的 Bean 提前引用,会报 “has been injected into other beans in its raw version as part of a circular reference, but has eventually been wrapped”;④ Boot 2.6+ 默认关闭。

🌳 高级:怎么消除而不是容忍循环依赖

  • 根因是职责划分问题:A、B 互相调用,通常意味着有第三个概念没被抽出来。做法:抽出公共服务 C 让 A、B 都依赖 C;或用事件(ApplicationEvent)解耦:A 发布事件,B 监听,去掉直接依赖;或把其中一个依赖改为 ObjectProvider<B> / @Lazy 延迟获取(治标)。
  • 项目治理:用 ArchUnit 或模块化(多 module)在编译期禁止循环依赖,比运行时报错更早。
  • 面试加分:能解释“为什么 Spring 要在 Boot 2.6 收紧默认”——循环引用掩盖设计问题,还引发代理不一致、初始化顺序难以推理等隐患。

四、AOP 与动态代理

🌱 初级:AOP 概念

  • AOP(面向切面)把日志、权限、事务、限流这类横切关注点从业务代码里抽出来。术语:切面 Aspect、连接点 JoinPoint(Spring 里只支持方法执行)、切点 Pointcut、通知 Advice(前置/后置/返回/异常/环绕)、织入 Weaving。
  • Spring AOP 基于运行时动态代理;AspectJ 是编译期/加载期织入,能力更强(能切构造器、字段、自调用)。

🌿 中级:JDK 代理 vs CGLIB,与自调用

  • JDK 动态代理:目标实现了接口时使用,基于 java.lang.reflect.Proxy,代理的是接口。CGLIB:没有接口时使用,生成目标类的子类。官方文档明确了 CGLIB 的限制:final 类不能代理,final/private 方法不能被增强。
  • Spring Boot 从 2.0 起 spring.aop.proxy-target-class 默认为 true,即默认用 CGLIB;纯 Spring Framework 则是“有接口用 JDK,没接口用 CGLIB”。所以“Boot 项目里注入 ServiceImpl 类型也不报错”,而在纯 Spring 且用 JDK 代理时会报类型不匹配。
  • 自调用问题(官方文档明确):this.bar() 直接调用目标对象,不经过代理,所以切面/事务/异步/缓存注解全部失效。解决:① 重构,把被调方法抽到另一个 Bean(首选);② 注入自身代理(@Autowired private Self self,官方也列为一种做法);③ AopContext.currentProxy()(需要 exposeProxy,官方不推荐,耦合 Spring AOP);④ 改用 AspectJ 织入模式(AdviceMode.ASPECTJ)。
  • 通知执行顺序(同一切面,Spring 5.2.7+ 的规定):Around 前 → Before → 目标方法 → AfterReturning/AfterThrowing → After → Around 后。多个切面用 @Order 控制,值小的在外层。
@Aspect @Component
public class CostAspect {
    @Around("@annotation(com.demo.Timed)")            // 切点:带 @Timed 的方法
    public Object time(ProceedingJoinPoint pjp) throws Throwable {
        long t0 = System.nanoTime();
        try { return pjp.proceed(); }
        finally { log.info("{} cost {} ms", pjp.getSignature(), (System.nanoTime() - t0) / 1_000_000); }
    }
}

🌳 高级:AOP 在工程里的边界

  • 切面里不要吞异常、不要改变方法语义;高频调用的切面注意性能(反射 + 多层拦截器链)。日志切面打印大对象、序列化入参会拖慢整个接口,必须限长。
  • 把“通用能力”做成注解 + 切面(限流、幂等、审计日志、数据权限)是中台常见玩法,但注解语义一定要在文档和 Code Review 里讲清楚,否则新人不知道方法背后有多少隐形逻辑。自调用失效要在规范里写明。
  • 调试技巧:AopUtils.isAopProxy(bean)、打印 bean.getClass()(看到 $$SpringCGLIB$$ 就是 CGLIB 代理)确认对象是不是代理。

五、事务:传播行为与失效场景

🌱 初级:@Transactional 基本用法

  • 声明式事务:方法上加 @Transactional,Spring 通过 AOP 代理在方法前后开启/提交/回滚。需要事务管理器(Boot 自动配置 DataSourceTransactionManager)。
  • 默认配置(官方文档):传播 REQUIRED、隔离级别 DEFAULT(用数据库默认,MySQL InnoDB 为 REPEATABLE READ)、读写事务、无超时、只对 RuntimeException 和 Error 回滚,受检异常默认不回滚。
  • ACID:原子性(undo log)、一致性、隔离性(锁 + MVCC)、持久性(redo log)。

🌿 中级:七种传播行为与常见失效

传播行为语义典型用途
REQUIRED(默认)有事务就加入,没有就新建绝大多数业务
REQUIRES_NEW总是新开独立事务,挂起外层审计日志、失败也要落库的记录
NESTED外层事务内用保存点(savepoint),内层可单独回滚到保存点可选步骤失败不影响主流程(仅 JDBC 事务)
SUPPORTS有则加入,无则非事务执行只读查询
NOT_SUPPORTED总以非事务执行,挂起已有事务长耗时、无需事务的调用
MANDATORY必须在事务内调用,否则抛异常强约束的内部方法
NEVER必须非事务,有事务则抛异常极少用

官方文档强调三点:REQUIRED 共享同一个物理事务,内层标记 rollback-only 而外层不知情时,外层提交会抛 UnexpectedRollbackException;REQUIRES_NEW 独立物理事务,会占用第二个数据库连接,连接池不够大会耗尽(外层持有一个连接等待内层拿另一个 → 池被占满形成死锁式卡死);NESTED 是同一物理事务上的保存点。

事务失效的高频场景(背这张清单):

  1. 自调用:同类内 this.xxx() 不经代理(见上文)。
  2. 方法可见性:接口代理/JDK 代理下方法必须是 public;Spring 6.0 起基于类的代理(CGLIB)允许 protected 与包可见,但 private / final / static 依然不行。
  3. 异常被 catch 吞掉:方法内 try { … } catch (Exception e) { log } 没抛出去,代理看不到异常,照常提交。
  4. 受检异常没配 rollbackFor:抛 IOException/自定义受检异常默认不回滚,要 @Transactional(rollbackFor = Exception.class)。(Spring 6.2 起可全局配置 rollbackOn = ALL_EXCEPTIONS,官方最佳实践也建议这么统一。)
  5. Bean 不被 Spring 管理:自己 new 出来的对象、没有 @Service 的类。
  6. 多线程:事务绑定在当前线程(ThreadLocal),新开线程/@Async/线程池里的操作不在同一事务中。
  7. 数据库不支持:MyISAM 引擎无事务;多数据源没指定正确的事务管理器(@Transactional("order"))。
  8. 传播行为用错:比如内层用了 NOT_SUPPORTED、REQUIRES_NEW 而以为会一起回滚。
  9. 在 @PostConstruct 里用事务:此时代理可能尚未就绪(官方最佳实践里也建议避免)。
@Service
public class OrderService {
    @Autowired private OrderService self;                 // 自注入代理,解决自调用(也可拆到另一个 Bean)

    public void createBatch(List<Order> orders) {
        for (Order o : orders) {
            try { self.createOne(o); }                     // 走代理,每单独立事务
            catch (Exception e) { log.warn("order fail {}", o.getNo(), e); }
        }
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void createOne(Order o) { /* 落库 */ }
}

关于 readOnly = true:它是一个提示,Spring 会设置连接为只读、Hibernate 会跳过脏检查,可以为读写分离路由提供依据,但并不能阻止你在里面写(取决于驱动/数据库),也不是“加了就更快”的魔法。

🌳 高级:大事务、事务与消息、分布式事务

  • 大事务的危害:长时间占用连接、锁持有时间长、主从延迟、回滚代价高。治理:事务里不要有 RPC/HTTP 调用、不要有耗时的计算与文件 IO;查询放事务外;批量操作分批提交;用编程式事务 TransactionTemplate 精确控制范围。
  • “事务提交后再发消息”:在事务里直接发 MQ,可能出现“消息发出去了但事务回滚”或“事务提交了消息发送失败”。做法:@TransactionalEventListener(phase = AFTER_COMMIT) 在提交后再发(仍可能发送失败);更稳的是本地消息表/事务发件箱(Outbox)——业务数据与消息记录在同一本地事务里落库,后台任务扫描投递,消费端做幂等;或用 RocketMQ 事务消息。
  • 跨服务一致性:先想能否避免分布式事务(合并服务、同库、最终一致);确实需要则按场景选:TCC(资源预留,业务侵入大)、Saga(长流程,补偿)、Seata AT(对代码侵入小,依赖全局锁和 undo log,有性能与隔离代价)、本地消息表 + 幂等(最常用、最稳)。共同前提:接口幂等、状态机、对账兜底。
  • 幂等设计:唯一索引/去重表、状态机前置状态校验(update … where status = 'INIT')、业务单号 + Redis SETNX、Token 机制。重试是微服务的常态,幂等是它的另一半。

六、Spring Boot 自动装配

🌱 初级:Boot 到底省了什么

  • Starter 依赖聚合 + 自动配置 + 内嵌容器 + 约定优于配置。@SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan。
  • 配置:application.yml/properties;多环境 profile;命令行参数、环境变量优先级高于配置文件(外部化配置有一套官方定义的优先级顺序)。

🌿 中级:自动装配原理

  1. @EnableAutoConfiguration 通过 @Import(AutoConfigurationImportSelector.class) 触发。
  2. Selector 读取所有 jar 中的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件(官方文档写明的注册位置;Boot 2.7 引入、3.0 起只认它,早期版本是 spring.factories 里的 EnableAutoConfiguration key)拿到候选自动配置类全限定名。
  3. 经过去重、排除(exclude)、条件过滤:@ConditionalOnClass(类路径存在某类)、@ConditionalOnMissingBean(用户没自己定义才生效——这就是“非侵入、可替换”的关键)、@ConditionalOnProperty、@ConditionalOnWebApplication 等。满足条件的才注册其中的 @Bean。
  4. 属性绑定:@ConfigurationProperties 把配置文件绑到 Properties 类,自动配置类通过 @EnableConfigurationProperties 引入。

官方文档给了两个实用技巧:启动加 --debug 可输出“条件评估报告”,告诉你哪些自动配置生效、哪些没生效及原因;要禁用某个自动配置可用 @SpringBootApplication(exclude = …)、excludeName 或属性 spring.autoconfigure.exclude。自己添加一个 DataSource Bean,默认的嵌入式数据源配置就会退让——正是 @ConditionalOnMissingBean 的效果。

// 自定义 Starter 的骨架
@AutoConfiguration
@ConditionalOnClass(MyClient.class)
@EnableConfigurationProperties(MyProperties.class)
public class MyClientAutoConfiguration {
    @Bean @ConditionalOnMissingBean
    public MyClient myClient(MyProperties p) { return new MyClient(p.getEndpoint(), p.getTimeout()); }
}
// 并在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中
// 写入 com.demo.MyClientAutoConfiguration(一行一个类名)

🌳 高级:升级 Boot 3 与配置治理

  • Boot 3 的硬门槛:JDK 17+;javax.* → jakarta.* 包名迁移(Servlet、JPA、Validation 全受影响);spring.factories 中自动配置注册方式改为 .imports;部分依赖版本要同步升级(Hibernate 6、Spring Security 6)。建议先升到 2.7.x 清掉所有弃用告警,再跳 3.x。
  • 配置治理:敏感配置(密钥、密码)不进代码库,走配置中心/密钥管理;配置变更要有审计与灰度;@RefreshScope 动态刷新的 Bean 会被重建,持有状态的 Bean(连接池、线程池)要谨慎,别把它们标成 RefreshScope。
  • Actuator:暴露 health/metrics/prometheus,K8s 用 liveness/readiness 分离;生产只暴露必要端点并加鉴权,heapdump、env 这类端点默认不要对外。

七、微服务:注册中心与配置中心

🌱 初级:为什么需要,以及是什么

  • 注册中心:服务启动时注册地址,消费者拉取/订阅实例列表,实现服务发现,并通过心跳/健康检查摘除故障实例。常见:Nacos、Eureka、Consul、ZooKeeper;K8s 环境里也可直接用 Service + DNS。
  • 配置中心:集中管理各环境配置,支持动态推送,改配置不用重新发布。Nacos 官方定位就是“动态服务发现 + 动态配置管理 + 服务管理”,并提供配置版本追踪、灰度发布、一键回滚。
  • 负载均衡:客户端负载(Spring Cloud LoadBalancer、Dubbo 内置)vs 服务端负载(Nginx、网关、K8s Service)。

🌿 中级:CAP 取舍与注册中心选型

组件一致性取向特点
EurekaAP节点对等,自我保护机制(网络分区时宁可保留可能失效的实例也不大规模摘除),已进入维护状态
ZooKeeperCPZAB 强一致,选主期间不可用;临时节点做会话保活;作为注册中心偏重
NacosAP + CP 可切换临时实例走 AP(Distro 协议,心跳保活),持久实例走 CP(Raft);同时提供配置中心
ConsulCPRaft,内置健康检查与 KV,跨数据中心

为什么注册中心更倾向 AP?服务发现场景下“返回一个稍旧的实例列表”比“整个注册中心不可用”代价小得多;调用方有重试、熔断与本地缓存兜底,可以容忍短时不一致。这也是面试题“注册中心挂了会怎样”的答案:调用方本地缓存了实例列表,已有调用不受影响,只是新实例/下线感知会延迟——前提是客户端确实有缓存(要验证)。

  • 心跳与摘除:客户端定时发心跳;超过阈值未收到,注册中心标记不健康/摘除;优雅下线时要先主动注销再停服,否则消费者还会往已停的实例发请求,造成发布期间的 5xx。
  • 配置动态刷新:Nacos 客户端长轮询监听配置变化;Spring Cloud 中用 @RefreshScope 或 @ConfigurationProperties 绑定的 Bean 刷新。本地缓存与容灾:客户端会把配置/实例快照缓存到本地文件,配置中心宕机时用快照启动。

🌳 高级:注册/配置中心的治理

  • 大规模下的问题:实例数上万时的推送风暴、心跳量、变更抖动;对策:服务分组/命名空间隔离、控制心跳间隔、变更限速、客户端本地缓存。
  • 配置变更是线上事故第一来源之一:必须有版本、审计、灰度(先推给少数实例)、一键回滚;关键配置(限流阈值、开关)变更后要有监控验证。配置项要有 schema 校验,避免“写错一个符号,全集群重启失败”。
  • 注册中心自身高可用:集群部署、跨机房、客户端容灾缓存、注册中心不可用时“不摘除 + 保持本地缓存”的降级策略。

八、网关

🌱 初级:网关做什么

网关是所有外部流量的统一入口:路由转发、鉴权认证、限流熔断、协议转换、日志与监控、灰度路由。Spring Cloud Gateway(基于 WebFlux/Netty 的响应式网关)的三个核心概念(官方文档):Route(路由)= 目标 URI + 一组断言 + 一组过滤器;Predicate(断言)决定请求是否匹配该路由(路径、Host、Header、Method 等);Filter(过滤器)在请求/响应前后做横切处理(改写路径、加请求头、限流 RequestRateLimiter、重试、熔断 CircuitBreaker、令牌中继等,官方内置 40+ 种)。

🌿 中级:网关的设计要点

  • Gateway vs Zuul 1.x:Zuul 1 是同步阻塞模型(一请求一线程),Gateway 是非阻塞响应式,吞吐更高。在响应式网关里不能写阻塞代码(同步查库、Thread.sleep、阻塞 HTTP 调用),否则会卡住少量的事件循环线程,整个网关一起卡死——这是最常见的坑;不得不阻塞的调用要切到 boundedElastic 调度器。
  • 鉴权位置:网关做统一认证(校验 JWT/Session、验签、黑白名单、防重放),把用户身份透传给后端(Header);后端仍要做授权校验,不能因为“有网关”就假设内网绝对安全。
  • 限流:网关层做全局与按维度(用户/IP/接口/租户)限流,保护后端;RequestRateLimiter 需要 KeyResolver,内置基于 Redis 的令牌桶实现。
  • 网关自身的可用性:无状态、多实例、前置 LB;超时要设置清楚(连接超时、响应超时),否则慢后端会占满网关连接。

🌳 高级:网关与流量治理

  • 网关不该塞业务逻辑:聚合、编排、复杂转换交给 BFF/聚合服务,网关保持“薄”,否则变成新的单体瓶颈。
  • 灰度与多环境路由:网关按 Header/Cookie/用户标签/权重把流量导向新版本,配合下面“灰度发布”一章的全链路灰度。
  • 可观测:网关是产生 traceId 的天然起点;记录路由、上游耗时、状态码分布;对 5xx、超时、被限流的比例设置告警。

九、熔断、限流、降级

🌱 初级:三个词的区别

  • 限流:入口处限制请求速率,保护自己不被打垮(“我扛不住”)。
  • 熔断:调用下游失败率/慢调用比例过高时,暂时停止调用,快速失败(“对方不行了,别再等它”)。
  • 降级:主功能不可用时返回兜底结果或关闭非核心功能(“保主链路,牺牲体验”)。
  • 还有超时、重试、隔离(舱壁)。它们合起来叫“韧性(Resilience)”。

🌿 中级:熔断器状态机与限流算法

熔断器三态(以 Resilience4j 为例,官方文档):CLOSED(正常放行并统计)→ 失败率或慢调用率达到阈值 → OPEN(拒绝请求,抛 CallNotPermittedException)→ 等待一段时间 → HALF_OPEN(放行少量探测请求)→ 成功则回 CLOSED,失败则回 OPEN。默认值(官方文档):失败率阈值 50%、滑动窗口 100 次、最小调用数 100、OPEN 等待 60 秒、半开允许 10 次。“最小调用数”很关键:流量太小时统计不稳定,不会触发熔断。

限流算法:

算法原理特点
固定窗口每个时间窗口计数,超过则拒绝简单;窗口边界可能瞬时放行 2 倍流量
滑动窗口按更细的时间片滑动统计更平滑,内存与计算略高
漏桶请求入桶,以恒定速率流出输出平滑,不允许突发
令牌桶以恒定速率放令牌,请求拿到令牌才通过,桶可累积允许一定突发,最常用(Guava RateLimiter、Gateway 均基于此)
  • Sentinel vs Hystrix(Sentinel 官方对比):Hystrix 用线程池隔离,需要预分配线程、带来排队与上下文切换开销;Sentinel 用并发线程数(信号量)限制,加上基于响应时间的熔断,开销更小,同时提供流控、熔断降级、系统自适应保护(按 CPU/负载)与控制台。Hystrix 已停止新功能开发(维护模式),新项目选 Sentinel 或 Resilience4j。
  • 重试的三个原则:只重试幂等操作;加退避(指数 + 抖动);限制次数和总时长。否则重试会放大流量,把已经不健康的下游彻底压垮(重试风暴)。多层各自重试还会相乘:3 层 × 3 次 = 27 倍。
  • 超时要逐层递减:网关超时 > 服务 A 调 B 的超时 > B 调 C 的超时,否则上游已放弃,下游还在白干。

🌳 高级:雪崩治理与降级预案

  • 服务雪崩的链条:某下游变慢 → 调用方线程被占满(请求堆积)→ 调用方变慢 → 上游再被拖垮。核心手段:超时 + 舱壁隔离(每个下游独立线程池/信号量)+ 熔断 + 限流 + 降级,缺一环都可能漏。
  • 降级预案要提前演练:核心与非核心分级(支付下单是核心,评论/推荐是非核心);每个非核心依赖都要有降级开关与兜底值(缓存旧值、默认值、空列表);开关放配置中心,可一键切换。大促前做故障演练(注入延迟/错误),验证熔断阈值是否合理。
  • 阈值怎么定:不是拍脑袋——基于压测得到的单机容量,留 20%–30% 余量;限流阈值随实例数变化(单机限流 vs 集群限流,后者需 Redis 或 Sentinel 集群流控)。
  • 保护自己也要保护别人:被限流/熔断时要返回明确的错误码,让调用方区分“业务失败”与“被保护”,否则调用方会盲目重试。

十、Feign 与 Dubbo(服务间调用)

🌱 初级:两者是什么

  • OpenFeign:声明式 HTTP 客户端,写接口 + 注解,底层生成代理发 HTTP 请求(JSON),与 Spring Cloud 生态(负载均衡、熔断)集成。跨语言、易调试(curl 可测)。
  • Dubbo:高性能 RPC 框架,接口即服务,默认二进制序列化。Dubbo 3 推出 Triple 协议(兼容 gRPC,基于 HTTP/2),支持应用级服务发现。适合内部高吞吐、低延迟的 Java 服务间调用。

🌿 中级:关键机制

  • Feign 调用链路:接口 → JDK 动态代理(FeignInvocationHandler)→ 解析注解生成请求模板 → 负载均衡选实例 → HTTP 客户端(HttpURLConnection/Apache HttpClient/OkHttp)→ 解码。超时配置务必显式设置:Spring Cloud OpenFeign 默认连接超时约 10 秒、读超时约 60 秒,生产环境这个值通常太长,会让慢下游长时间占住线程;同时注意 Feign 默认重试策略与你的业务幂等性是否匹配。
  • Feign 常见坑:GET 请求带对象参数需要 @SpringQueryMap;请求头/用户上下文不会自动透传,需要 RequestInterceptor 处理;异常解码要自定义 ErrorDecoder,否则拿到的是笼统的 FeignException;调用方线程池被慢接口占满要配合熔断与超时。
  • Dubbo 的核心:Provider / Consumer / Registry / Monitor;集群容错策略:Failover(默认,失败重试其他节点,默认重试 2 次即最多调用 3 次)、Failfast、Failsafe、Failback、Forking、Broadcast;负载均衡:随机(默认,加权)、轮询、最少活跃、一致性哈希。写操作别用默认的 Failover(会重复执行),设置 retries=0 或做幂等。
  • 序列化与兼容性:接口参数类加字段、删字段时的前后兼容;Hessian/Protobuf 各有规则;接口发布用独立 API 包,版本号与灰度配合。
维度OpenFeign(HTTP/JSON)Dubbo(RPC)
协议与性能HTTP/1.1 为主,文本 JSON,较重二进制,长连接复用,性能更高(Triple 基于 HTTP/2)
跨语言天然Triple/gRPC 可跨语言,传统协议偏 Java
治理能力依赖 Spring Cloud 组件内置负载均衡、容错、路由、限流
调试简单需要专用工具/泛化调用
适用对外/异构、团队熟 Spring 全家桶内部核心链路、高吞吐

🌳 高级:调用治理与选型

  • 选型不是信仰:团队规模、语言栈、性能需求、运维能力决定。K8s + Service Mesh(Istio 等)把重试、熔断、路由、mTLS 下沉到 sidecar,业务代码更干净,代价是运维复杂度和额外一跳延迟。
  • 契约管理:接口变更遵循向后兼容(只加字段不删不改语义;新增必填要有默认值);用 API 契约(OpenAPI/Proto)与契约测试;废弃走“标记 Deprecated → 观察调用量 → 下线”。
  • 同步调用的扇出:一个请求触发 N 个下游同步调用,可用性 ≈ 各下游可用性的乘积(10 个 99.9% 的依赖,整体约 99.0%);用并行 + 超时 + 降级 + 缓存降低耦合,非核心依赖异步化。

十一、链路追踪与可观测

🌱 初级:Trace 与 Span

一次请求跨多个服务,怎么知道慢在哪?分布式链路追踪:一个请求对应一个 Trace(全局 traceId),每个服务/方法内的一段工作是一个 Span(有起止时间、属性、事件、父子关系、状态、类型 Client/Server/Producer/Consumer 等,OpenTelemetry 官方概念)。

🌿 中级:上下文传播与落地

  • 上下文传播(Context Propagation):官方文档称之为分布式追踪的关键——把 traceId/spanId 通过 HTTP Header(W3C traceparent)、RPC 附件、MQ 消息属性传给下游,下游继续同一 Trace。断链的常见原因:线程池/异步没有传递上下文、MQ 没携带、网关没透传、定时任务没有起点。
  • 与日志关联:把 traceId 放进 SLF4J MDC 并输出到每行日志,出问题时用 traceId 一键拉出整条链路的日志。MDC 是 ThreadLocal,线程池中同样需要透传(见并发篇 TTL)。
  • 采样:全量采集成本高;常用头部采样(按固定比例)+ 错误/慢请求必采(尾部采样)。要保证出错和高耗时链路不被采样丢掉。
  • 选型:SkyWalking(Java Agent 无侵入、国内常用)、OpenTelemetry(厂商中立标准,Trace/Metrics/Logs 统一)、Zipkin/Jaeger。趋势是以 OpenTelemetry 为采集标准,后端可替换。

🌳 高级:可观测体系的落地

  • 三大支柱:Metrics(指标,看趋势与告警)、Logs(日志,看细节)、Traces(链路,看路径与耗时);用 traceId 把三者串起来,形成“告警 → 指标定位服务 → 链路定位慢节点 → 日志看细节”的排查路径。
  • 黄金指标:延迟、流量、错误、饱和度(Google SRE)。给每个服务和每个下游依赖都建这四条基线与告警,并关注 P99 而不是平均值。
  • Agent 带来的开销与风险要评估(内存、启动时间、字节码增强兼容性);灰度上线探针,别全量一起换。

十二、灰度发布与流量治理

🌱 初级:发布策略对比

策略做法特点
滚动发布逐批替换旧实例资源省;新旧版本同时存在,回滚慢
蓝绿发布准备完整的新环境,流量一次切换回滚快;资源翻倍
灰度/金丝雀(Canary)先让少量流量/特定用户走新版本,逐步放量风险可控,需要流量染色与观测

🌿 中级:灰度怎么实现

  • 流量分流方式:按权重(5% → 20% → 50% → 100%)、按用户/租户/Header 标记(内部员工、白名单商户先用)、按地域。实现位置:LB/Ingress(Nginx、K8s Ingress canary 注解)、网关、服务网格(Istio VirtualService)、注册中心元数据标签。
  • 注册中心 + 元数据:新版本实例注册时带 version=gray 元数据,消费端负载均衡时按请求标记过滤实例。Nacos 支持实例元数据与权重调整,配置也有灰度(Beta)发布。
  • 全链路灰度:一个请求进入灰度环境后,标记要沿整条调用链传递(Header/RPC 上下文/MQ 属性),链路上每个服务优先路由到同标记的实例,没有则回落到基线版本。关键点:标记透传不能断(线程池、异步、MQ 消费者都是断点),这和链路追踪的上下文传播是同一类问题,通常复用同一套机制。
  • 发布中的配套:健康检查通过才接流量(readiness);优雅上下线;预热(JIT 预热/连接池预热,避免新实例一上线就被打满);发布期间监控核心指标(错误率、延迟、业务成功率),设置自动回滚门槛。

🌳 高级:数据与兼容性——灰度最难的部分

  • 数据库变更要“向前兼容”:新老版本会同时读写同一份库。采用 Expand / Contract 模式:① 先加列/表(新旧代码都能跑)→ ② 发新版本双写/兼容读 → ③ 全量后回填、切换读 → ④ 最后再删旧列。永远不要在同一次发布里既改表结构又删旧列,否则回滚就不可能。
  • 消息与接口兼容:新版本生产的消息,旧版本消费者必须能解析(字段只增不减);接口变更用版本号或兼容字段。
  • 状态与缓存:新旧版本缓存 key/序列化格式不同会互相污染;缓存 key 带版本前缀,或让格式向前兼容。
  • 发布的度量与决策:灰度不是“放一点看看”,而是事先定义好观测指标与放量节奏(如 5% 观察 15 分钟,错误率不超过基线 +0.1%、P99 不劣化 10%,则放到 20%),达标自动放量,不达标自动回滚。这套门槛写下来,比“感觉没问题”可靠得多。
  • 加分项:提到“开关(Feature Flag)与发布解耦”——代码先随版本上线但默认关闭,再用开关按租户灰度打开,出问题秒级关闭而不用回滚发布。

十三、高频追问速查表

问题一句话答案出处/延伸
IoC 与 DIIoC 是思想(容器管对象),DI 是手段(注入依赖)官方推荐构造器注入
Bean 生命周期实例化 → 属性填充 → Aware → BPP 前 → 初始化 → BPP 后(AOP 代理) → 使用 → 销毁factory-nature 文档
初始化回调顺序@PostConstruct → afterPropertiesSet → init-method官方文档
BPP 与 BFPP 区别BFPP 改 BeanDefinition(实例化前),BPP 改 Bean 实例(初始化前后)ConfigurationClassPostProcessor
三级缓存分别是什么成品池 / 早期引用 / ObjectFactoryDefaultSingletonBeanRegistry
为什么要三级而不是两级第三级工厂延迟决定是否提前生成代理,保证代理与原对象不出现两份getEarlyBeanReference
哪些循环依赖解不了构造器注入、prototype、被 @Async 等后置代理的原始对象;Boot 2.6+ 默认禁止allow-circular-references
JDK 与 CGLIB 代理区别接口代理 vs 子类代理;CGLIB 不能代理 final/private;Boot 默认 CGLIBAOP proxying 文档
@Transactional 默认回滚RuntimeException 与 Error;受检异常不回滚需 rollbackFor
事务失效场景自调用、非 public(老版本)、异常被吞、受检异常、非 Spring 管理、多线程、引擎不支持本文第五章
REQUIRES_NEW 风险独立事务占第二个连接,池小可耗尽;不受外层回滚影响官方 tx-propagation
NESTED 与 REQUIRES_NEWNESTED 同物理事务的保存点;REQUIRES_NEW 全新物理事务JDBC savepoint
自动装配原理Selector 读 AutoConfiguration.imports → 条件过滤 → 注册 BeanBoot auto-configuration 文档
如何排查某个自动配置没生效启动加 --debug 看条件评估报告ConditionEvaluationReport
注册中心 AP 还是 CP倾向 AP(可用优先);Nacos 临时实例 AP、持久实例 CPNacos / Eureka / ZK
Gateway 核心概念Route + Predicate + Filter,响应式,勿写阻塞代码Gateway 文档
熔断三态CLOSED → OPEN → HALF_OPEN;默认失败率 50%、窗口 100、等待 60sResilience4j 文档
令牌桶与漏桶令牌桶允许突发,漏桶输出恒定Guava / Gateway
Sentinel 与 HystrixSentinel 信号量并发数限制+慢调用熔断;Hystrix 线程池隔离,已维护模式Sentinel 介绍
Dubbo 默认容错Failover,默认重试 2 次;写操作要幂等或 retries=0Dubbo 集群容错
Feign 超时坑默认超时偏长,务必显式配置并与熔断、重试协调OpenFeign 文档
链路追踪断链原因线程池/异步/MQ/网关未传递上下文OpenTelemetry 上下文传播
灰度发布怎么做权重/标记分流 + 实例元数据 + 全链路标记透传 + 指标门槛自动回滚Expand/Contract 处理库表

一句话备忘:Spring 记“代理没走等于没用:自调用、private、异常吞了、多线程,事务都失效;三级缓存的第三级是为了延迟代理”;Boot 记“imports 文件 + Conditional,排查用 --debug”;微服务记“注册中心倾向 AP、网关薄而不阻塞、超时逐层递减、重试只对幂等、熔断三态、灰度靠标记透传与库表向前兼容”。

写完这篇我最大的感受是:Spring 的八股背得再熟,也不如亲手遇到一次“事务没回滚”来得印象深;微服务的治理概念再多,最后落到三件事——别让慢拖垮快、别让重试放大故障、别让发布把数据搞坏。把这三条讲出真实的案例,比罗列十个组件名字强得多。

参考资料

— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人