Skip to content

Spring 全栈硬核教程 · 08:实战、性能调优、事故复盘与面试 ​

前七篇讲的是"知识",这一篇讲的是"经验"——那些只能在真实生产环境里长出来的东西。四个事故复盘全部是行业里高频重复发生的类型,每一个都值得你在自己的系统里做一次对照检查。


1. 生产级项目骨架 ​

1.1 分层与包结构 ​

com.example.order/
├── OrderApplication.java
├── config/                    # 配置类(Security、Redis、线程池、MyBatis)
├── controller/                # ⭐ 只做参数接收、调用 Service、返回 DTO,不写业务逻辑
│   └── dto/                   #    请求/响应对象(绝不暴露实体)
├── service/                   # 业务逻辑的唯一归属地
│   ├── OrderService.java
│   └── impl/
├── repository/ (或 mapper/)   # 数据访问,只关心存取
├── domain/                    # 实体/值对象/领域服务
├── client/                    # 外部服务调用封装(HTTP Interface / Feign)
├── event/                     # 应用事件与监听器
├── exception/                 # 异常定义 + 全局处理器
├── support/                   # 工具类、常量、通用组件
└── job/                       # 定时任务

三条铁律:

  1. 实体对象永不出 Service 层 —— 一次性解决懒加载异常、循环序列化、字段泄露三个问题
  2. Controller 不写业务逻辑 —— 判断标准:这个方法能不能不改一行地被定时任务/MQ 消费者复用?
  3. 外部调用统一收口到 client/ —— 换供应商、加重试、加熔断、加 mock 全都只改一个地方

1.2 必备基础设施清单(新项目开箱即上) ​

组件作用优先级
全局异常处理 + ProblemDetail统一错误格式,带 traceId🔥
统一响应包装(ResponseBodyAdvice)前端不用处理两种格式🔥
链路追踪 + traceId 打进日志排查效率提升一个数量级🔥
参数校验 + 分组校验挡掉一半的脏数据问题🔥
Actuator(独立端口)健康检查、指标暴露🔥
优雅停机发版不掉请求🔥
接口耗时切面 + 慢接口告警性能问题提前暴露⭐
线程池统一管理 + 监控见 4.3 的事故⭐
接口文档(SpringDoc OpenAPI)前后端协作⭐

1.3 线程池:别再用 Executors ​

java
@Configuration
@EnableAsync
public class ThreadPoolConfig {

    @Bean("bizExecutor")
    public ThreadPoolTaskExecutor bizExecutor() {
        var executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(500);              // ⚠️ 必须有界!
        executor.setThreadNamePrefix("biz-");        // ⭐ 线程名要有意义,dump 时能认出来
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);   // ⭐ 配合优雅停机
        executor.setAwaitTerminationSeconds(30);
        // ⭐ 传递 MDC(traceId) 和 SecurityContext 到异步线程
        executor.setTaskDecorator(new ContextCopyingDecorator());
        return executor;
    }
}

Executors.newFixedThreadPool() 的队列是无界的 LinkedBlockingQueue —— 任务堆积时不会拒绝,会一直吃内存直到 OOM。阿里规约明确禁止使用 Executors 创建线程池,就是这个原因。


2. 性能调优方法论 ​

2.1 顺序不能乱:先测量,再优化 ​

① 定义指标       —— P99 延迟 < 200ms?QPS > 5000?先说清楚目标是什么
② 压测建立基线   —— 没有基线的"优化",你不知道有没有变好
③ 定位瓶颈       —— ⭐ 这一步花的时间应该最多
④ 针对性优化     —— 一次只改一个变量
⑤ 回归验证       —— 确认真的变好了,且没引入新问题

最常见的错误:跳过①②③直接做④——"我觉得是数据库慢",然后加了一堆缓存,结果瓶颈在 JSON 序列化上。凭直觉优化的命中率大约是三分之一,而每次改动都带来新的风险。

2.2 定位瓶颈的工具链 ​

