“你们的 @Transactional 为什么没回滚?”——这是我见过最能让一个组会瞬间安静的问题。回滚没生效,钱多扣了、库存少了、对账差了,大家一起对着代码盯了两小时,最后发现是同一个类里 this.save() 调了另一个带事务注解的方法。代理没走,注解等于注释。
Spring 和微服务是后端面试里“最像开放题、其实全是细节”的一块:面试官不想听你背 IoC 的定义,他想听你说出“三级缓存里为什么第三级放的是工厂而不是对象”。怎么用:每个专题分 🌱 初级(是什么/怎么用)→ 🌿 中级(为什么/怎么选/常见坑)→ 🌳 高级(怎么落地/出事怎么办)三层,每层给标准回答要点、追问链、加分项。版本相关内容我对照了 Spring Framework、Spring Boot 与各组件官方文档(文末有出处),文中会标注版本。
一、知识地图
二、IoC、Bean 生命周期
🌱 初级:IoC 与 DI
标准回答要点:
- IoC(控制反转):把对象的创建与依赖关系的装配交给容器,而不是自己
new;DI(依赖注入)是实现方式:构造器注入、Setter 注入、字段注入。 - 官方推荐构造器注入(Spring 文档):得到不可变对象、保证必需依赖非空、组件返回时是完全初始化状态;可选依赖用 Setter 注入。字段注入(
@Autowired打在字段上)便于偷懒,但难以单测、隐藏依赖、无法声明 final,团队规范里往往不推荐。 @Autowiredvs@Resource:前者 Spring 注解,默认按类型,同类型多个用@Qualifier或字段名匹配;后者 JSR-250,默认按名称再回退类型。- Bean 作用域:singleton(默认,容器内单例)、prototype(每次获取新建)、request、session 等。单例 Bean 不等于线程安全——它只是唯一,有可变成员变量就要自己保证。
- BeanFactory vs ApplicationContext:前者是基础容器(延迟加载);后者是它的扩展,加了事件、国际化、资源加载、AOP 集成,默认启动时预实例化所有单例,把配置错误尽早暴露在启动阶段。
🌿 中级:Bean 生命周期
这条链一定要能一口气说完,并知道每一步是谁干的:
- 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 | 提前暴露的“早期引用”(半成品,可能是代理) |
| 三级 | singletonFactories | ObjectFactory:需要时调用 getEarlyBeanReference 产出早期引用 |
流程(A、B 互相 Setter 依赖):
- 创建 A:实例化(构造器执行,还是个空壳)→ 把 A 的 ObjectFactory 放进三级缓存 → 开始属性填充,发现需要 B;
- 创建 B:实例化 → B 的工厂放入三级缓存 → 填充属性,需要 A;
- B 去找 A:一、二级没有,三级缓存有——调用 A 的工厂拿到早期引用(如果 A 需要被 AOP 代理,这里会提前生成代理对象),放入二级缓存并移除三级,注入到 B;
- 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 是同一物理事务上的保存点。
事务失效的高频场景(背这张清单):
- 自调用:同类内
this.xxx()不经代理(见上文)。 - 方法可见性:接口代理/JDK 代理下方法必须是 public;Spring 6.0 起基于类的代理(CGLIB)允许 protected 与包可见,但
private/final/static依然不行。 - 异常被 catch 吞掉:方法内
try { … } catch (Exception e) { log }没抛出去,代理看不到异常,照常提交。 - 受检异常没配 rollbackFor:抛
IOException/自定义受检异常默认不回滚,要@Transactional(rollbackFor = Exception.class)。(Spring 6.2 起可全局配置rollbackOn = ALL_EXCEPTIONS,官方最佳实践也建议这么统一。) - Bean 不被 Spring 管理:自己
new出来的对象、没有@Service的类。 - 多线程:事务绑定在当前线程(ThreadLocal),新开线程/
@Async/线程池里的操作不在同一事务中。 - 数据库不支持:MyISAM 引擎无事务;多数据源没指定正确的事务管理器(
@Transactional("order"))。 - 传播行为用错:比如内层用了
NOT_SUPPORTED、REQUIRES_NEW而以为会一起回滚。 - 在
@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;命令行参数、环境变量优先级高于配置文件(外部化配置有一套官方定义的优先级顺序)。
🌿 中级:自动装配原理
@EnableAutoConfiguration通过@Import(AutoConfigurationImportSelector.class)触发。- Selector 读取所有 jar 中的
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(官方文档写明的注册位置;Boot 2.7 引入、3.0 起只认它,早期版本是spring.factories里的EnableAutoConfigurationkey)拿到候选自动配置类全限定名。 - 经过去重、排除(
exclude)、条件过滤:@ConditionalOnClass(类路径存在某类)、@ConditionalOnMissingBean(用户没自己定义才生效——这就是“非侵入、可替换”的关键)、@ConditionalOnProperty、@ConditionalOnWebApplication等。满足条件的才注册其中的@Bean。 - 属性绑定:
@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 取舍与注册中心选型
| 组件 | 一致性取向 | 特点 |
|---|---|---|
| Eureka | AP | 节点对等,自我保护机制(网络分区时宁可保留可能失效的实例也不大规模摘除),已进入维护状态 |
| ZooKeeper | CP | ZAB 强一致,选主期间不可用;临时节点做会话保活;作为注册中心偏重 |
| Nacos | AP + CP 可切换 | 临时实例走 AP(Distro 协议,心跳保活),持久实例走 CP(Raft);同时提供配置中心 |
| Consul | CP | Raft,内置健康检查与 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放进 SLF4JMDC并输出到每行日志,出问题时用 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 与 DI | IoC 是思想(容器管对象),DI 是手段(注入依赖) | 官方推荐构造器注入 |
| Bean 生命周期 | 实例化 → 属性填充 → Aware → BPP 前 → 初始化 → BPP 后(AOP 代理) → 使用 → 销毁 | factory-nature 文档 |
| 初始化回调顺序 | @PostConstruct → afterPropertiesSet → init-method | 官方文档 |
| BPP 与 BFPP 区别 | BFPP 改 BeanDefinition(实例化前),BPP 改 Bean 实例(初始化前后) | ConfigurationClassPostProcessor |
| 三级缓存分别是什么 | 成品池 / 早期引用 / ObjectFactory | DefaultSingletonBeanRegistry |
| 为什么要三级而不是两级 | 第三级工厂延迟决定是否提前生成代理,保证代理与原对象不出现两份 | getEarlyBeanReference |
| 哪些循环依赖解不了 | 构造器注入、prototype、被 @Async 等后置代理的原始对象;Boot 2.6+ 默认禁止 | allow-circular-references |
| JDK 与 CGLIB 代理区别 | 接口代理 vs 子类代理;CGLIB 不能代理 final/private;Boot 默认 CGLIB | AOP proxying 文档 |
| @Transactional 默认回滚 | RuntimeException 与 Error;受检异常不回滚 | 需 rollbackFor |
| 事务失效场景 | 自调用、非 public(老版本)、异常被吞、受检异常、非 Spring 管理、多线程、引擎不支持 | 本文第五章 |
| REQUIRES_NEW 风险 | 独立事务占第二个连接,池小可耗尽;不受外层回滚影响 | 官方 tx-propagation |
| NESTED 与 REQUIRES_NEW | NESTED 同物理事务的保存点;REQUIRES_NEW 全新物理事务 | JDBC savepoint |
| 自动装配原理 | Selector 读 AutoConfiguration.imports → 条件过滤 → 注册 Bean | Boot auto-configuration 文档 |
| 如何排查某个自动配置没生效 | 启动加 --debug 看条件评估报告 | ConditionEvaluationReport |
| 注册中心 AP 还是 CP | 倾向 AP(可用优先);Nacos 临时实例 AP、持久实例 CP | Nacos / Eureka / ZK |
| Gateway 核心概念 | Route + Predicate + Filter,响应式,勿写阻塞代码 | Gateway 文档 |
| 熔断三态 | CLOSED → OPEN → HALF_OPEN;默认失败率 50%、窗口 100、等待 60s | Resilience4j 文档 |
| 令牌桶与漏桶 | 令牌桶允许突发,漏桶输出恒定 | Guava / Gateway |
| Sentinel 与 Hystrix | Sentinel 信号量并发数限制+慢调用熔断;Hystrix 线程池隔离,已维护模式 | Sentinel 介绍 |
| Dubbo 默认容错 | Failover,默认重试 2 次;写操作要幂等或 retries=0 | Dubbo 集群容错 |
| Feign 超时坑 | 默认超时偏长,务必显式配置并与熔断、重试协调 | OpenFeign 文档 |
| 链路追踪断链原因 | 线程池/异步/MQ/网关未传递上下文 | OpenTelemetry 上下文传播 |
| 灰度发布怎么做 | 权重/标记分流 + 实例元数据 + 全链路标记透传 + 指标门槛自动回滚 | Expand/Contract 处理库表 |
一句话备忘:Spring 记“代理没走等于没用:自调用、private、异常吞了、多线程,事务都失效;三级缓存的第三级是为了延迟代理”;Boot 记“imports 文件 + Conditional,排查用 --debug”;微服务记“注册中心倾向 AP、网关薄而不阻塞、超时逐层递减、重试只对幂等、熔断三态、灰度靠标记透传与库表向前兼容”。
写完这篇我最大的感受是:Spring 的八股背得再熟,也不如亲手遇到一次“事务没回滚”来得印象深;微服务的治理概念再多,最后落到三件事——别让慢拖垮快、别让重试放大故障、别让发布把数据搞坏。把这三条讲出真实的案例,比罗列十个组件名字强得多。
参考资料
- Spring Framework:自定义 Bean 的特性(生命周期回调):初始化/销毁回调三种机制及执行顺序、Aware 接口、SmartLifecycle 与 @PostConstruct 使用注意。
- Spring Framework:依赖注入:构造器与 Setter 注入的取舍、官方对构造器注入的推荐,以及循环依赖的说明。
- Spring Framework:AOP 代理机制:JDK 动态代理与 CGLIB 的选择与限制、自调用问题及其解决办法、AspectJ 织入的区别。
- Spring Framework:@Transactional 用法:默认设置、回滚规则、代理模式限制、方法可见性与最佳实践。
- Spring Framework:事务传播:REQUIRED、REQUIRES_NEW、NESTED 的语义与连接占用风险。
- Spring Boot:自动配置:AutoConfiguration.imports 注册方式、--debug 条件报告、exclude 的三种做法。
- Spring Cloud Gateway 参考文档:Route、Predicate、Filter 三大核心概念及内置过滤器。
- Nacos 官方文档:什么是 Nacos:服务发现、动态配置、服务管理三大能力,以及配置版本与灰度发布特性。
- Resilience4j CircuitBreaker 文档:熔断器三态、滑动窗口与默认配置值。
- Sentinel 官方介绍:流量控制、熔断降级、系统自适应保护,以及与 Hystrix 线程池隔离的差异。
- OpenTelemetry:Traces 概念:Trace、Span 的结构与上下文传播的作用。
一起聊聊