1. 引言
单体 Spring Boot 应用在业务初期往往是最务实的选择:部署简单、调试方便、团队上手快。然而随着用户量增长,并发请求一上来,应用响应变慢、CPU 飙升、数据库连接池被打满,各种问题接踵而至。很多人第一反应是「拆微服务」,但拆服务成本高、周期长,未必是当下最优解。
其实在单体架构内,通过合理的配置调优、线程池治理、连接池优化、缓存引入和代码层面的并发改造,往往就能把并发能力提升数倍甚至一个数量级。本文从配置层、容器层、数据层、代码层四个维度,系统梳理单体 Spring Boot 提升并发数的实战手段。
2. 先搞清楚瓶颈在哪
提升并发数之前,先要回答一个问题:当前系统的瓶颈到底在哪? 盲目调参往往事倍功半。
常见的瓶颈点包括:
- CPU 密集:大量计算、加解密、序列化,CPU 使用率长期 90% 以上。
- IO 密集:频繁访问数据库、Redis、外部 HTTP 接口,线程大量阻塞在等待 IO。
- 内存受限:GC 频繁、堆内存不足,导致 Full GC 拖垮吞吐。
- 连接池耗尽:数据库连接池、HTTP 连接池被打满,请求排队超时。
- 锁竞争:同步块、分布式锁、数据库行锁竞争激烈,线程大量阻塞。
定位手段:
- 用
top、vmstat观察 CPU、上下文切换; - 用
jstack抓线程栈,看线程到底阻塞在哪; - 用 Arthas 的
dashboard、thread命令快速定位热点线程; - 用 APM 工具(SkyWalking、Prometheus + Grafana)观察接口耗时分布。
原则:先度量,再优化。没有数据支撑的调优都是碰运气。
3. 配置层:JVM 与 Spring Boot 基础调优
3.1 JVM 参数
合理的堆内存设置能显著减少 GC 停顿。示例:
bash
java -Xms4g -Xmx4g -Xmn2g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=100 \
-XX:+HeapDumpOnOutOfMemoryError \
-jar app.jar
要点:
-Xms与-Xmx设为相同值,避免运行时动态扩容带来的性能损耗;- 根据业务对象生命周期调整新生代大小;
- 开启 GC 日志,便于事后分析。
3.2 Spring Boot 内嵌 Tomcat 调优
Spring Boot 默认使用内嵌 Tomcat,核心配置如下:
yaml
server:
tomcat:
threads:
max: 200 # 最大工作线程数
min-spare: 20 # 最小空闲线程数
accept-count: 200 # 等待队列长度
max-connections: 10000 # 最大连接数
connection-timeout: 5000
注意:线程数并非越大越好。线程过多会导致上下文切换开销剧增,反而降低吞吐。一般经验值:
- CPU 密集型:
线程数 ≈ CPU 核数 + 1 - IO 密集型:
线程数 ≈ CPU 核数 × 2(配合虚拟线程可进一步放宽)
3.3 启用虚拟线程(JDK 21+)
如果项目已升级到 JDK 21,可以开启虚拟线程,极大提升 IO 密集型场景的并发能力:
yaml
spring:
threads:
virtual:
enabled: true
开启后,每个请求不再独占一个平台线程,而是由虚拟线程承载,线程数上限大幅提升,非常适合大量阻塞在数据库/Redis/远程调用的场景。
4. 容器层:连接池与线程池治理
4.1 数据库连接池
数据库连接池是单体应用最常见的瓶颈。以 HikariCP(Spring Boot 默认)为例:
yaml
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
连接池大小经验公式:
连接数 = ((核心线程数 × 2) + 有效磁盘数)
但更务实的做法是:压测得出最优值。连接池过小会排队,过大则数据库本身成为瓶颈。
4.2 HTTP 客户端连接池
如果单体应用需要调用外部服务,务必使用带连接池的 HTTP 客户端(如 RestTemplate + HttpClient、WebClient + Reactor Netty),避免每次请求都新建连接。以 Apache HttpClient 为例:
java
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(200);
cm.setDefaultMaxPerRoute(50);
4.3 业务线程池隔离
把不同业务的线程池隔离,避免某个慢接口拖垮整个应用。例如用 ThreadPoolTaskExecutor 单独处理异步任务:
java
@Bean("asyncExecutor")
public ThreadPoolTaskExecutor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
5. 数据层:缓存与查询优化
5.1 引入本地缓存 + Redis 多级缓存
缓存是提升并发最立竿见影的手段。热点数据优先走本地缓存(Caffeine),未命中再查 Redis,最后才落到数据库:
java
@Cacheable(cacheNames = "user", key = "#id")
public User getUserById(Long id) {
return userMapper.selectById(id);
}
5.2 数据库查询优化
- 为高频查询字段建立合适的索引;
- 避免
SELECT *,只查必要字段; - 分页查询用覆盖索引或游标分页,避免深分页;
- 复杂统计查询考虑异步落库 + 定时汇总。
5.3 读写分离
如果读多写少,可以配置主从数据源,读请求走从库,写请求走主库,显著降低主库压力。
6. 代码层:并发改造与性能热点优化
6.1 接口幂等与并发安全
高并发下要特别注意共享资源的线程安全:
SimpleDateFormat非线程安全,改用DateTimeFormatter或ThreadLocal;HashMap并发写会丢数据,改用ConcurrentHashMap;- 数据库扣减库存用乐观锁或
UPDATE ... WHERE stock > 0原子操作。
6.2 串行改并行
多个无依赖的远程调用或查询,可以用 CompletableFuture 并行执行,缩短接口响应时间:
java
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.get(id));
CompletableFuture<Order> orderFuture = CompletableFuture.supplyAsync(() -> orderService.get(id));
User user = userFuture.get();
Order order = orderFuture.get();
6.3 热点代码优化
- 减少不必要的对象创建,避免频繁 GC;
- 用
StringBuilder替代字符串拼接; - 避免在循环中做数据库查询(N+1 问题);
- 大对象、大集合操作尽量异步化或分片处理。
7. 压测验证:让数据说话
调优完成后,必须用压测验证效果。推荐工具:
- JMeter:简单易用,适合接口级压测;
- wrk:命令行工具,适合快速压测;
- Arthas:线上问题定位,观察线程、GC、方法耗时。
压测要点:
- 逐步增加并发数,观察吞吐量(TPS)和响应时间(RT)的变化曲线;
- 关注 P99 延迟,而不是只看平均值;
- 压测环境尽量贴近生产(数据量、硬件配置)。
8. 总结
单体 Spring Boot 提升并发数,核心思路是先定位瓶颈,再分层优化:
| 层次 | 关键手段 |
|---|---|
| 配置层 | JVM 参数、Tomcat 线程数、虚拟线程 |
| 容器层 | 数据库/HTTP 连接池、业务线程池隔离 |
| 数据层 | 多级缓存、索引优化、读写分离 |
| 代码层 | 并发安全、并行化改造、热点优化 |
单体架构的并发天花板并不低,关键在于系统性地排查瓶颈、有针对性地调优。当单体经过充分优化仍无法满足业务需求时,再考虑拆分微服务也不迟。