层次工具看什么
应用整体Actuator + Prometheus + GrafanaQPS、P99、错误率、GC 频率
链路级SkyWalking / Zipkin哪一跳最慢,慢在哪个服务
方法级Arthas(trace/watch/profiler)⭐ 线上不重启就能定位到具体方法耗时
SQLp6spy / 慢查询日志 / EXPLAIN慢 SQL、N+1、没走索引
JVMJFR、jstat、jmap、MATGC 停顿、内存泄漏
火焰图async-profilerCPU 热点一目了然

Arthas 三板斧(生产排查必备):

bash
trace com.example.OrderService createOrder '#cost > 500'   # 哪个子调用慢
watch com.example.OrderService createOrder '{params, returnObj}' -x 3   # 看参数和返回值
profiler start --event cpu; profiler stop --format html    # 生成火焰图

2.3 JVM 参数基线(容器环境) ​

bash
java -XX:MaxRAMPercentage=75.0 \        # ⭐ 容器里用百分比,别写死 -Xmx
     -XX:+UseG1GC \                     # Java 21 默认;超大堆(>32G)可考虑 ZGC
     -XX:MaxGCPauseMillis=200 \
     -XX:+HeapDumpOnOutOfMemoryError \  # ⭐ OOM 时自动 dump,这是事后唯一的证据
     -XX:HeapDumpPath=/data/dumps \
     -XX:+ExitOnOutOfMemoryError \      # ⭐ OOM 后直接退出,让 K8s 重启,别半死不活
     -jar app.jar

-Xmx 在容器里的经典坑:早期 JVM 读的是宿主机内存,容器限制 2G 但 JVM 以为有 64G,于是堆开到 16G → 容器被 OOMKilled,而且堆里看不到任何异常(因为是被内核杀的,不是 JVM 自己 OOM)。现代 JDK 已能感知容器限制,但用 MaxRAMPercentage 仍是更稳的写法。


3. ⭐ 四个真实生产事故复盘 ​

事故一:数据库连接池打满,全站瘫痪 ​

现象:大促期间,所有接口超时,日志刷满 Connection is not available, request timed out after 30000ms。

排查:/actuator/metrics/hikaricp.connections.active 显示连接全被占用;线程 dump 显示大量线程卡在获取连接上。

根因:某个下单接口的 @Transactional 方法里调用了第三方支付 HTTP 接口(回顾 04 篇 4.1)。支付网关当天响应从 200ms 劣化到 8 秒,每个请求持有数据库连接 8 秒,20 个连接的池子在几秒内被打满,全站所有数据库操作连带瘫痪。

修复:

  1. 紧急:给支付调用加 3 秒超时 + 熔断
  2. 根治:把 HTTP 调用移出事务边界,改成"短事务建单 → 事务外调支付 → 短事务改状态"
  3. 预防:加 leak-detection-threshold: 60000;连接池活跃数配告警

通用教训:一个下游服务的抖动,会通过共享资源(连接池)放大成自己的全站故障。 事务里只做数据库操作,这条规则的价值就在这里。


事故二:缓存雪崩 ​

现象:凌晨 2:00 整,数据库 CPU 瞬间 100%,接口大面积超时,持续约 5 分钟后自行恢复。

根因:前一天 2:00 做了一次全量缓存预热,所有 key 用了相同的 TTL(24 小时),于是 24 小时后同一秒全部过期,所有请求同时回源数据库。

修复:

java
// 过期时间加随机抖动
long ttl = Duration.ofHours(24).toSeconds() + ThreadLocalRandom.current().nextLong(3600);
redis.opsForValue().set(key, value, Duration.ofSeconds(ttl));

外加:热点数据逻辑过期 + 后台异步刷新;回源加互斥锁;数据库层面加熔断兜底。

通用教训:任何"批量同时发生"的事件都是风险源——同时过期、同时重试、同时重启、定时任务同时触发。加随机抖动是成本最低的防护手段,几乎所有批量操作都该考虑。


事故三:线程池拒绝,异步任务大面积丢失 ​

现象:用户反馈"订单成功了但没收到短信",日志里有 RejectedExecutionException,但量不大没人注意。

根因:@Async 用了默认线程池(SimpleAsyncTaskExecutor,每次都新建线程,不复用);后来改成自定义池,但队列设了 100、拒绝策略用了默认的 AbortPolicy——高峰期队列满了直接抛异常丢任务,而异步方法的异常没人接,静默失败。

