资讯动态

Spring Boot性能优化实战:从800ms到160ms的500%提升

发布时间:2026/10/3 7:51:54 来源:尧图企业网站定制
先说结论把一个 Spring Boot 接口的响应时间从 800ms 压到 160ms我干过不止一次。“500%性能提升”看着像标题党其实就是这套组合拳的真实收益。很多人一提到性能优化就想到改缓存、加索引、调 JVM 参数但真正动手时容易陷入“东一榔头西一棒子”的误区。这篇博客我结合自己做过的一个企业办公用品管理系统把 Spring Boot 性能提升最常踩的坑和最有效的几个手段串起来讲一遍。无论你现在用的是 Spring Boot 2.3、2.6 还是 3.x里面的思路都通用。1. 性能提升500%之前先搞清楚瓶颈在哪1.1 别再瞎调参先量化再动手我见过不少同事一上线就说慢然后凭感觉在配置文件里加几个线程数、改几个缓存过期时间最后测完发现不仅没变快反而把服务搞得不稳定。性能优化的第一步永远是量化不是改代码。先拿压测工具比如 JMeter、wrk或者线上监控把接口的吞吐量、响应时间、CPU、内存、GC、数据库慢查询这几个指标拉出来。有了这些数据你才能判断瓶颈到底在应用层、数据库层、缓存层还是 JVM 层。实操建议先跑一轮最简单的压测观察 TPS 和响应时间曲线。如果 QPS 上不去但 CPU 没跑满多半是锁、线程池配置或者数据库连接池不够如果 CPU 跑满了再看是 GC 频繁还是业务计算消耗大。工具方面我习惯用 Arthas 看线上方法的耗时分布用 JProfiler 做深层次分析但这都建立在先有监控数据的前提下。1.2 瓶颈分层的思考框架性能瓶颈通常分成四层并发层线程模型、异步、锁、计算层算法、JIT、GC、IO 层数据库、Redis、外部 HTTP 调用、基础设施层带宽、磁盘、容器配置。你要想清楚500% 的优化空间是从哪个层面榨出来的。比如接口响应时间 800ms可能数据库查询占了 500ms剩下 300ms 是业务逻辑和网络开销。数据库这个大头不解决线程调再多都没用。一个很典型的例子在办公用品管理系统的物品申请接口里用户点击申请后要查询库存、查询审批人、写入申请单还要调一个外部企业微信通知服务。第一次优化前我观察数据发现数据库查询占了 70%外部接口调用占了 20%业务代码只占 10%。所以优化重心就很清晰先搞 SQL 索引和缓存再异步化外部通知调用而不是一开始就去调 Tomcat 线程池。1.3 500% 是如何算出来的这里说点实在的把一次请求里串行执行的耗时操作改成并行或缓存收益是乘法级的。比如一个接口原来耗时 1000ms其中 600ms 是查询数据库400ms 是调用外部服务。如果给数据库查询加 Redis 缓存假设命中率 90%那么这部分平均耗时变成 60ms外部调用改成异步不阻塞主线程那么主流程大概剩下几十毫秒。整体响应时间从 1000ms 降到 120ms这就是 8 倍差距。500% 不是靠单个玄学参数而是把每一层耗时分拆后逐个击破。2. 并发与异步把 Tomcat 线程池和异步逻辑用明白2.1 Tomcat 线程池参数不是随便调的Spring Boot 内嵌的 Tomcat 线程池很多初学者默认不改也能跑但高并发场景下默认参数不一定合适。核心配置是这几个server: tomcat: threads: max: 300 min-spare: 50 max-connections: 10000 accept-count: 200 connection-timeout: 5000threads.max 控制最大工作线程数min-spare 是初始空闲线程数max-connections 是服务端能接受的连接数accept-count 是等待队列长度。这里有个关键认知线程不是越多越好。每个线程都会占用内存和 CPU 上下文切换成本如果你把线程调到 1000数据库连接池只有 50那大部分线程都在等连接反而拖垮吞吐。经验做法是先压测再调整从 200 起步逐档增加观察响应时间和线程阻塞率。2.2 Async 与自定义线程池的正确姿势Spring Boot 的 Async 是我最喜欢的性能核武器之一但前提是别直接使用默认的 SimpleAsyncTaskExecutor。它每次请求都会新建线程生产环境绝对不能这么用。正确做法是自定义线程池Configuration public class AsyncConfig { Bean(bizExecutor) public ThreadPoolTaskExecutor bizExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(200); executor.setThreadNamePrefix(biz-async-); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }使用的时候把 Async(bizExecutor) 加在方法上。这里有几个坑我必须提醒第一方法必须从一个 Bean 的外部调用否则 Async 代理不生效第二异步线程拿不到主线程的 ThreadLocal像 traceId、用户信息都会丢要手动传递或使用装饰器第三用 CompletableFuture 做异步编排时要指定线程池不然还是会走到公共 ForkJoinPool一旦任务阻塞会影响其他异步操作。2.3 WebSocket 长连接场景的线程模型配置热搜里有人问 Spring Boot 集成 WebSocket 的 yml 配置。WebSocket 和普通 HTTP 请求不一样连接建立后是长连接如果每个连接都占一个线程服务端很快就会被拖垮。我的经验是WebSocket 连接数量的限制不在 Tomcat 线程而在系统文件句柄和内存所以要避免把连接和业务执行线程绑定。处理消息时把接收到的 WebSocket 消息丢到自定义线程池里异步处理让 I/O 线程快速返回去处理下一个消息。spring: websocket: # 这里没有标准的全局配置主要在服务端代码里控制实际上Spring Boot 的 WebSocket 支持分两种一种是使用ServerEndpoint的 Java WebSocket 标准一种是 Spring MVC 集成下的WebSocketHandler。我建议在业务中继承TextWebSocketHandler在handleMessage里把任务提交给业务线程池并且一定要设置发送消息的超时时间否则当客户端消费不过来时服务端消息积压会占用大量内存。还有一个容易忽略的点WebSocket 的线程池要独立配置不要和 HTTP 接口争用同一个池子。2.4 Spring Boot 3 与响应式 WebFlux 的选择如果你正在用 Spring Boot 3响应式 WebFlux 也是一个提升吞吐量的方向它不像传统 Servlet 模型那样一个请求占一个线程而是基于 Netty 的事件循环。但我要说实话WebFlux 的学习成本和改造代价都不小尤其当你用的还是 MySQL 的 JDBC 驱动时阻塞调用会在事件循环线程里卡住反而拖慢性能。必须配合 R2DBC 或异步数据库驱动才有意义。拿 Spring Boot 3 和 Python FastAPI 相比两者在异步 IO 思想上有相似点但 Spring Boot WebFlux 生态更重适合构建复杂的企业级系统。我的建议是如果现有项目是一套成熟的 Servlet 栈不要为了追求性能盲目上 WebFlux。它适合从零搭建、并且流量模型是 IO 密集型的场景。性能优化不是最先进的技术就好而是最适合当前团队和业务阶段。3. 缓存层优化把热点数据留在离接口最近的地方3.1 本地缓存 Caffeine 还是分布式缓存 Redis办公用品系统里有一个典型热点数据物品分类和库存表。这类数据读多写少直接用 Redis 每次请求都走网络反而浪费。我采用的方案是两级缓存本地 Caffeine 先查一级缓存不命中再去 Redis最后再查数据库。Caffeine 的配置非常简单Bean public CacheString, Category categoryCache() { return Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(30)) .build(); }为什么不用 Redis 替代本地缓存因为本地缓存的读取耗时是微秒级Redis 是毫秒级网络 IO虽然差别不大但在高 QPS 下积少成多。Caffeine 适合缓存单机范围内的热点数据Redis 适合多个实例共享的数据。判断标准很简单如果数据变化不频繁可以接受不同实例短暂不一致优先用 Caffeine如果多个服务实例必须看到同一份数据就用 Redis。真实收益库存查询从原来平均 350ms 降到 30ms。3.2 缓存穿透、击穿、雪崩的工程解法缓存不是简单 get/set 就完事这三个问题每个上线项目都会遇到。缓存穿透是指查的 key 在库里根本不存在每次都会落到数据库。解决办法是缓存空值或者用布隆过滤器先拦截。我建议优先缓存空值代码改动最小。缓存击穿是某个热点 key 过期瞬间有大量请求打到数据库。传统做法是加互斥锁但实现起来容易出问题。我更推荐“逻辑过期”方案缓存里写一个过期时间读到时发现逻辑过期就启一个线程去刷新缓存其他线程先返回旧值这样既不会阻塞请求又能保证数据最终一致。缓存雪崩是大量 key 同时过期解决核心是错峰过期。在设置过期时间时加随机数比如base new Random().nextInt(300)秒。同时在查询前做多级缓存保护即使 Redis 宕机本地缓存也能挡一阵。真实踩坑经验有一次把所有缓存过期时间设为 30 分钟结果整点 30 分一到数据库瞬间被请求淹没就是没加随机数导致的。3.3 缓存与数据库的一致性采用 Cache Aside 模式性能提升不等于数据可以乱缓存一致性是优化时必须跨过的坑。我一直使用 Cache Aside 模式读的时候先读缓存读不到再读数据库然后回填缓存写的时候先更新数据库再删除缓存。为什么是先更新库再删缓存而不是先删缓存再更新数据库因为并发窗口期是致命的。举个实际例子线程 A 先删了缓存线程 B 此时查询数据库并按旧值回填了缓存然后 A 才更新数据库这条数据就永久不一致了。删除缓存失败的兜底方案是消息队列。我在项目里用 RocketMQ 事务消息业务更新数据库成功后发送一条删除缓存消息消费者收到消息后删除缓存。如果删除失败重试机制会再次触发而不是默默放弃。这套方案看似多绕了一层但在线故障时价值极大。注意点缓存删除操作本身也会有延迟所以不能对一致性要求极高的数据例如金额变动做这种优化只能针对不敏感的热点查询。3.4 序列化性能的隐形损耗Redis 的 value 序列化方式直接决定缓存读写性能。Spring Boot 默认的 RedisTemplate 如果不指定序列化器会用 JdkSerializationRedisSerializer它有两个问题缓存内容可读性极差而且序列化体积大、速度慢。我第一次压测时就发现同一个对象存 JSON 只需要 120ms用 JDK 序列化要 350ms。推荐配置Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; }对于特别大的缓存体还可以考虑 Kyro、Protostuff 这类二进制序列化压缩后体积更小。但要注意一旦使用私有序列化协议运维排查缓存数据时看不到明文调试成本会上升。我的经验是先用 Jackson JSON满足大多数场景只有单条缓存超过 10KB 且访问量极大时再考虑二进制方案。4. 数据库层SQL、索引、连接池与 ORM 性能4.1 慢 SQL 定位与索引设计办公用品系统的实战数据库往往是 Spring Boot 性能最大的短板尤其是企业管理类系统表多、关联多、数据量大。办公用品管理系统的申请表、审批记录表、库存表经常产生多表关联查询。我会在 MySQL 侧开启慢查询日志把超过 500ms 的 SQL 都捞出来。定位到慢 SQL 之后用 EXPLAIN 看执行计划。有一个查询特别典型物品申请列表要关联审批人姓名和物品名称原本是三条 SQL 循环拼接耗时 600ms 多。优化方式是改成一条多表 JOIN并给外键字段创建联合索引。索引有最左匹配原则要查user_id和status时建(user_id, status)才有效。如果索引建反了优化器可能还是走全表扫描。这个阶段的实际收益最明显SQL 优化往往能把接口直接提速一倍以上。4.2 深分页优化从 LIMIT 到游标分页很多后台管理列表都有翻页需求但列表越往后越慢这是 LIMIT 深翻页的老问题。比如LIMIT 100000, 20会先扫描前 100020 条再丢弃前 100000 条这是浪费时间。我的替代方案是游标分页SELECT * FROM apply_record WHERE id #{lastId} ORDER BY id LIMIT 20;前端传上一页的最后一条记录 ID而不是 pageNum * pageSize。这样每次查询都能走主键索引不会因为页数加深而变慢。如果是按创建时间排序的需要给create_time建索引同时把id作为排序的 tie-breaker否则会导致排序不稳定。4.3 避免 N1在 ORM 层堵住性能黑洞Spring Data JPA 和 MyBatis 都很容易写出 N1 查询。JPA 的OneToMany懒加载循环遍历子实体时每访问一次就发一条 SQL。MyBatis 的association或collection如果不小心配置也会造成子查询循环。这在大批量列表展示时是致命的。JPA 侧可以用BatchSize(size 50)或者查询时使用EntityGraph把关联带出来。MyBatis 侧推荐用分步查询加fetchSize或者写一个resultMap整体 JOIN 查询。我的个人习惯是列表接口里直接写一个专用的查询 SQL为了“通用性”用 ORM 自动关联查询结果是牺牲性能得不偿失。宁愿多维护一条 SQL也要保证一次查询搞定。4.4 HikariCP 连接池参数别把它当成无限资源Spring Boot 默认的 HikariCP 很好用但参数需要结合实际调整。很多人以为连接池越大越好这是一个误区。连接池大小受数据库 CPU、内存和磁盘 IO 限制。一般推荐公式是(CPU核心数 * 2) 1。如果你的服务部署在 4 核机器上maximum-pool-size 大概在 9就足够了。连接池过大反而会让数据库同时处理大量无用的空闲连接降低请求响应速度。实际配置里我还会设置connection-timeout为 3000ms避免线程无限等待连接max-lifetime设置为与数据库 wait_timeout 相近但略小的值leak-detection-threshold设为 5000ms帮助发现连接泄漏。曾经遇到过一个诡异问题某个接口偶尔慢几秒最后发现是代码里没有关闭连接把连接池占满后面的请求全在等连接。开这个检测阈值后半小时就抓到了问题代码。5. JVM、容器与部署把性能优化延续到运行时5.1 容器内存感知与堆内存设置Spring Boot 应用跑在 Docker 或 K8s 里最容易被坑的就是 JVM 不认识容器限制。虽然现代 JDK 默认有UseContainerSupport但我还是强烈建议显式设置内存参数。比如容器限制 1G 内存你可以使用-XX:MaxRAMPercentage75让 JVM 的堆内存不超过 750MB剩下留给 JVM 元空间、线程栈和堆外内存。# 启动命令参考 java -XX:MaxRAMPercentage75.0 -XX:InitialRAMPercentage50.0 -jar app.jar如果堆设置得太大容器会出现 OOM Killer 杀掉进程设置太小则 GC 频繁。这里的经验是通过监控观察实际堆占用再反推合理百分比。启动时最好设置InitialRAMPercentage让 JVM 在一开始就有足够的堆而不是慢慢扩容导致启动阶段频繁 Full GC。5.2 G1GC 调优控制暂停时间而不是盲目换 GCJDK 8 之后的 Spring Boot 应用大部分使用 G1 垃圾回收器。G1 的核心参数不是堆大小而是预期暂停时间-XX:MaxGCPauseMillis。默认值往往是 200ms如果你希望接口响应更稳定可以调到 100ms。但暂停时间调得越短G1 会做更多背景回收可能导致吞吐量下降。所以这个值要结合你的业务容忍度反复测试。还有一点堆内存占满之前G1 会不断执行混合回收。真正需要避免的是“大对象直接进入老年代”和“Full GC”。在真实场景里我曾用-XX:PrintGCDetails看到频繁 Full GC最后定位是本地缓存存了太多对象导致老年代被打爆。后来改用 Caffeine 的 maximumSize 限制容量问题马上缓解。GC 日志是性能调优的宝藏别只盯着 JVM 堆大小。5.3 分层 JAR、启动预热与 GraalVM 的取舍Spring Boot 2.3 开始支持分层 JAR这虽然不是直接提升运行性能但对 DevOps 部署效率帮助很大。把 JAR 分为依赖层和业务层后Docker 构建时只有业务代码变化才需要重新上传依赖层镜像构建和发布速度大幅提升。启动预热是老生常谈第一次接口调用往往因为类加载、JIT 编译而慢项目方可以在启动后主动调用几个核心接口完成预热。关于 Spring Boot 3 的 GraalVM Native Image我要给个谨慎的评价启动速度和内存占用确实大幅下降但 Spring 生态里的反射、动态代理都是大坑很多中间件不兼容。如果只是追求启动快可以用 CRaC 或者简单的启动预热替代只有当你的服务需要快速扩容、且对内存要求极度敏感时再考虑 Native Image。把它当成核武器可以但别轻易亮出来。5.4 Spring Boot 2.3 / 2.6 / 3.x 的版本差异与性能影响这几个版本我都有升级过说说和性能有关的差异。Spring Boot 2.6 默认禁止了循环依赖从结构上避免了 Bean 初始化时的性能浪费同时它的 Spring MVC 路径匹配从AntPathMatcher换成更快的PathPatternParser在路由较多的系统里解析性能有提升。Spring Boot 3 相对 2.x 最大的变化是 JDK 17 基线可利用虚拟线程在 Spring Boot 3.2 中 preview对高并发阻塞场景帮助巨大。我建议不要盲目升级大版本但如果你还在 2.1、2.3且项目满足升级条件升到 2.6 或 3.x 本身就是一次性能提升。比如在 2.6 的 PathPatternParser 在大量接口模式下静态路径匹配比 2.3 快了一个数量级。升级时重点关注配置变化尤其是spring.mvc.pathmatch.matching-strategy相关的适配。6. 监控与可观测性能调优的地图6.1 Spring Boot Admin 与 Actuator 快速搭建没有监控就做优化相当于闭眼开车。Spring Boot Admin 是最快的选型服务端引入spring-boot-admin-starter-server客户端引入spring-boot-admin-starter-client几行依赖就能看到所有应用的内存、线程、HTTP 接口和 GC 数据。特别适合中小团队在没有专业监控平台时快速用起来。用 Spring Boot Admin 能直接看到每个接口的调用次数和耗时还能查看堆内存使用曲线。我自己用的时候最常用到的是“线程”和“堆内存”面板。有一次系统告警我打开 Admin 发现某个线程数飙升顺藤摸瓜找到是外部短信服务调用超时且没有设置超时时间所有线程都卡在等待响应。加上连接超时和读超时后问题立刻解除。6.2 Micrometer Prometheus 指标采集Spring Boot Admin 适合人工查看但告警和长期趋势还是 Micrometer Prometheus Grafana 的组合更好。Spring Boot 3 内置了 Micrometer只需要引入micrometer-registry-prometheus依赖然后在配置里暴露prometheus端点。这样 Prometheus 就能每 15 秒抓取指标。我通常会自定义几个业务指标。比如用Timer记录某个核心接口的耗时分布用Counter统计缓存命中次数。这样可以把“性能优化是否有效”变成一个客观数字而不是压测时跑一次就结束。举一个例子上线缓存后我通过 Grafana 面板看到库存查询接口的耗时从 P95 350ms 降到 P95 40ms这才能确认优化生效。6.3 两个真实线上问题的排查实录再分享两个真实问题。第一个办公用品申请接口偶发超时压测时看不到一到生产就有。通过 Admin 和 Arthas 的 trace 命令发现是 Redis 连接池满了。原来有段代码在高并发下每次都 new 一个 Jedis 对象而不是用连接池。换成 Spring 的 RedisTemplate 共享连接池后超时消失。第二个服务整体响应时间正常但内存曲线一直上涨。看 GC 日志发现老年代不断膨胀最终定位到 session 中存储了不必要的用户分页查询结果。解决办法是限制 session 存储内容并给 Redis 缓存加最大内存容量。这两个问题都不复杂但没有监控和动态工具只靠看代码我估计要排查好几天。7. 避坑经验速查表常见问题根本原因解决方案Tomcat 线程调大后性能反而下降线程切换开销和连接池瓶颈逐档压测结合数据库连接池配置调整Async 不生效方法调用发生在同类内部AOP 代理未触发注入代理 Bean或者拆分逻辑到另一个 Bean缓存击穿导致数据库打崩热点 key 过期瞬间大量并发回源逻辑过期 单线程刷新先返回旧值Redis 缓存数据乱码默认使用 JDK 序列化器改为 Jackson JSON 或自定义序列化器分页页数越深越慢LIMIT 深翻页扫描大量数据换成游标分页或基于覆盖索引优化服务器频繁 OOMJVM 堆内存超过容器限制设置 -XX:MaxRAMPercentage预留堆外内存WebSocket 连接一多就无响应消息处理阻塞 IO 线程将消息处理提交到独立业务线程池接口偶尔慢几秒第三方调用未设置超时或连接池泄漏统一设置连接/读取超时开启连接池泄漏检测升级 Spring Boot 后接口报 4042.6 开始的 PathPattern 策略差异检查适配器和相关配置必要时调整匹配策略除了表格里的问题我再补充一个很多人容易忽略的心得性能优化一定要记录基线。每次改动前记录当前的 QPS、耗时和负载改动后再记录一组放在同一个实验里对比。如果改进没有带来数据变化就果断回滚不要自我感动。最后一个技巧压测时不要只测平均响应时间一定要看 P95、P99 和 Error Rate。平均时间被几个高性能请求拉低实际情况可能是大部分用户都在痛苦等待。我所有性能优化项目的验收标准都是看 P95 是否达到目标而不是平均值。这个项目后续如果想继续做深可以把缓存、线程池和数据库连接池的参数都做成配置中心管理配合监控平台做动态调整。对我来说这次办公用品系统的优化经验最大的价值不是提升了某一台机器 500% 的吞吐量而是让团队建立起一套“先测量、再优化、用数据复盘”的工作方式。互联网应用没有一劳永逸的性能方案只有当每个改动都有监控支撑时速度提升才是可持续的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