大家有没有发现开启虚拟线程后:
很多出现了诡异反向现象:
- 单机并发轻松上万,QPS 大幅提升
- 但高峰期数据库连接直接打满、大量请求排队超时
- 以为虚拟线程无敌,结果崩得比平台线程更快
核心原因: 虚拟线程只管 "扛并发",不管 "数据库瓶颈"。启用虚拟线程后,可以同时挂起大量请求,但真正执行 SQL 的并发量仍然受 HikariCP 连接数限制。
虚拟线程可以轻松创建成千上万个、几乎无开销,但 MySQL 连接是有限的 。 无数虚拟线程同时抢几十个数据库连接,直接触发: PoolTimeoutException: Connection is not available
bash
大量 HTTP 请求
↓
大量虚拟线程
↓ 等待连接时挂起
HikariCP(例如最多 25 个连接)
↓
数据库最多并发处理约 20 个连接
目前存在两个极端错误认知:
- 老公式无脑套:
CPU*2+IO,并发上来直接排队雪崩 - 以为虚拟线程无上限,直接把连接池拉到 100+,打爆 MySQL
今天结合线上真实压测 + 生产事故,输出虚拟线程专属的 HikariCP 调优逻辑、唯一正确公式、4 个新版独有致命坑、完整上线配置。
一、先来看下虚拟线程为什么会打崩连接池
传统平台线程(JDK8/JDK17)
- 线程昂贵、数量有限(默认几十~几百)
- 并发上不去,天然帮数据库 "限流"
JDK21 虚拟线程(本质变化)
- JVM 轻量级管理、M:N 调度、创建无压力
- 瞬时可以爆发数千并发数据库请求
- 完全没有线程层面的限流保护
👉 最终真相 虚拟线程解决的是 CPU / 线程调度瓶颈 解决不了 MySQL 连接数、锁、事务、IO 瓶颈
下游数据库连接池不变,上游并发放大 10 倍 = 排队爆炸、连接耗尽
二、全网纠正:虚拟线程环境不能再用老公式
❌ 老错误公式(平台线程适用,虚拟线程直接废)
maximum-pool-size = CPU核心数*2 + IO数
4 核机器算出来只有 10 左右 虚拟线程上万并发瞬间挤爆队列,大量请求超时。
✅ 虚拟线程【生产唯一正确配比逻辑】
适配 SpringBoot3 + JDK21 + 短事务业务
- 不再绑定 CPU,绑定数据库承压能力
- 单机默认安全阈值:20~35
- 多实例部署:单实例连接数 < MySQL 最大连接上限(默认 151)
- 慢 SQL / 长事务多的业务:适度上浮,不超 40
- 严禁无脑拉到 100+,会导致 MySQL 上下文切换、锁竞争炸裂
- 可以先用下面的方法估算。 按数据库总连接预算分配 假设:
- 数据库允许 300 个连接;
- 为管理员、监控和临时任务预留 60 个;
- 共有 6 个应用实例。 则单实例上限约为: (300 - 60) ÷ 6 = 40 这只是连接预算上限,不代表最优值。可以从每实例 10~20 个连接开始压测。
按连接占用时间估算 使用类似 Little's Law 的方式: 所需连接数 ≈ TPS × 平均数据库连接占用时间 例如: 800 TPS × 0.025 秒 = 20 个连接 可以从 20~25 个连接开始验证。如果增加连接数后:
- TPS 不再增长;
- 数据库 CPU 上升;
- SQL 延迟变长;
- 锁等待增加; 说明数据库已经接近饱和,继续增加连接通常只会恶化延迟。HikariCP 也强调连接池并非越大越好,应以部署环境和实际压测结果确定。
极简生产结论
虚拟线程:小连接池 + 极短事务 + 快进快出 = 最高吞吐
三、SpringBoot3 开启虚拟线程(标准配置)
1. pom 依赖(SpringBoot3.2 + 自带支持)
无需额外引入,原生适配
2. application.yml 开启虚拟线程
yaml
spring:
threads:
virtual:
enabled: true
开启后:所有 Web 请求自动使用虚拟线程处理
四、虚拟线程专属 HikariCP 生产最终配置
适配:高并发、虚拟线程、短事务、SpringBoot3 最佳实践
yaml
spring:
datasource:
hikari:
# 虚拟线程专属区间:20~35, 生产环境必须根据数据库能力确定
maximum-pool-size: 25
minimum-idle: 8
# 等待超时适当放大(应对瞬时高并发排队)
connection-timeout: 40000
# 空闲连接销毁
idle-timeout: 600000
# 连接最长生命周期,防止数据库主动断开,应略短于数据库或代理的连接超时
max-lifetime: 1800000
# 保活探测,解决云服务器防火墙断连
keepalive-time: 60000
重点区别旧版本: 平台线程 max 10 虚拟线程 max 20-35
启用虚拟线程后,传统的 server.tomcat.threads.max、spring.task.execution.pool.* 等线程池参数可能不再按原来的方式起作用,因为任务不再由固定大小的平台线程池执行。 HikariCP 的 maximum-pool-size 仍然有效。连接被全部借出后,后续虚拟线程会在获取连接的位置等待,超过 connection-timeout 后抛出异常。
五、以上配置完我们来看一个简单的Demo
验证请求是否运行在虚拟线程中
java
@RestController
@RequestMapping("/virtual-thread")
public class VirtualThreadController {
private final JdbcTemplate jdbcTemplate;
public VirtualThreadController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@GetMapping("/check")
public Map<String, Object> check() {
Thread thread = Thread.currentThread();
Integer result = jdbcTemplate.queryForObject(
"select 1",
Integer.class
);
return Map.of(
"thread", thread.toString(),
"virtual", thread.isVirtual(),
"result", result
);
}
}
访问:GET http://localhost:8080/virtual-thread/check 返回结果中的 virtual 应当是 true。
模拟连接池饱和 下面的接口会占用数据库连接约两秒,仅用于测试:
java
@Service
public class SlowQueryService {
private final JdbcTemplate jdbcTemplate;
public SlowQueryService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Transactional(readOnly = true)
public Map<String, Object> execute() {
jdbcTemplate.execute("select sleep(2)");
Thread thread = Thread.currentThread();
return Map.of(
"virtual", thread.isVirtual(),
"thread", thread.toString()
);
}
}
java
@RestController
@RequestMapping("/test")
public class TestController {
private final SlowQueryService slowQueryService;
public TestController(SlowQueryService slowQueryService) {
this.slowQueryService = slowQueryService;
}
@GetMapping("/slow-query")
public Map<String, Object> slowQuery() {
return slowQueryService.execute();
}
}
假设:
- 同时发起 1,000 个请求;
- HikariCP 最大连接数为 20;
- 每次查询占用连接两秒。 运行时大致是:
- 可以创建约 1,000 个虚拟线程;
- 只有约 20 个请求可以同时持有数据库连接;
- 其余虚拟线程等待连接;
- 等待超过两秒的请求可能出现连接获取超时。 这说明虚拟线程没有绕过连接池,只是显著降低了"等待连接"期间的线程资源成本。
六、虚拟线程 + 连接池 四大新版致命坑(只有 JDK21 会出现)
坑 1:虚拟线程无限并发 + 连接池过小 → 大规模排队超时
现象:接口不报错、线程不阻塞、但是大量慢响应、超时 原因:虚拟线程太多,抢少量连接,全部卡在获取连接阶段
✅解决:使用上面虚拟线程专属阈值,不再套 CPU 公式
坑 2:线程钉扎(Pinning)导致连接不归还、隐性泄露
虚拟线程遇到 synchronized 同步块、部分老旧锁、旧驱动 会发生 线程钉扎 ,虚拟线程无法卸载、阻塞挂起 导致连接长期占用不释放,慢慢耗尽池内连接
高危场景:
- 老 SDK、老工具类大量 synchronized
- MySQL 驱动版本过低
- HikariCP 版本低于 5.1.0
✅修复方案
- 升级核心依赖
- MySQL 驱动 ≥ 8.0.33
- HikariCP ≥ 5.1.0
- 优先使用
ReentrantLock替代 synchronized - 测试环境开启钉扎监控
ini
-Djdk.tracePinnedThreads=short
坑 3:长事务在虚拟线程环境破坏力翻倍
传统线程:长事务顶多拖垮几个线程 虚拟线程:一个长事务占一条连接,上千虚拟线程排队
后果:
- 少量慢 SQL 直接打满连接池
- 服务瞬间雪崩
✅铁律 虚拟线程项目:事务必须极致短、禁止事务内 RPC/HTTP/ 大循环
坑 4:误以为虚拟线程可以多线程共用事务
Transaction、ThreadLocal 依旧线程隔离 虚拟线程依旧不能跨线程传递事务 子线程操作数据库依旧是独立事务、不会回滚
七、推荐生产策略
1. 连接池调优铁律
- 平台线程:看 CPU
- 虚拟线程:看数据库承压
2. 代码铁律
- 永远不手动获取 Connection
- 若必须手动,先写资源闭环、再写业务
- 全部使用 try-with-resources / 回调模板
3. 事务铁律
- 虚拟线程环境零容忍长事务
- 所有 RPC、HTTP、耗时计算、大 IO 全部移出事务
- 耗时业务一律队列异步化
八、虚拟线程环境正确排查链路
出现连接耗尽,排查顺序完全变了:
- 查是否存在 线程钉扎 阻塞连接释放
- 查是否存在 长事务霸占连接
- 查慢 SQL 耗时
- 最后再微调连接池大小
旧顺序:先调大连接池 新顺序:先找阻塞泄露,再谈参数
九、生产 CheckList(SpringBoot3+JDK21 专属)
一套比较稳妥的落地方案是:
- 使用 Java 21+ 和 Spring Boot 3.2+。
- 开启 spring.threads.virtual.enabled=true。
- HikariCP 从每实例 10~20 个连接开始。
- 设置较短且合理的 connection-timeout,避免请求无限排队。
- 缩短事务,禁止事务内执行远程调用和长时间计算。
- 同时监控连接池 pending、SQL 延迟和数据库负载。
- 分别对平台线程和虚拟线程方案做相同压力测试。
- 只有在数据库仍有余量且连接等待明显时,才逐步增加连接池。
- 对慢报表、批处理和在线请求考虑使用独立连接池或独立数据源。
最终应形成这样的资源边界:
html
虚拟线程:承载大量并发请求
连接池:限制数据库访问并发
数据库:决定系统最终吞吐上限
超时与限流:防止等待队列无限增长
十、AI 代码局限性
AI 给出的连接池配置,99% 是 JDK8 老平台线程逻辑 无脑套用在虚拟线程环境,直接线上雪崩。 AI 不会区分线程模型差异,只会套固定公式。
虚拟线程提升的是系统吞吐、调度能力 不会提升数据库能力
SpringBoot3+JDK21 虚拟线程生产稳定的核心: 适度放大连接池 + 消灭长事务 + 杜绝钉扎阻塞 + 快取快还
不是越大越好,也不是越小越稳,适配模型才是最优解。
如果对你有帮助,欢迎点赞收藏,关注我不错过后续更新。不对的地方也欢迎大家指正。