修复:

  1. 队列容量按峰值合理设置,拒绝策略改 CallerRunsPolicy(让调用方线程执行,天然形成背压)
  2. 关键任务不走内存队列,走 MQ(可持久化、可重试、可追溯)
  3. 配置 AsyncUncaughtExceptionHandler 兜住异步异常
  4. 线程池指标(活跃数、队列深度、拒绝数)接入监控告警

通用教训:内存队列里的任务 = 随时可能丢失的任务。进程重启、队列满、线程池拒绝,任何一个都能让它消失。"丢了会有业务损失"的任务,必须走持久化 MQ。


事故四:内存泄漏,每周重启一次 ​

现象:服务运行 5-7 天后 Full GC 频繁,响应变慢,运维养成了"每周重启"的习惯。

排查:jmap -dump 后用 MAT 分析,发现一个 ConcurrentHashMap 占了 1.8G。

根因:为了"提升性能"写了个本地缓存 static Map<Long, UserDTO>,只放不删,没有容量上限,没有过期时间。用户量增长后无限膨胀。

修复:换成 Caffeine(有界 + 过期 + 命中率统计):

java
Cache<Long, UserDTO> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(Duration.ofMinutes(30))
    .recordStats()                                  // ⭐ 暴露命中率指标
    .build();

通用教训:任何长生命周期的集合都必须有边界(容量上限 + 过期策略)。手写 static Map 做缓存是内存泄漏的头号来源。"每周重启一次"不是运维方案,是没被正视的 bug。


事故复盘的通用方法(比事故本身更重要) ​

1. 时间线还原:几点几分发生什么,别凭记忆,看监控和日志
2. 根因分析:连问五个为什么,别停在"因为下游慢了"
3. 影响面评估:多少用户、多少订单、多少钱
4. 修复措施分三层:
   ├─ 止血(当时怎么恢复的)
   ├─ 根治(怎么保证同样原因不再发生)
   └─ 预防(怎么让同类问题在别处也不会发生)⭐ 最有价值的一层
5. 检查同类风险:⭐⭐ 这个根因在系统的其他地方还存在吗?

第 5 步是区分"应付复盘"和"真正复盘"的分水岭。修好了这一个事务里的 HTTP 调用,系统里还有多少个?全局搜一遍 @Transactional,比修一个 bug 有价值十倍。


4. 分层面试题库 ​

Level 1 · 能用(应届 / 1 年内) ​

  1. @SpringBootApplication 包含哪三个注解?各自作用?
  2. @Autowired 和 @Resource 的区别?
  3. @RequestParam / @PathVariable / @RequestBody 分别从哪里取值?
  4. Spring Boot 怎么做多环境配置?
  5. @Controller 和 @RestController 的区别?
  6. 说说 Spring MVC 处理一个请求的大致流程。

Level 2 · 懂原理(1-3 年,⭐ 分水岭) ​

  1. 画出 Bean 完整生命周期,AOP 代理在哪一步生成?
  2. 三级缓存怎么解决循环依赖?为什么两级不够?构造器注入为什么解决不了?
  3. @Transactional 的 7 种失效场景及各自根因?
  4. 自动配置的发现机制?Boot 2 和 3/4 有什么区别,为什么要改?
  5. JDK 动态代理和 CGLIB 的区别?Spring Boot 默认用哪个,为什么?
  6. 外部化配置的优先级顺序?"一次构建多环境部署"怎么落地?
  7. 为什么单例 Bean 里不能放可变成员变量?
  8. @ConditionalOnMissingBean 为什么是自动配置体系的灵魂?

Level 3 · 能解决问题(3-5 年) ​

  1. 事务传播的七种行为?REQUIRES_NEW 在高并发下有什么陷阱?
  2. 为什么"事务方法里调 HTTP 接口"是定时炸弹?
  3. 缓存穿透/击穿/雪崩的区别和解法?为什么过期时间要加抖动?
  4. 连接池为什么不是越大越好?怎么确定合理大小?
  5. Spring Security 过滤器链的顺序?自定义 Filter 该插在哪?
  6. 为什么 @RestControllerAdvice 捕获不到 401/403?
  7. SecurityContextHolder 在异步线程里为什么取不到用户?
  8. N+1 查询是什么?怎么在开发阶段就发现它?
  9. open-in-view 默认 true 有什么代价?
  10. 线程池的拒绝策略有哪几种?生产该选哪个,为什么?

