Spring Boot 4.1升级启用虚拟线程后HikariCP连接池异常崩溃问题分析

作者:袖梨 2026-06-02

在Spring Boot升级过程中,你是否经历过这样的场景:严格按照官方迁移指南操作,CI/CD流水线全部通过,部署上线后却在压力测试第一轮就遭遇数十个请求卡死?坚控显示HikariCP连接池全部耗尽,最终导致请求超时。

为什么4.1版本成为关键升级节点?

Spring Boot 4.0版本带来了最剧烈的架构变革——强制要求Java 17+、Jakarta EE 11、Spring Framework 7等新特性,甚至连Jackson组件都迁移到了新的命名空间。对于仍在使用3.x版本的用户而言,直接升级到4.0需要付出巨大代价。

4.1版本则完全不同。作为4.0基础上的增强版,它几乎没有引入破坏性变更,主要将4.0中需要手动配置的功能转为默认启用。这意味着从4.0升级到4.1的成本远低于从3.x跨越到4.0。

对于仍在使用3.x版本的用户,当前面临的关键问题是:3.5.x将是3.x系列的最后一个大版本,即将进入维护期。4.1 RC1的发布预示着稳定版即将到来。现在进行技术预研,远比被迫升级时仓促应对要明智得多。

组件 Spring Boot 3.x Spring Boot 4.0 Spring Boot 4.1
Java最低要求 Java 17 Java 17 Java 17
Spring Framework 6.x 7.0 7.0+
Jakarta EE 10(Servlet 5.0) 11(Servlet 6.1) 11(Servlet 6.1)
Jackson 2.x 3.0(组件迁移) 3.x
Hibernate 6.x 7.1 7.x
Kotlin 1.9+ 2.2+ 2.3+

img_6a1e677b3080730.webp

虚拟线程的默认启用:优势与隐患

这是4.1版本最重要的改进,也是最容易引发问题的变更点,需要特别关注。

平台线程与虚拟线程的区别

传统Java线程(平台线程)与操作系统线程保持1:1绑定关系。创建线程时,操作系统需要分配内核线程,每个线程默认栈大小为512KB~1MB。一台8GB内存的服务器,理论上最多支持数千个线程。

Tomcat默认将maxThreads设为200,就是因为超过这个数值后,线程上下文切换的开销将超过IO等待带来的收益。

虚拟线程是Java 21引入的Project Loom成果。作为JVM层面的线程,它不与操作系统线程保持1:1绑定。当虚拟线程执行阻塞操作(如IO、数据库查询、Thread.sleep)时,JVM会自动将其"卸载"到载体线程,待IO完成后再重新挂载。整个过程对代码完全透明,synchronized、Thread.currentThread()等操作仍可正常使用。

实际效果表现为:相同硬件条件下,使用虚拟线程可以轻松维持数万个并发连接,因为大多数线程都在等待IO,不占用CPU资源。

Spring Boot 4.1在Java 21+环境下会自动使用虚拟线程替换Tomcat的工作线程池(相当于配置了spring.threads.virtual.enabled=true)。这一变更无需修改任何代码。

潜在问题分析

问题主要出现在数据库连接池的设计上。

HikariCP的设计理念是:连接数越少越好。因为数据库端的并发连接本身就是昂贵资源,HikariCP的maximumPoolSize默认仅为10,官方文档甚至建议单个服务不要超过20-30个连接。

在传统架构下这种设计非常合理:当Tomcat线程数限制在200个时,最多只有200个并发请求,且这些请求不可能同时进行数据库操作,因此10-20个连接完全够用。

虚拟线程彻底改变了这一前提条件。

启用虚拟线程后,Tomcat可以同时处理数万个请求。假设5000个请求同时到达且都需要查询数据库,而HikariCP连接池仅有10个连接时,剩下的4990个请求将全部挂起等待连接,最终在30秒默认超时后抛出SQLTimeoutException。

