为什么给Tomcat开虚拟线程后,数据库连接反而更快被耗尽

发布时间:2026/11/5 1:53:50
为什么给Tomcat开虚拟线程后,数据库连接反而更快被耗尽 本文摘要虚拟线程拆掉了 Tomcat 的线程上限数据库连接池会在数秒内被打满。平台线程池原本是隐式限流可用信号量等显式背压替代它。方案适用于下游连接数固定的服务数据库已是瓶颈时应回到查询与索引优化。一、问题与结论Spring Boot 3.2.5 JDK 21在application.yml加一行spring.threads.virtual.enabled: true后压测吞吐没有明显提升P99 反而变差日志开始刷HikariPool-1 - Connection is not available, request timed out after 30000ms同一份代码、同一份连接池配置把开关改回false后连接池偶尔繁忙但从未在几十秒内被彻底打满。结论先行这个开关只替换请求处理的线程模型不改变任何下游资源池的容量。HikariCP 的maximum-pool-size仍是 10数据库能承受的并发连接数也没变。虚拟线程移除了最多 200 个请求同时执行这道隐式限流连接池因此以更快的速度被填满并压满——瓶颈从线程数平移到了连接数。三个可观测的次生症状吞吐不涨请求卡在getConnection()上排队CPU 花在调度而非执行上。P99 变差connection-timeout之内的排队时间全部计入请求耗时。日志串号常见于把请求上下文放在可被载体线程复用的ThreadLocal自定义线程包装、异步 appender 在别的线程读 MDC。这与 pinning 无关具体表现依日志框架版本而定需在你的框架上单独验证。二、排查与选择依据先复现再归因。用wrk对同一接口压两轮第二轮切到带信号量的路径wrk-t4-c200-d30s--latencyhttp://localhost:8080/query wrk-t4-c200-d30s--latencyhttp://localhost:8080/query-limited同时每秒抓一次连接池状态第四节的/pool-status虚拟线程模式下active立刻等于maximum-pool-size、waiting持续增长平台线程模式下waiting上限被maxThreads默认 200压住。若 P99 依旧异常再排查 pinning。这是虚拟线程场景下另一类独立问题JDK 21 可用启动参数直接抓java-Djdk.tracePinnedThreadsfull-jardemo.jar示意输出行内容取决于真实堆栈本文未在你的环境验证Thread[#42,ForkJoinPool-1-worker-1] is pinned at: com.demo.DemoController.sleep(DemoController.java:41) com.demo.DemoController.query(DemoController.java:35) java.base21/java.lang.VirtualThread.run(VirtualThread.java:592)三类常见成因逐条说清synchronizedJDK 21 中虚拟线程进入synchronized块后无法从载体线程卸载整个块执行期间占死一个载体线程。JDK 24 起不再由synchronized触发 pinning但跑在 JDK 21 上仍需自己规避。JNI 调用native 方法内部如果做了阻塞 I/O老版本数据库驱动的 socket 读同样无法卸载载体线程被长期占用。ThreadLocal这是最容易被误判的一项——读写ThreadLocal本身不会导致 pinning每个虚拟线程都有独立的ThreadLocal表。它的真实代价是内存每个虚拟线程一份与上下文传递异步线程上取不到排查时不要把它当卡顿根因。替代方案与取舍做法选择条件代价与边界Semaphore显式背空本文方案下游连接池容量固定想保留虚拟线程的低成本等待需保证异常路径也release()每个访问下游的入口都要包回退平台线程并调大maxThreads瓶颈本就不在应用线程I/O 等待不多线程栈内存开销大上限受 OS 约束R2DBC 非阻塞数据访问并发极高且能接受整层重写事务模型不同迁移与学习成本高不该用本文方案的两种情况一是数据库本身已是瓶颈慢查询、锁竞争加任何应用层控制都是掩盖问题应先做索引与查询优化二是请求几乎不碰数据库纯外部 HTTP 调用虚拟线程的收益本就集中在这里不必额外引入信号量。三、关键原理平台线程模式下Tomcat 的 200 个线程是一道天然减速带最多 200 个请求在执行最多 200 个请求持有连接其余在 accept 队列排队。虚拟线程让等待几乎免费——初始栈仅数百字节挂起与恢复由 JVM 调度器接管不占操作系统线程。于是更多请求毫无代价地涌进getConnection()的等待队列队列膨胀速度远高于平台线程模式同时数千虚拟线程并发竞争 HikariCP 的ConcurrentBagCAS 与内部锁的争用在 200 并发下可忽略在数千并发下变成可观测损耗。一句话背压不会凭空消失只会在你没设防的位置堆起来。四、可运行示例最小工程JDK 21、Spring Boot 3.2.5、内嵌 H2。在spring-boot-starter-parent3.2.5 的基础上增加两个依赖dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-jdbc/artifactId/dependencydependencygroupIdcom.h2database/groupIdartifactIdh2/artifactIdscoperuntime/scope/dependency配置文件application.ymlspring:threads:virtual:enabled:truedatasource:url:jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1driver-class-name:org.h2.Driverusername:sapassword:hikari:maximum-pool-size:5connection-timeout:10000pool-name:demo-pool控制器importcom.zaxxer.hikari.HikariDataSource;importorg.springframework.jdbc.core.JdbcTemplate;importorg.springframework.transaction.support.TransactionTemplate;importorg.springframework.web.bind.annotation.*;importjava.util.Map;importjava.util.concurrent.Semaphore;RestControllerpublicclassDemoController{privatefinalJdbcTemplatejdbc;privatefinalTransactionTemplatetx;privatefinalHikariDataSourceds;privatefinalSemaphorelimit;// permits 取连接池上限避免无效排队publicDemoController(JdbcTemplatejdbc,TransactionTemplatetx,HikariDataSourceds){this.jdbcjdbc;this.txtx;this.dsds;this.limitnewSemaphore(ds.getMaximumPoolSize());}GetMapping(/query)publicStringquery(){returntx.execute(s-{jdbc.queryForList(SELECT 1);try{Thread.sleep(3000);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}returndone;});}GetMapping(/query-limited)publicStringqueryLimited()throwsInterruptedException{limit.acquire();try{returnquery();}finally{limit.release();}}GetMapping(/pool-status)publicMapString,ObjectpoolStatus(){varpds.getHikariPoolMXBean();returnMap.of(active,p.getActiveConnections(),idle,p.getIdleConnections(),total,p.getTotalConnections(),waiting,p.getThreadsAwaitingConnection());}}启动并压测mvn spring-boot:run再执行第二节的两条wrk命令。/query在事务里持有连接 3 秒用来放大排队效应。一个常见失败把limit.acquire()写进try块内部异常路径漏掉release()信号量几轮请求后泄漏并永久卡死。修复方式是固定先acquire()、try/finally中release()的顺序如queryLimited()所示。五、验证结果与边界预期输出/query下active恒为 5、waiting稳定在并发数减 5 的水平wrk 的 P99 逼近connection-timeout切到/query-limited后waiting不再增长P99 回落到约 3 秒超时报错消失。实际输出本文示例在 H2 内存库与本地wrk下验证过连接池秒级被打满、信号量路径消除等待堆积这一现象tracePinnedThreads的具体堆栈与生产驱动相关上文输出仅为格式示意未在你的环境验证。启用 pinning 排查前先确认自己确实在 JDK 21 上运行。参考资料JEP 444: Virtual ThreadsJDK 21Spring Boot Reference Documentation: Virtual ThreadsApache Tomcat 10.1 DocumentationHikariCP Wiki

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询