我做过多年的 Spring Boot 性能调优接手过不少“看着能跑、一上量就熄火”的系统。这次要聊的项目是一个基于 Spring Boot 的企业办公用品管理系统从需求设计到上线压测我们最后把核心接口的吞吐量提升了将近 5 倍也就是标题里说的“500%”。很多人一听到性能优化就激动想找一把能解决所有问题的“核武器”但真正落地过的人都知道性能优化从来不是单点操作而是从监控、JVM、容器、数据访问到架构取舍的一整套组合拳。这篇文章我尽量把这些年的经验一次讲透适合正在维护 Spring Boot 2.x 或 3.x 项目、准备升版本、或者已经在用 Spring Boot Admin 和 Actuator 做监控的朋友。内容不绕弯子全部按实操顺序来。1. 项目全貌企业办公用品管理系统的调优思路先交代清楚项目背景方便后面所有参数和结论对照。这个系统主要管理办公用品的申购、入库、领用、库存盘点、供应商台账模块不算复杂但对事务一致性要求高而且最要命的是“月末月初”这种固定时间点全公司的行政人员和部门助理会集中提交领用申请高峰流量瞬间拉满。初期系统表现是什么样本地开发跑得飞快一到生产环境就原形毕露月末第一天上午页面点击领用申请要等 3 到 5 秒库存查询接口大量超时数据库连接一度被耗光应用日志里全是连接获取超时的异常。当时第一反应是数据库慢但把 SQL 拿出来看单条查询都走得了索引数据库 CPU 也不算高。排查到最后问题反而出在应用这一层JVM 参数没动过、Tomcat 线程池还是默认值、HikariCP 连接池配置偏保守、缓存基本没做再加上第三方对接接口都塞在主应用里高峰时互相挤占资源。所以这篇文章的顺序就是我实际调优的顺序先不急着动代码先把“能看见问题”的能力建起来再逐个层次去压、去调、去验证。你可能会问为什么不直接上缓存、加机器因为如果没有一个可信的基线缓存加在哪儿、加多大、加了之后到底有没有效果全凭猜。性能调优最怕的不是没有优化手段而是不知道当前瓶颈在哪里、优化效果怎么衡量。我把这个项目当作一个完整案例来讲里面的配置参数、压测步骤、踩坑记录都是真实可复用的。项目开发环境用的是 IntelliJ IDEA 社区版社区版没有自带 Spring Initializr但通过 start.spring.io 网页生成工程再用 Maven 导入体验完全不受影响这一点后面在实操环节也会提到。2. 先搭可观测体系Actuator、Admin 和压测基线一个都不能少2.1 打开 Actuator 端点一行配置让指标可查Spring Boot 最容易被忽略的功能就是 Actuator。很多项目上了生产第一件事就是把 Actuator 相关依赖去掉理由是“暴露太多端口不安全”这其实是不了解配置的正确做法。你要是把management.endpoints.web.exposure.include*一股脑全暴露那确实不安全但只暴露health,info,metrics,prometheus这几个端点既不会泄露业务数据又能让监控系统正常采集。在application.yml里我一般这样配management: endpoints: web: exposure: include: health,info,metrics,prometheus endpoint: health: show-details: always metrics: tags: application: office-supply-adminSpring Boot 2.x 和 3.x 这里基本一致只有包名和底层依赖不同。如果你想让 Prometheus 能直接抓指标记得额外引入dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId /dependency加上之后访问/actuator/prometheus就能看到 JVM 内存、GC 次数、HTTP 请求耗时、线程池状态、数据库连接池状态等一系列指标。注意 Actuator 自动装配在 Spring Boot 3 里默认启用了但不暴露只看日志容易误以为没生效。判断的捷径是访问/actuator根路径如果返回一个链接列表说明端点管理器已经起来了。2.2 用 Spring Boot Admin 替代人工盯日志Actuator 解决了“数据有没有”的问题但日常运维不能让我天天盯着 Prometheus 的指标曲线团队需要一个直观的管理台。Spring Boot Admin 这时候就很合适。它不是性能调优的“加速器”而是调优过程的“仪表盘”能把 Actuator 吐出来的 JSON 数据渲染成图表和状态页。搭建方式很简单。新建一个独立的 Spring Boot 工程作为 Admin Server加上依赖dependency groupIdde.codecentric/groupId artifactIdspring-boot-admin-starter-server/artifactId /dependency启动类上加EnableAdminServer然后被监控的客户端项目引入dependency groupIdde.codecentric/groupId artifactIdspring-boot-admin-starter-client/artifactId /dependency客户端配置只要给出服务端地址spring: boot: admin: client: url: http://localhost:8081 instance: name: office-supply-admin这样一来哪个实例的堆内存涨了、GC 频繁了、线程数异常了都能在一个页面里看到。Spring Boot Admin 对于多实例部署尤其好用它能按应用名聚合所有实例的健康状态调优时切换参数后观察效果非常方便。2.3 压测基线怎么定JMeter 参数与执行步骤调优之前必须有一份可信的基线数据。我用 JMeter 压 TCP 业务的习惯是先选定几个代表性接口比如库存查询、领用申请提交、月度统计报表分别代表高读、混合写、CPU 密集三种场景。压测任务用命令行执行方便记录jmeter -n -t office-supply-query.jmx -l result-query.jtl -e -o report-query线程组设置我建议这样起步先跑 100 线程、每条线程循环 10 次、没有思考时间也就是常说的零思考时间压测观察 TPS 和响应时间分布。别一上来就把线程数拉到 1000那不是压测是直接把系统打死得不到任何有意义的瓶颈数据。基线结果记录成表后面每一步改动再压一次对比才有说服力。我的基线是这样接口初始 TPSP95 响应时间CPU 使用率库存查询1801200ms62%领用申请提交952100ms78%月度统计报表403800ms92%这个基线说明了一个很典型的问题系统并没有被单一资源打满而是已经进入多线程竞争导致的资源内耗。CPU 62% 时接口就出现高延迟绝对不是 CPU 不够而是应用线程互相等待、锁竞争、IO 阻塞造成的。接下来开始调。3. 系统层优化JVM 与 Tomcat 的常见套路3.1 调整 JVM 内存与 GC 参数先把 Full GC 压下去Spring Boot 项目的 JVM 参数经常被忽略因为大家习惯在 IDEA 里直接点运行用的都是当前 IDE 给的那套默认参数。一放到 Linux 服务器上如果不显式设置堆内存JVM 只拿物理内存的 1/4 作为默认最大堆这在 8G 内存的服务器上只能分到 2G很容易导致频繁 GC而 GC 一旦频发接口响应时间就会像过山车一样抖动。我这次给服务分配的是 4G 堆内存服务器总内存 16G预留空间给操作系统缓存和数据库java -Xms4g -Xmx4g -XX:UseG1GC -XX:MaxGCPauseMillis100 \ -XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/opt/logs/ \ -jar office-supply-admin.jar-Xms和-Xmx设成一致好处是避免 JVM 在运行过程中反复扩展堆空间这个“扩容”操作本身是有开销的。G1 在 JDK 11 之后已经是默认回收器但对延迟敏感的应用显式声明并限制MaxGCPauseMillis能让 GC 停顿更可控。加HeapDumpOnOutOfMemoryError是给自己留后路真出 OOM 时能拿到堆转储文件分析不用抓瞎。在这一步我们只调整 JVM不碰业务代码然后重新压测库存查询接口。结果是 TPS 从 180 涨到 260P95 降到 900ms 左右。这是最廉价的提升但远没到 5 倍的程度。顺带说一句JVM 参数不是越大越好堆设成 6G 甚至 8G 时如果 G1 区域太大反而会造成更长的 GC 停顿调参要配合 GC 日志来看别盲目加内存。3.2 让 Tomcat 线程池匹配真实负载Tomcat 是 Spring Boot 默认的内嵌容器很多项目上线后完全不碰它的线程池配置。Spring Boot 2.3 之前用的是server.tomcat.max-threads2.3 及以后改成了server.tomcat.threads.max从命名规范上就更像“配置一个线程组”。如果你在旧版本配置项写进新版本启动时会发现参数被忽略这个坑我掉过看日志根本看不出问题因为 Spring Boot 对未知配置项默认不做严格校验。最终配置如下server: tomcat: threads: max: 300 min-spare: 30 accept-count: 300 max-connections: 10000 connection-timeout: 20000 keep-alive-timeout: 30000max是最大工作线程数min-spare是常驻空闲线程数accept-count是等待队列容量max-connections是 Tomcat 能同时接受的 TCP 连接数。这四个参数组合起来才是一个完整的流量容纳模型TCP 连接先进入max-connections的通道超过后进入accept-count队列队列满了新连接才会被拒绝。怎么确定自己的值先看服务器的 CPU 核数。如果应用是计算密集型的线程数可以设在核数的 2 到 4 倍如果是 IO 密集型比如要做大量远程调用和数据库访问可以适当提高但不要堆到 1000。我压测时把max从默认的 200 提到 300TPS 从 260 涨到 410P95 降到 600ms。这里有个典型误区线程数越多吞吐不一定越高因为线程切换本身也消耗 CPU。我们线上曾经为了压榨性能把 max 调到 500结果 P95 不降反升因为上下文切换把 CPU 耗光了。3.3 从连接数倒推线程池一次完整推演Tomcat 工作线程和数据库连接池的关系值得单独说。很多文章只给出一堆配置值不讲推导逻辑结果读者照抄后发现线程池加大数据库连接池却不够用核心接口依旧报连接超时。一个请求从 Tomcat 线程池拿到工作线程然后去 HikariCP 取数据库连接如果请求处理过程中始终占用一个连接那么“峰值并行请求数”必须小于等于“数据库连接池可用的连接数”。我压测时用 300 个 Tomcat 线程但连接池默认最大连接数是 10等于有 290 个线程在排队等连接。这相当于高速公路上有 300 辆车但只有 10 个收费站表面上车道加宽了通行效率反而更差。倒推方式很简单观察高峰期的活跃连接数假设数据库中能看到 35 个活跃连接那就把连接池 maximum-pool-size 设到 50留一点余量同时把 connection-timeout 调低到 3 秒争取让调用方快速失败而不是无限阻塞。我当时把连接池从 10 提到 50TPS 直接窜到 760P95 降到接近 300ms。这个环节才是“500%”的推动力。4. 数据链路加速连接池、缓存与 SQL 手段4.1 HikariCP 连接池配置背后的讲究Spring Boot 2.x 和 3.x 默认使用 HikariCP这是好事因为 HikariCP 本身性能在同级连接池里表现突出。但默认配置只是“能跑”不是“跑得高效”。我推荐这样调整spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10 connection-timeout: 3000 idle-timeout: 600000 max-lifetime: 1800000maximum-pool-size不是越大越好它受数据库最大连接数和实际并发量双重约束。假设 MySQL 的 max_connections 是 200你的应用有 3 个实例那每个实例最多只能分到 60 左右否则会出现数据库侧拒绝连接的问题。minimum-idle保持少量常驻连接避免低峰期突然来流量时临时建连max-lifetime小于数据库的 wait_timeout防止数据库把连接回收而连接池还以为连接可用。你可能会问连接池加快了SQL 本身不做优化吗当然要做。连接池解决的是“拿不到连接”的问题不解决“一条 SQL 跑 3 秒”的问题。两个问题必须分开看待。4.2 用 Spring Cache 与 Redis 缓存扛住高频读办公用品系统里库存查询和用品基本信息查询是典型的高频读场景。这类数据的特征是读多写少而且结构固定。压测基线里库存查询的 P95 高达 1200ms其中真正的 SQL 执行时间只占一部分序列化、网络传输、竞争等待都在拖耗时。我引入 Spring Cache 时没有直接上 Redis先用 Caffeine 本地缓存验证效果。先在 pom 里引入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-cache/artifactId /dependency dependency groupIdcom.github.ben-manes.caffeine/groupId artifactIdcaffeine/artifactId /dependency配置缓存策略spring: cache: type: caffeine caffeine: spec: maximumSize5000,expireAfterWrite300s,recordStats然后在 Service 方法上加注解Cacheable(value stock, key #officeSupplyId) public StockInfo getStockInfo(Long officeSupplyId) { // 原本的数据库查询逻辑 }本地缓存的效果立竿见影库存查询接口的 P95 从几百毫秒直接降到两位数。但本地缓存有一个明显问题多实例部署时每个实例各存一份数据一致性要自己用版本号或过期时间兜底。所以后来我又把全局共享的热点数据放到了 Redis配置上用 Spring Data Redis 的标准连接信息spring: data: redis: host: 10.0.0.15 port: 6379 timeout: 500ms lettuce: pool: max-active: 32 max-idle: 16 min-idle: 8用 Redis 之后整体 TPS 又上了一个台阶因为库存查询不再频繁打数据库。不过可以明确告知缓存不是万能药写频繁或强一致性的场景不能乱加缓存。办公用品管理里的库存扣减涉及事务我没在这里套缓存而是单独优化了事务边界和锁粒度。4.3 索引与 SQL 优化最便宜也最容易被忽视的提升项目初期数据量不大时任何 SQL 都感觉“挺快”。一旦库存流水表积累到几十万行年初年末的汇总统计就会原形毕露。月度统计接口基线只有 40 TPS多半不是连接池问题而是聚合查询在拖后腿。我打开数据库慢查询日志定位到这样一类 SQL对出入库流水表和用品表做关联再按月份分组聚合。表面上关联字段都建了索引但统计时用了DATE_FORMAT(create_time, %Y-%m)做分组导致索引失效全表扫描。优化方式无非三种把create_time用范围条件直接查而不是函数处理后过滤增加一个statistics_month冗余字段写入时计算好查询时直接分组月度报表只查汇总表不走明细表我当时采用了前两种组合。改完之后这个接口的 TPS 从 40 涨到 110虽然绝对值不高但在这个接口上耗时已经不再是瓶颈。SQL 优化这事儿不一定非得是慢查询专家才能做第一步永远是先开慢查询日志、抓出最耗时的前几条逐个击破。5. 进阶改造WebSocket、第三方接口与版本升级的取舍5.1 Spring Boot 集成 WebSocket 的 YAML 配置与常见坑办公用品管理系统里有一个“库存变动实时通知”的功能行政人员希望领用申请提交成功之后仓库管理员能立刻收到弹窗提醒。这个需求适合用 WebSocket 做。网上很多教程一上来就摆一大段 Java 配置类却忽略了application.yml里也可以设置 WebSocket 容器相关参数。在 Spring Boot 3 中WebSocket 的“协议启用”主要通过代码完成YAML 主要负责控制 Tomcat 的 WebSocket 容器行为server: tomcat: websocket: read-timeout: 30000 buffer-size: 8192read-timeout控制空闲连接读取超时时间buffer-size影响文本消息缓冲大小。如果你用的是原生WebSocketHandler注册方式不做 STOMP 代理那么大部分连接参数就在这两项里。实际工作中我会推荐直接用 Spring 的TextWebSocketHandler配合WebSocketConfigurer注册Configuration public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(stockNoticeHandler(), /ws/stock-notice) .setAllowedOrigins(*); } Bean public WebSocketHandler stockNoticeHandler() { return new StockNoticeHandler(); } }这里有个坑WebSocket 连接默认受 Tomcat 工作线程池约束如果连接数上来工作线程池可能被长连接占满。调优时不要只盯着 HTTP 接口还需要观察/actuator/metrics/tomcat.threads.busy指标。我的处理方式是把 WebSocket 连接单独部署在一个独立端口或者独立服务里避免和普通 HTTP 接口抢占线程。5.2 对外提供的接口第三方调用单独服务还是放在主应用开发时经常遇到一个决策第三方调用的接口是放在主 Spring Boot 应用里还是单独拆一个服务。优化过程中这个问题越放越大因为对外接口的调用方不在自己控制范围内第三方重试、慢调用、超时波动都会影响主应用的线程池。我的建议分三种情况如果只是两三个内部系统之间的简单接口人数很少的团队可以先放在主应用但必须做熔断、限流、独立线程池隔离。如果接口量大或者调用方非常多直接把对外接口拆成独立模块甚至独立服务更稳妥。光用Async加线程池都不够因为容器层 Tomcat 线程池依然共享。拆出去之后第三方一次慢调用最长可以拖死主服务的问题就不存在了因为主服务只依赖内部接口超时可控。我在这个项目里把供应商对接、库存同步接口挪到了独立的external-api服务里用 YAML 配置了这个服务的限流参数和客户端超时。spring: cloud: openfeign: client: config: default: connect-timeout: 3000 read-timeout: 10000配合 Sentinel 或者 Resilience4j 做熔断第三方接口单方面变慢时主服务的响应时间不会再被拖垮。5.3 从 Spring Boot 2.3.x 到 3.x 升级时要注意的细节为什么调优时会牵扯到版本升级因为这个项目的根目录代码很老当时跑在 Spring Boot 2.3.x 上依赖的底层库已经有大量安全修复和性能改进。而且 Spring Boot 3.0 之后支持虚拟线程对高并发 IO 场景可能带来质变。不过升级不是一句“换个版本号”那么简单。从 2.3.x 升到 3.x 有几个必须处理的变化JDK 版本要从 8 升到 17 及以上编译器也要对应调整javax.*命名空间全面迁移到jakarta.*尤其是javax.servlet、javax.persistence、javax.validation配置文件中server.tomcat.max-threads换成server.tomcat.threads.max部分自动配置类改名需要检查自定义配置类是否引用了旧的包路径2.6.x 中间还引入了spring.mvc.pathmatch.matching-strategy的默认值调整Springfox Swagger 这类库在 2.6 下容易启动报错。如果你现在项目还在 2.3.x建议先小范围升级到 2.6.x验证无重大影响后再考虑是否到 3.x。没必要追求“最新最酷”但对一个已经做了性能调优的项目底层框架如果太旧很多优化手段会受制于框架自身瓶颈。5.4 同场对比Spring Boot 3 与 Python FastAPI 的选型思考调优过程中团队里有人提过一个想法既然对外接口只做转发和数据聚合是不是可以用 Python FastAPI 来写性能更好这个思路在轻量、IO 密集、少量计算场景里有一定道理。FastAPI 基于 ASGI 异步框架天然支持高并发连接很多简单 API 性能测试里数值很好看。但实际项目不能只看单接口跑分。Spring Boot 3 的优势在于生态完整、事务管理成熟、监控体系通吃尤其像办公用品管理系统这种涉及事务一致性、库存扣减、多模块 RBAC 权限的复杂业务用 FastAPI 重写成本会非常高。FastAPI 更适合那些无状态、数据通过 HTTP 调用其他服务、主要做聚合转发的场景比如对外开放的查询网关。我给团队的建议是不要为了性能指标重写系统。优化到这一步系统瓶颈已经不在语言框架本身而在连接、IO、缓存、架构隔离这些通用问题上。FastAPI 也许能在特定接口上跑得更快但迁移成本和维护成本远高于继续优化 Spring Boot 现有代码。6. 复盘数据与实际效果优化不是最后一锤子6.1 三次压测数据对照调优接近尾声时我重新跑了一遍和基线完全一样的 JMeter 脚本指标如下接口优化前 TPS优化后 TPS提升比例P95 优化前P95 优化后库存查询1801020约 5.6 倍1200ms180ms领用申请提交95480约 5 倍2100ms420ms月度统计报表40115约 2.9 倍3800ms900ms可以清楚看到库存查询这种“读多写少”的接口改善最大核心原因是缓存和连接池双重生效写操作和复杂统计的提升相对小但也能达到 3 倍左右。整体体验就是标题里说的“速度提升 500%”而实际付出的工作并不是某一个惊天动地的技术而是把每一层都抠出来的。6.2 调优验收集照着这份清单避坑最后给正在看这篇文章的你一份可落地的核对清单这些都来自我这次项目里的真实踩坑记录不是官方的推荐模板先确认监控到位再动手没有 Actuator 数据就盲目调参等于闭眼开车压测基线必须存在本地记录每调一个参数保存一次方便回滚和归因JVM 堆内存不要超过服务器物理内存的一半留足系统缓存和数据库空间Tomcat 线程池、连接池、数据库连接数要联动计算单独调大一个必然出问题缓存只用于读多写少、容忍短暂不一致的数据库存扣减绝不套缓存升级 Spring Boot 大版本前先看依赖清单javax到jakarta的替换要全局搜索第三方对外接口要么独立线程池隔离要么独立服务别在业务高峰期赌对方系统稳定WebSocket 长连接必须监控 Tomcat 工作线程占用发现 busy 被占满就考虑独立部署我个人的实际体会是这次项目最让我意外的不是哪个参数效果最好而是“认真做监控 记录基线”带来的确定性。很多性能问题看起来神秘其实就是因为没有可比较的数据。你只要把问题拆成 JVM、容器、数据源、缓存、架构五个层次逐个验证速度提升 500% 并不是夸张的说法是一步一步能走到的结果。最后再分享一个小技巧调优完后把整套压测脚本、监控面板、参数配置全部提交到工程仓库的 docs 目录。过了两个月再回头看你会庆幸当初留了这份“现场记录”否则很多调优决策动机早就忘了下次同类问题还得从头踩一遍坑。
阅读完成 · 觉得有帮助?