这就是文章开头所述问题的根源:虚拟线程将IO并发能力提升了100倍,但连接池容量未能同步扩展。

解决方案有两个方向,建议同时实施:

方案一:调整HikariCP连接数上限

spring:
  datasource:
    hikari:
      # 虚拟线程环境下适当增加连接数
      # 但不要无限提高——数据库端连接资源有限
      maximum-pool-size: 50
      # 缩短连接等待超时,快速失败而非长时间阻塞
      connection-timeout: 3000
      # 设置连接最大生命周期,防止数据库侧超时
      max-lifetime: 1800000

方案二:启用LazyConnectionDataSourceProxy(4.1新功能)

Spring Boot 4.1内置了对LazyConnectionDataSourceProxy的自动集成,通过配置spring.datasource.connection-fetch属性启用。惰性连接机制确保只有在实际执行SQL语句时才从连接池获取物理连接,而非在事务开始时立即占用连接。

spring:
  datasource:
    # Spring Boot 4.1新增,启用惰性连接获取
    connection-fetch: lazy

这一改进看似微小但效果显著:对于那些开启事务但不一定会执行数据库操作的代码路径(如先读取缓存,命中后直接返回),可以大幅减少无效的连接占用。

组合策略:将连接池调整到50,同时启用惰性连接,配合虚拟线程使用,实际可用比传统架构少3/4的连接数服务更多并发请求。

img_6a1e677b3080c31.webp

更隐蔽的问题:synchronized锁钉死

虚拟线程存在一个已知限制:当虚拟线程在持有synchronized锁的情况下遇到阻塞操作时,无法被JVM卸载,会直接占用载体线程(pinning)。这意味着在此期间该载体线程完全被阻塞,无法执行其他虚拟线程。

这个问题在Java 21中仍然存在,但Java 24已经修复。如果使用Java 21,需要注意:

  1. 项目中使用的第三方库若大量使用synchronized(如某些老版本JDBC驱动),在虚拟线程环境下可能表现不佳
  2. 可通过JVM参数-Djdk.tracePinnedThreads=full检测pinning发生的位置

3.x升级到4.x:不兼容变更清单

对于计划从3.x升级到4.1的用户,必须仔细检查以下变更清单。部分变更不会在编译时报错,只会在运行时出现异常行为。

测试框架:@MockBean和@SpringBootTest行为变更

这是最容易踩的坑,因为它不会在编译时报错。

// Spring Boot 3.x:有效写法
@MockBean
private UserService userService;// Spring Boot 4.x:必须修改为
@MockitoBean
private UserService userService;

更严重的是@SpringBootTest行为的变更。在3.x版本中,@SpringBootTest会自动注入MockMvc和TestRestTemplate。而在4.x版本中,必须显式添加注解:

// 3.x写法(4.x中这样写会导致MockMvc为null,但不会报错!)
@SpringBootTest
class UserControllerTest {
    @Autowired
    MockMvc mockMvc; // 将为null,调用时抛出NPE
}// 4.x正确写法
@SpringBootTest
@AutoConfigureMockMvc // 必须添加此注解
class UserControllerTest {
    @Autowired
    MockMvc mockMvc; // 此时才有值
}

Jackson:从2.x迁移到3.x,组件ID变更

如果在pom.xml中直接依赖了Jackson模块,4.x需要修改Group ID:


<dependency>
    <groupId>com.fasterxml.jackson.coregroupId>
    <artifactId>jackson-databindartifactId>
dependency>
<dependency>
    <groupId>tools.jackson.coregroupId>
    <artifactId>jackson-databindartifactId>
dependency>

Spring Boot 4.0的starter已经引入了正确版本,但如果有直接依赖,需要手动修改。此外,Jackson的注解名称也有变化,@JsonComponent已改为@JacksonComponent。

MongoDB配置命名空间调整

# Spring Boot 3.x
spring:
  data:
    mongodb:
      uri: mongodb://localhost:27017/mydb# Spring Boot 4.x
