# Spring Boot 3.5 开虚拟线程后Hikari 连接池按库能力定容别按 VT 数量扩池开虚拟线程后 Tomcat 池顶消失瓶颈常落到 Hikari按库能力定小池、fail-fast别按 VT 数扩连接。## 一、痛点VT 一开排队从「线程池」挪到了「连接池」Spring Boot 3.5 里把 spring.threads.virtual.enabledtrue需 Java 21一开任务执行与调度会切到基于虚拟线程的 SimpleAsyncTaskExecutor / SimpleAsyncTaskScheduler。请求侧「一请求一 VT」变得便宜Tomcat 那套平台线程池的硬顶不再是第一瓶颈。接下来的问题往往是大量 VT 同时进业务、同时借 JDBC 连接Hikari 默认 maximum-pool-size10 被打满后面的请求全堵在借连接这一步。要是再把池调成「跟 VT 一样大」压力会原样砸到数据库的会话、锁和 CPU 上。InfoQ 归纳 JDK 24 之后的虚拟线程落地情况也指向同一类模式**升级后出问题多半是下游资源先耗尽连接池、文件描述符、限流很少是「虚拟线程本身算不动」**。很多人以为「开了 VT 自动获得更高吞吐」其实只去掉了**平台线程池这一层天花板**。数据库连接、远端 HTTP、消息分区每一层仍是有界资源。哪一层先饱和哪一层就变成新的排队点。开 VT 之后最容易被忽略、也最容易被误扩的就是 Hikari。这篇只管 **Boot 3.5 开 VT 之后Hikari 怎么定容**。上下文在 VT 间怎么传前两篇讲过2026-09-21 虚拟线程与 ThreadLocal、2026-09-25 JDK 25 Scoped Values。那两篇走「上下文」线这篇走「池与下游容量」线线程局部变量就不再翻了。【配图虚拟线程开启后瓶颈移到 Hikari/DB】## 二、机制开 VT 不会自动扩 Hikari官方文档Task Execution and Scheduling里spring.threads.virtual.enabledtrue 管的是 **执行器 / 调度器以及内嵌 Web 容器的请求线程是否使用虚拟线程**文档没有把它和数据源关联。Hikari 走的是另一条数据源自动配置链**不会**因为打开虚拟线程开关就改写 maximum-pool-size、minimum-idle 或超时项。默认值仍要记住两处HikariCP 与 Boot 数据源绑定的常见默认| 项 | 默认 | 含义 ||----|------|------|| maximum-pool-size | 10 | 池内最多同时借出的连接数 || connection-timeout | 30000 ms | 借不到连接时最长等待超时抛异常 |VT 把「能同时跑起来的业务单元」抬高了**连接借还规则完全没变**。瓶颈只是从「平台线程不够」平移到「连接不够 / 数据库不够」。落地时最常见的误会就是把这两件事当成同一个「性能开关」。顺带说个边界免得误读社区 issueGitHub spring-boot **#49919** 里出现过「开 VT 后 Hikari 启动挂起」的讨论结论是 **环境 / 数据库配置问题**别写成「Boot 开虚拟线程必然卡死连接池」。排障先查数据库可达性、认证、SSL 和连接串别急着怪虚拟线程开关。可以做个对照实验同一套业务压测脚本只改 spring.threads.virtual.enabledHikari 参数不动。如果平台线程时代的瓶颈在 Tomcat 池打开 VT 后吞吐往往先上升。随后要是冒出大量 SQLTransientConnectionException或借连接等待飙升说明瓶颈已经交到了连接池 / 数据库。这组对照能直接说明「VT 开关」和「池大小」是两套旋钮别绑在一起调。## 三、定池用 Hikari wiki 公式不用 VT 计数HikariCP wiki《About Pool Sizing》给的起点是textconnections ((core_count * 2) effective_spindle_count)意思是**按数据库主机的 CPU 核数与有效磁盘轴数估一个小池**让连接尽量都在干活、池保持饱和池容量不用去对齐前端并发。SSD 场景 effective_spindle_count 往往取很小常见按 01 估4 核库大约先落在个位数到十余个连接再靠监控微调。wiki 反复强调**要小而饱和的池别按前端并发放大池**。配置示例YAML按库规格改数字即可yamlspring:threads:virtual:enabled: truedatasource:hikari:# 例4 核 SSD → 先试 10勿写成「预期 VT 数」maximum-pool-size: 10minimum-idle: 10# 默认 30000ms生产可收紧做 fail-fastconnection-timeout: 3000pool-name: app-hikari落地时抓住三点1. **先小后调**默认 10 往往已经接近很多 OLTP 库的甜点区观察活跃连接、等待时长、数据库 CPU 与锁再加减 24别一次跳到上百。2. **fail-fast**connection-timeout 设得过长只是把失败推迟成「虚拟线程干等」。宁可快点失败、触发限流或降级也不要让请求无限堆在借连接队列里。3. **多实例算总账**实例数 × maximum-pool-size 必须小于数据库 max_connections 的安全余量单实例「看起来不大」的池乘上滚动发布中的实例数就会顶满库。公式只是起点不是终值。读写比例、是否大量持锁更新、连接是否走代理中间件会再占一层会话都会让甜点区偏移。稳一点的做法压测时盯「池利用率接近饱和、数据库 CPU / 锁还没打满」这个窗口。一旦数据库侧先红再加池只会更糟该回头减入口并发或优化 SQL。反过来若池长期空闲、数据库也很闲而业务仍慢瓶颈多半不在 JDBC别用扩池当安慰剂。【配图Hikari 定容决策按库能力 vs 按 VT 数量】## 四、反模式事务跨 HTTP连接被「长租」池再小也扛不住「借了不还」。最常见的写法是Transactional 包住整段业务中间再调 RestClient / WebClient 访问下游。事务不提交连接就不归还虚拟线程再便宜也只是让更多请求同时踩中这条路径把默认十个连接全部长租出去。表面看是「Hikari 排队变长」根因是**租约时间被远端 RTT 拉长**跟池偏不偏小关系不大。反例结构示意勿照抄到生产javaServicepublic class OrderFacade {private final OrderRepository orders;private final RestClient billing;public OrderFacade(OrderRepository orders, RestClient billing) {this.orders orders;this.billing billing;}// 反模式事务边界罩住 HTTP连接被长租Transactionalpublic void place(OrderCmd cmd) {orders.insert(cmd); // 已占用连接billing.post().uri(/invoice).body(cmd).retrieve().toBodilessEntity(); // 网络等待期间连接仍被占用orders.markPlaced(cmd.id());}}改法原则- **事务尽量短**只包本地读写HTTP、消息、文件 I/O 放到事务外。- **先本地落库再异步对账**或用 Outbox避免「库连接 远端往返」叠在同一租约里。- 若必须「读库 → 调远端 → 写库」拆成两段短事务接受中间态并用幂等收敛别用一条长事务绑死连接。正例骨架javaServicepublic class OrderFacade {private final OrderRepository orders;private final RestClient billing;public OrderFacade(OrderRepository orders, RestClient billing) {this.orders orders;this.billing billing;}public void place(OrderCmd cmd) {Long id orders.insertPending(cmd); // 短事务在 Repository 内提交billing.post().uri(/invoice).body(cmd).retrieve().toBodilessEntity();orders.markPlaced(id); // 另一段短事务}}开 VT 之后这类反模式会更容易被流量放大以前平台线程池把并发硬顶在几十长租连接的危害被「线程不够」掩盖现在 VT 放开并发长租会更快把小池掏空。定容公式管「池该多大」短事务管「连接占多久」两件事得一起改。## 五、落地清单与监控上线或回归时逐条勾一遍1. **确认 VT 开关与池配置分离**spring.threads.virtual.enabled 与 spring.datasource.hikari.* 分开评审禁止「开 VT 顺手把 pool 调到 200」。2. **用公式给初值**按数据库核数与磁盘估 maximum-pool-size多实例时核对总连接上限。3. **超时策略**connection-timeout 保持秒级 fail-fast业务侧对借连接失败做限流与快速失败响应避免无脑重试。4. **代码审计**搜 Transactional 方法内是否有 HTTP 或长 RPC有则拆边界。5. **监控面板**Hikari 活跃连接、空闲连接、等待获取的线程数数据库侧会话数、锁等待、CPU。池打满但数据库 CPU 仍低优先查「长租连接」池打满且数据库 CPU / 锁已高再谈是否略增池或削减入口并发。6. **回滚预案**若线上因扩池过大导致数据库会话打满优先把 maximum-pool-size 收回公式附近并临时收紧入口限流不要在未看数据库指标前继续加池。7. **与上下文改造分工**ThreadLocal / Scoped Values 解决的是请求上下文在 VT 间的传递成本Hikari 定容解决的是下游有界资源。两者可以同一迭代推进但评审清单要分开打勾避免「改了上下文就顺手把池调爆」。最后一句**虚拟线程放大的是并发单元数量数据库能力一分没涨。** Hikari 继续按库定容、小池饱和、超时快速失败。排队就留在应用侧看得见的地方别用「按 VT 扩池」把压力偷偷甩给数据库。