Level 4 · 能做架构决策(5 年以上 / 技术负责人) ​

  1. 虚拟线程的原理?它怎么改变了 MVC vs WebFlux 的选型逻辑?有哪些陷阱?
  2. 什么情况下才该拆微服务?拆错边界的代价是什么?
  3. 分布式事务的四种方案成本对比?为什么说本地消息表是多数场景最优解?
  4. 熔断的核心价值是保护自己还是保护下游?为什么?
  5. 重试为什么危险?怎么安全地重试?
  6. GraalVM 原生镜像的真实收益和代价?什么场景值得?
  7. JWT 的三个真实代价?什么情况下不该用 JWT?
  8. Spring Modulith 解决什么问题?和微服务是什么关系?
  9. Boot 3 → 4 升级有哪些主要断点?怎么规划迁移?
  10. 怎么设计一个防重复提交的下单接口?
  11. 一个接口 P99 突然从 100ms 涨到 2s,你的排查路径是什么?

面试官视角的提示:Level 2 的问题答不上来基本就止步初级了,因为它们检验的是"有没有真正理解框架"而不是"记没记住 API"。Level 4 的问题没有标准答案,考察的是权衡意识——能说出"什么场景选 A、什么场景选 B、代价分别是什么"的人,比背出一堆方案名词的人强得多。


5. 学习检查清单:给自己打个分 ​

□ 能徒手画出 Bean 生命周期,并指出 AOP 代理的生成时机
□ 能解释三级缓存,并说清"为什么两级不够"
□ 能列出 @Transactional 的 7 种失效场景
□ 能画出 DispatcherServlet 的请求处理流程
□ 能说清 Spring Security 过滤器链的关键顺序
□ 知道自己项目的连接池大小是怎么定的,以及为什么
□ 知道自己项目里有没有"事务里调 HTTP"的代码(去搜一遍)
□ 知道自己项目的缓存过期时间有没有加抖动
□ 知道自己项目的线程池拒绝策略是什么,队列是不是有界
□ 能说出虚拟线程适合和不适合的场景
□ 能给出"该不该拆微服务"的判断依据
□ 遇到线上问题,有一套固定的排查路径而不是靠猜

打勾少于 6 个 → 回去重读 01/02 篇的原理部分; 打勾 6-9 个 → 你已经比大多数人扎实了,重点补 04/06 篇的实战部分; 打勾 10 个以上 → 你该去带人了,把 08 篇的事故复盘当作团队培训材料。


6. 结语:怎么持续跟进这个生态 ​

Spring 生态每年都在变,但变的是 API,不变的是设计思想。IoC、AOP、约定优于配置、可扩展的策略链——这些机制从 2003 年到现在几乎没变过。把原理吃透了,新版本的变化对你来说只是"换了个写法",而不是"要重新学一遍"。

持续跟进的三个渠道(按性价比排序):

  1. 官方 Release Notes —— 每个大版本发布时花半小时读一遍,比读十篇二手博客有效
  2. Spring 官方博客 —— 新特性的设计意图通常只有这里讲得清楚
  3. 自己的生产系统 —— 最好的老师永远是一次真实的线上事故

最后一句:技术选型没有"最好",只有"在你的约束下最合适"。 这套教程给了你各种方案的代价清单,但做决定的是你——因为只有你知道你的团队规模、业务阶段、运维能力和容错空间。


全系列完。

篇章主题
00总纲:全景图与学习路线
01Spring 核心:IoC / AOP / 事务
02Spring Boot:自动配置与工程化
03Spring Web:MVC / WebFlux / REST
04Spring Data:数据访问全栈
05Spring Security:认证与授权
06Spring Cloud:微服务与云原生
07前沿与扩展:AI / 虚拟线程 / GraalVM / Modulith
08实战、调优、事故复盘与面试(本篇)

基于 Vite 强力驱动 | 纯静态轻量托管