spring:
  mongodb:
    uri: mongodb://localhost:27017/mydb

只有Spring Data MongoDB专属配置仍保留在spring.data.mongodb.*下,基础连接配置已全部迁移到spring.mongodb.*。

健康探针:默认启用

# Spring Boot 3.x:默认关闭,需手动开启
management:
  endpoint:
    health:
      probes:
        enabled: true# Spring Boot 4.x:默认已开启
# 如需关闭,需手动配置:
management:
  endpoint:
    health:
      probes:
        enabled: false

如果Kubernetes Pod没有配置livenessProbe和readinessProbe,升级后Spring Boot会自动暴露/actuator/health/liveness和/actuator/health/readiness端点。这通常是好事,但如果基础设施有严格的Actuator访问控制,需要提前确认权限策略。

server.error.*属性迁移

# Spring Boot 3.x
server:
  error:
    include-message: always
    include-stacktrace: never# Spring Boot 4.x
spring:
  web:
    error:
      include-message: always
      include-stacktrace: never

官方提供了spring-boot-properties-migrator工具,添加到pom.xml后可在运行时自动识别废弃配置并给出迁移提示:

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-properties-migratorartifactId>
    <scope>runtimescope>
dependency>

迁移完成后请记得移除该依赖,不要带到生产环境。

img_6a1e677b3080f32.webp

AOT编译与GraalVM原生镜像:当前进展

这部分功能对多数业务团队而言尚不紧迫,但有几点进展值得关注。

Spring Boot 4.x系列持续投入AOT(提前编译)支持。AOT的核心价值在于:在编译阶段将Spring的Bean注册、条件判断、依赖注入等运行时逻辑预先生成为静态代码,这样使用GraalVM编译成原生镜像后,启动时间可从秒级降至毫秒级,内存占用减少50-80%。

4.1对GraalVM的要求是native-image v25+(Spring Boot 4.0就已开始要求此版本)。

适合使用原生镜像的场景:

  1. Serverless/FaaS函数(冷启动时间是关键指标)
  2. 边缘计算节点(内存限制严格)
  3. CLI工具(需要快速启动)

不适合的场景:

  1. 长期运行的业务服务(JIT预热后的性能通常优于AOT)
  2. 频繁变更的服务(每次代码修改都需要重新编译原生镜像,耗时数十分钟)
  3. 包含大量反射操作的遗留代码(AOT需要手动声明反射元数据,改造成本高)

坦白说,对于大多数维护中等规模Spring Boot服务的团队,现阶段采用原生镜像的收益不够明显,维护成本过高。虚拟线程才是4.x中最值得立即采用的改进。

迁移路径:最稳妥的升级策略

直接从3.3/3.4跳到4.1 RC1对生产服务风险太大。建议按以下顺序执行:

第一步:先升级到3.5.x

3.5.x是3.x系列的最后一个大版本,它会以"废弃"形式提前暴露4.0中的许多不兼容变更。此步骤的目标是:让编译器帮助找出所有会在4.x中出现问题的代码。


<parent>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-parentartifactId>
    <version>3.5.0version>
parent>

升级到3.5后,开启所有废弃警告,清除所有废弃API调用(如@MockBean→@MockitoBean等),确保零警告。

第二步:升级到4.0.x(当前稳定版4.0.6)

4.0是最大的跨越,主要工作包括:

  1. Jackson Group ID迁移
  2. 配置命名空间调整(使用properties-migrator工具辅助)
  3. 测试代码中的@SpringBootTest检查
  4. 移除Undertow(如在使用)

此步骤建议仅在非生产环境执行,并进行全量测试。

第三步:升级到4.1.x(当前为RC1,GA版即将发布)

从4.0升级到4.1几乎没有破坏性变更,主要是功能增强。升级后重点检查:

  1. 连接池配置(需要重新评估maximum-pool-size)
  2. 启用spring.datasource.connection-fetch: lazy
  3. 在Java 21+环境下确认虚拟线程已自动启用(可通过日志确认)

