我见过太多 Spring Boot 项目:功能没问题、测试全绿、线上也能跑——但一到压测就“像糊在蜂蜜里”。后来我才慢慢意识到:Spring Boot 并不慢,慢的是我们把“默认值”当成了“最佳实践”。
这篇我按“真实踩坑路径”把常见的性能坑拆开讲:从数据库、线程、序列化、启动、JVM、到观测与配置。每一节都给你能落地的动作,你拿去对照自己的项目做一轮体检,效果通常比你想象更明显。
0)先立规矩:别用“感觉”,用“证据”优化前先把基线立起来,否则你永远在“我觉得快了”。
最少做三件事:
如果你现在还没有“应用内部视角”,优先把 Actuator + Micrometer + Prometheus/Grafana 接上,别继续盲飞。
1)数据库:你以为是 “一次查询”,其实是 “101 次查询”典型症状列表接口返回 100 条用户,每条用户都要读 orders、roles、tags……然后你写了 getOrders()。
结果 Hibernate 默默给你打出 N+1:1 次查用户 + N 次查订单 = 延迟直线上升。
解决思路(别只会“全改 EAGER”)我更推荐三个工具组合,按场景选:
A. EntityGraph:声明式把某次查询变“带上关联”
@Entity@NamedEntityGraph( name = "User.orders", attributeNodes = @NamedAttributeNode("orders"))class User { ... }@EntityGraph("User.orders")List
B. JOIN FETCH:需要强控制时更直接
@Query("select u from User u join fetch u.orders where u.id in :ids")List
C. BatchSize:必须 lazy,但想“批量懒加载”
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user")@BatchSize(size = 25)private List
一句话标准:
很多人把 HikariCP 当“自带就不用管”,然后线上高峰出现:
默认池大小通常比较保守;在并发请求 + DB 操作偏多的系统里,连接池就是吞吐量阀门。
你可以这样调一个“可用起点” spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 20000 idle-timeout: 300000 max-lifetime: 1200000 leak-detection-threshold: 60000更关键的“思考方式”
池大小不是越大越好:
一个务实的策略是:先压测看“等待连接”的比例 → 再逐步调大到“等待基本消失但 DB 仍稳定”的点。
3)缓存:不是“全加 @Cacheable 就快”我也干过这种事:凡是查 DB 的方法都 @Cacheable。最后发现:
缓存的本质是:用空间换时间,但前提是“命中”和“稳定”。
我自己的规则(很朴素但好用)只缓存同时满足:
另外,别把“派生字段”单独再缓存一次,应该复用主对象缓存。
4)JSON 序列化:你返回的是“实体”,输出的是“整个世界”把 JPA Entity 直接当 API Response,Jackson 往往会:
解决方式:DTO。别偷懒。DTO 不只是性能,它还是 API 契约隔离。
record UserSummaryDTO(Long id, String name) {}@GetMapping("/users/{id}/summary")UserSummaryDTO summary(@PathVariable Long id) { var u = userService.findById(id); return new UserSummaryDTO(u.getId(), u.getName());}5)异步:别让用户等“非关键路径”
注册接口里最常见的慢操作:发邮件、打点、发 MQ、刷缓存……这些不影响“主流程成功”的动作,就不要阻塞响应。
Spring 的 @Async + 明确线程池,是最简单的提速手段之一。
@EnableAsync@Configurationclass AsyncConfig { @Bean("appExecutor") Executor appExecutor() { var ex = new ThreadPoolTaskExecutor(); ex.setCorePoolSize(5); ex.setMaxPoolSize(10); ex.setQueueCapacity(200); ex.setThreadNamePrefix("async-"); ex.initialize(); return ex; }}@Async("appExecutor")public void sendWelcomeEmail(...) { ... }
额外一句:如果你在 Java 21 + Spring Boot 新版本上,虚拟线程也值得评估,但不要把它当“银弹”。异步边界、IO 依赖、DB 池大小依然决定上限。
6)启动慢:不是“Spring 太重”,是你让它“全加载”项目一大,启动慢通常来自两件事:
A. 缩小扫描包
@SpringBootApplication(scanBasePackages = { "com.xxx.controller", "com.xxx.service", "com.xxx.config"})class App {}
B. 开启 Lazy Init(谨慎:首次调用会变慢)
spring: main: lazy-initialization: true
C. spring-context-indexer(大项目很香)它会在构建期预计算候选组件,减少运行期扫描与反射开销。
进阶:GraalVM Native Image / CRaC(看你的部署形态)如果你是大量小服务、弹性伸缩、冷启动敏感:Native Image 的收益往往非常直观,但你要接受构建与兼容性成本。
7)JVM:你以为是“内存泄漏”,其实是“GC 在求救”很多“内存问题”本质是:我见过太多 Spring Boot 项目:功能没问题、测试全绿、线上也能跑——但一到压测就“像糊在蜂蜜里”。后来我才慢慢意识到:Spring Boot 并不慢,慢的是我们把“默认值”当成了“最佳实践”。
这篇我按“真实踩坑路径”把常见的性能坑拆开讲:从数据库、线程、序列化、启动、JVM、到观测与配置。每一节都给你能落地的动作,你拿去对照自己的项目做一轮体检,效果通常比你想象更明显。
0)先立规矩:别用“感觉”,用“证据”优化前先把基线立起来,否则你永远在“我觉得快了”。
最少做三件事:
如果你现在还没有“应用内部视角”,优先把 Actuator + Micrometer + Prometheus/Grafana 接上,别继续盲飞。
1)数据库:你以为是 “一次查询”,其实是 “101 次查询”典型症状列表接口返回 100 条用户,每条用户都要读 orders、roles、tags……然后你写了 getOrders()。
结果 Hibernate 默默给你打出 N+1:1 次查用户 + N 次查订单 = 延迟直线上升。
解决思路(别只会“全改 EAGER”)我更推荐三个工具组合,按场景选:
A. EntityGraph:声明式把某次查询变“带上关联”
@Entity@NamedEntityGraph( name = "User.orders", attributeNodes = @NamedAttributeNode("orders"))class User { ... }@EntityGraph("User.orders")List
B. JOIN FETCH:需要强控制时更直接
@Query("select u from User u join fetch u.orders where u.id in :ids")List
C. BatchSize:必须 lazy,但想“批量懒加载”
@OneToMany(fetch = FetchType.LAZY, mappedBy = "user")@BatchSize(size = 25)private List
一句话标准:
很多人把 HikariCP 当“自带就不用管”,然后线上高峰出现:
默认池大小通常比较保守;在并发请求 + DB 操作偏多的系统里,连接池就是吞吐量阀门。
你可以这样调一个“可用起点” spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 20000 idle-timeout: 300000 max-lifetime: 1200000 leak-detection-threshold: 60000更关键的“思考方式”
池大小不是越大越好:
一个务实的策略是:先压测看“等待连接”的比例 → 再逐步调大到“等待基本消失但 DB 仍稳定”的点。
3)缓存:不是“全加 @Cacheable 就快”我也干过这种事:凡是查 DB 的方法都 @Cacheable。最后发现:
缓存的本质是:用空间换时间,但前提是“命中”和“稳定”。
我自己的规则(很朴素但好用)只缓存同时满足:
另外,别把“派生字段”单独再缓存一次,应该复用主对象缓存。
4)JSON 序列化:你返回的是“实体”,输出的是“整个世界”把 JPA Entity 直接当 API Response,Jackson 往往会:
解决方式:DTO。别偷懒。DTO 不只是性能,它还是 API 契约隔离。
record UserSummaryDTO(Long id, String name) {}@GetMapping("/users/{id}/summary")UserSummaryDTO summary(@PathVariable Long id) { var u = userService.findById(id); return new UserSummaryDTO(u.getId(), u.getName());}5)异步:别让用户等“非关键路径”
注册接口里最常见的慢操作:发邮件、打点、发 MQ、刷缓存……这些不影响“主流程成功”的动作,就不要阻塞响应。
Spring 的 @Async + 明确线程池,是最简单的提速手段之一。
@EnableAsync@Configurationclass AsyncConfig { @Bean("appExecutor") Executor appExecutor() { var ex = new ThreadPoolTaskExecutor(); ex.setCorePoolSize(5); ex.setMaxPoolSize(10); ex.setQueueCapacity(200); ex.setThreadNamePrefix("async-"); ex.initialize(); return ex; }}@Async("appExecutor")public void sendWelcomeEmail(...) { ... }
额外一句:如果你在 Java 21 + Spring Boot 新版本上,虚拟线程也值得评估,但不要把它当“银弹”。异步边界、IO 依赖、DB 池大小依然决定上限。
6)启动慢:不是“Spring 太重”,是你让它“全加载”项目一大,启动慢通常来自两件事:
A. 缩小扫描包
@SpringBootApplication(scanBasePackages = { "com.xxx.controller", "com.xxx.service", "com.xxx.config"})class App {}
B. 开启 Lazy Init(谨慎:首次调用会变慢)
spring: main: lazy-initialization: true
C. spring-context-indexer(大项目很香)它会在构建期预计算候选组件,减少运行期扫描与反射开销。
进阶:GraalVM Native Image / CRaC(看你的部署形态)如果你是大量小服务、弹性伸缩、冷启动敏感:Native Image 的收益往往非常直观,但你要接受构建与兼容性成本。
7)JVM:你以为是“内存泄漏”,其实是“GC 在求救”很多“内存问题”本质是:
一个可用起点(示例):
java -jar app.jar \ -Xms512m -Xmx2g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:+UseStringDeduplication8)生产配置:最“便宜”的 25% 性能提升
很多性能提升不是改代码,而是改几行配置:Tomcat 线程、Hibernate 批处理、日志级别。
server: tomcat: threads: max: 200 min-spare: 10 connection-timeout: 20000 accept-count: 100spring: jpa: properties: hibernate: jdbc: batch_size: 20 order_inserts: true order_updates: true show-sql: falselogging: level: org.springframework: WARN org.hibernate: WARN com.yourapp: INFO最后一段我想强调的
性能优化的核心不是“技巧”,而是“把默认值当成可质疑对象”。Spring Boot 帮你把项目跑起来,但“跑得快、跑得稳、跑得省”,得靠你理解它默认做了什么、什么时候不适合你、以及你用数据证明改变是有效的。
一个可用起点(示例):
java -jar app.jar \ -Xms512m -Xmx2g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=200 \ -XX:+UseStringDeduplication8)生产配置:最“便宜”的 25% 性能提升
很多性能提升不是改代码,而是改几行配置:Tomcat 线程、Hibernate 批处理、日志级别。
server: tomcat: threads: max: 200 min-spare: 10 connection-timeout: 20000 accept-count: 100spring: jpa: properties: hibernate: jdbc: batch_size: 20 order_inserts: true order_updates: true show-sql: falselogging: level: org.springframework: WARN org.hibernate: WARN com.yourapp: INFO最后一段我想强调的
性能优化的核心不是“技巧”,而是“把默认值当成可质疑对象”。Spring Boot 帮你把项目跑起来,但“跑得快、跑得稳、跑得省”,得靠你理解它默认做了什么、什么时候不适合你、以及你用数据证明改变是有效的。
如果这篇内容对你有帮助,欢迎点赞 、收藏 、转发给需要的朋友
我会持续分享:
关注我,一起把“技术”真正用在项目和业务里。
你的每一次支持,都是我持续输出高质量内容的最大动力。
本站是社保查询公益性网站链接,数据来自各地人力资源和社会保障局,具体内容以官网为准。
定期更新查询链接数据 苏ICP备17010502号-11