You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Data JDBC PostgreSQL事务中连接泄漏问题排查求助

Spring Data JDBC PostgreSQL连接延迟释放问题排查

问题现象

  • 非事务方法的数据库连接能正常获取并归还连接池,但标注@Transactional的事务方法调用时,连接无法及时释放,触发HikariCP连接泄漏告警。
  • 矛盾点:告警触发时,连接池统计显示所有连接均为空闲状态(total=16, active=0, idle=16)。
  • 业务逻辑仅需毫秒级完成,但连接要在30秒后才归还至连接池,延迟时间与HikariCP的leakDetectionThreshold(30秒)接近。

配置类代码

@Configuration
@EnableJdbcRepositories("com.api.repositories")
public class DBConfig {
    
    @Autowired
    private DbProps dbProps;
    
    @Bean
    public DataSource readWriteDataSource() {
        LOGGER.info("DB Props: {}",dbProps.toString());
        PGSimpleDataSource dataSource = new PGSimpleDataSource();
        dataSource.setUrl(dbProps.getDbUrl());
        dataSource.setUser(dbProps.getDbUser());
        dataSource.setPassword(dbProps.getDbPassword());
        dataSource.setLogUnclosedConnections(true);
        return connectionPoolDataSource(dataSource);
    }

    protected HikariConfig hikariConfig(DataSource dataSource) {
        HikariConfig hikariConfig = new HikariConfig();
        int cpuCores = Runtime.getRuntime().availableProcessors();
        hikariConfig.setMaximumPoolSize(cpuCores*4);
        hikariConfig.setDataSource(dataSource);
        hikariConfig.setIdleTimeout(600000);
        hikariConfig.setLeakDetectionThreshold(30000);
        hikariConfig.setConnectionTimeout(30000);
        hikariConfig.setMaxLifetime(1800000);
        hikariConfig.setAutoCommit(false);
        return hikariConfig;
    }
 
    protected HikariDataSource connectionPoolDataSource(
            DataSource dataSource) {
        return new HikariDataSource(hikariConfig(dataSource));
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource readWriteDataSource) {
        return new DataSourceTransactionManager(readWriteDataSource);
    }
    
    @Bean
    public NamedParameterJdbcOperations usersJdbcOperations(DataSource readWriteDataSource) {
      return new NamedParameterJdbcTemplate(readWriteDataSource);
    }
}

事务方法代码

@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class)
public String placeBet(PlaceBet placebet) {
    try {
        Bet unsavedBet = Bet.builder().amount(BigDecimal.valueOf(placebet.amount()))
                .coin(placebet.coin())
                .game(placebet.game())
                .gid(placebet.gid())
                .uid(placebet.uid())
                .name(placebet.name())
                .created(LocalDateTime.now().truncatedTo(ChronoUnit.MILLIS))
                .originalOdd(BigDecimal.valueOf(placebet.original_odd())).build();
        Bet savedBet = betRepository.save(unsavedBet);
        if (null != savedBet) {
            LOGGER.info("saved Bet: {}",savedBet.toString());
        }
        return "success";
    } catch (Exception e) {
        LOGGER.error("Error {} encountered saving bet: {} changes rolled Back",e.getMessage(),placebet.toString());
        return "bet error";
    }
}

连接泄漏告警日志

2023-01-20 00:11:15 WARN  ProxyLeakTask.java:84 - 检测到连接泄漏:线程http-nio-8001-exec-11持有的Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@6fae4b49,堆栈跟踪如下
java.lang.Exception: 检测到明显的连接泄漏
    at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100)
    at org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:265)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:400)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:373)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:595)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:382)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)
    at com.api.services.BetService$$EnhancerBySpringCGLIB$$23929008.placeBet(<generated>)
    at com.api.controllers.BetController.placeNewBet(BetController.java:132)
    at jdk.internal.reflect.GeneratedMethodAccessor89.invoke(Unknown Source)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)

连接池统计日志