回滚策略

若升级后出现问题,最快的回滚方式是回退版本号,不要修改业务代码。因此升级时需要确保:

  1. pom.xml中的Spring Boot版本是唯一控制入口(避免分散配置如spring-framework.version等覆盖)
  2. 升级前运行完整集成测试,记录基线响应时间和连接池指标
  3. 上线后坚控HikariCP连接等待时间(hikaricp.connections.pending)和连接超时次数

专业建议

Spring Boot 4.1值得升级,但需要明确执行顺序。

当前使用3.x版本的用户,最紧迫的任务是:清除代码中所有对废弃API的调用,无论是否计划升级到4.x,这都能有效降低技术债务。

已在使用4.0版本的用户,升级到4.1几乎能获得免费收益——连接惰性获取、Redis注解器、SSRF防护等功能无需手动配置即可获得。唯一需要做的是检查连接池配置。

虚拟线程的实际收益取决于服务类型。对于CPU密集型计算(如图像处理、加解密、复杂规则引擎),虚拟线程帮助有限;而对于大量时间花费在等待数据库返回、下游HTTP接口或Redis响应的IO密集型服务,虚拟线程能在不增加硬件的情况下显著提升吞吐量。

归根结底,在没有时间压力的情况下,框架升级的最佳策略是:先在非核心服务上试点,运行一个月,解决所有问题后再推广到核心链路。强制"全量同时升级"往往是生产事故的温床。

Spring Boot 4.x系列在AOT编译方面的实现机制值得深入探讨——它究竟在编译阶段生成了什么代码,为何能让原生镜像启动如此迅速。这将是后续技术分析的重点方向。

对于正在评估4.x升级计划的团队,本文可直接作为工程师的前置阅读材料,帮助他们避免虚拟线程与连接池的典型问题。

常见问题解答

Q:Java 17能使用虚拟线程吗?必须Java 21才行?

A:必须使用Java 21+。虚拟线程在Java 19/20中是预览特性,Java 21才成为正式稳定版本。Spring Boot 4.x的虚拟线程自动配置仅在Java 21+环境下生效,Java 17/21只是最低要求,不代表Java 17能使用虚拟线程。

Q:HikariCP的maximumPoolSize设置多少合适?

A:没有万能答案,取决于数据库能承受的并发连接数。通用建议:先用公式(核心数×2)+磁盘并发数作为起点(HikariCP官方建议),对于支持虚拟线程的场景可适当提高到30-50,但绝对不要盲目提高到200+。数据库端连接是有成本的,连接数越多,数据库内存开销越大,反而会降低整体性能。

Q:升级后,原来用spring.mvc.async.request-timeout控制异步超时的配置还有效吗?

A:4.x中这个属性未被移除,但行为有细微变化。虚拟线程下,请求处理变为同步阻塞模式(尽管底层是异步IO),因此request-timeout主要影响同步请求的超时控制。如果之前依赖WebFlux的响应式超时,建议使用Mono.timeout()在代码层面显式控制。

Q:生产环境能直接使用RC1吗?

A:不建议。RC1表示候选发布版,功能已冻结但仍在修复bug,不会引入破坏性变更。现在进行技术预研、搭建测试环境是合理的,但生产环境应等待GA版本,通常RC1发布后2-4周会推出GA。

Q:能否在4.x中关闭虚拟线程,先进行"静默升级"?

A:可以。在application.yml中明确配置:

spring:
  threads:
    virtual:
      enabled: false

这样可先完成版本升级,将虚拟线程的影响评估和连接池调优安排到下一个迭代。这是风险最小的升级策略。

img_6a1e677b3081133.webp

参考资料

  1. Spring Boot 4.1.0-RC1发布说明
  2. Spring Boot 4.0迁移指南
  3. Spring Boot 4.0配置变更日志
  4. HikariCP关于连接池大小的说明
  5. JEP 444:虚拟线程(Java 21官方提案)

相关文章

精彩推荐