2023-01-20 00:11:58 DEBUG HikariPool.java:421 - HikariPool-1 - 连接池统计:总数=16,活跃=0,空闲=16,等待=0
2023-01-20 00:11:58 DEBUG HikariPool.java:518 - HikariPool-1 - 跳过填充连接池,当前连接数已足够

连接延迟释放日志

2023-01-20 00:11:15 WARN  ProxyLeakTask.java:84 - 检测到连接泄漏:线程http-nio-8001-exec-11持有的Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@6fae4b49,堆栈跟踪如下
2023-01-20 00:11:56 DEBUG DataSourceTransactionManager.java:330 - 提交JDBC事务,连接为[HikariProxyConnection@304420024 wrapping Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@6fae4b49]
2023-01-20 00:11:56 DEBUG DataSourceUtils.java:242 - 将JDBC连接[HikariProxyConnection@304420024 wrapping Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@6fae4b49]的隔离级别重置为2
2023-01-20 00:11:56 DEBUG DataSourceTransactionManager.java:389 - 事务结束后释放JDBC连接[HikariProxyConnection@304420024 wrapping Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@6fae4b49]
2023-01-20 00:11:56 INFO  ProxyLeakTask.java:91 - 之前报告的泄漏连接(线程http-nio-8001-exec-11持有的Pooled connection wrapping physical connection org.postgresql.jdbc.PgConnection@6fae4b49)已归还至连接池(无泄漏)

排查方案

1. 验证事务代理是否生效

事务方法延迟释放连接的常见原因是事务切面未正确生效,导致事务上下文未被管理:

  • 检查placeBet方法是否被同类内部方法调用(比如BetService中另一个非事务方法调用placeBet),这种情况Spring CGLIB代理无法拦截,事务不会被正确提交/回滚,连接会绑定在线程上下文直到线程被回收。
  • 确认控制器BetController注入的是Spring容器管理的BetService代理对象,而非手动new的实例。

2. 排查线程阻塞问题

从日志看,事务提交比业务逻辑完成晚了41秒,说明线程在事务方法执行完毕后被阻塞:

  • 在告警触发(00:11:15)到连接释放(00:11:56)之间,用jstack <PID>抓取线程栈,查看http-nio-8001-exec-11线程的状态:
    • 是否卡在锁等待、文件IO、远程调用等耗时操作?
    • 是否存在自定义AOP切面在事务切面之后执行,且切面逻辑中有阻塞行为?

3. 检查HikariCP自动提交配置

配置中设置了hikariConfig.setAutoCommit(false);,需确认:

  • Spring事务管理是否正确接管了事务提交:从日志看最终执行了commit,但延迟严重,检查是否有代码在事务方法外手动操作连接的提交状态。
  • 临时将autoCommit改为true测试(Spring事务方法内会自动将连接设为autoCommit=false,事务结束后恢复),观察连接释放是否恢复正常。

4. 验证Spring事务同步机制

  • 检查是否存在自定义的TransactionSynchronization实现,在事务完成后执行了耗时操作,导致连接无法及时释放。
  • 确认DataSourceTransactionManager是否有自定义扩展,比如重写了doCommit或doCleanupAfterCompletion方法,引入了延迟逻辑。

5. 数据库端连接状态排查

用PostgreSQL命令查询连接状态:

SELECT pid, state, query, backend_start FROM pg_stat_activity WHERE pid = '<连接对应的PID>';

确认在告警期间,该连接是处于active还是idle状态,判断是应用端未释放连接,还是数据库端导致的阻塞。

6. 解析连接池统计矛盾点

连接池统计显示active=0但实际连接未释放,是因为HikariCP的active统计仅跟踪直接调用getConnection()且未close()的连接,而Spring事务通过DataSourceUtils将连接绑定到线程上下文,HikariCP无法感知这种绑定。可以通过以下方式验证:

  • 启用Spring的DataSourceUtils日志(设置org.springframework.jdbc.datasource.DataSourceUtils为DEBUG级别),查看连接绑定和释放的详细流程。

内容的提问来源于stack exchange,提问作者martin kimani

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 20:31:02