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

Hibernate与Tomcat JDBC连接池:Connection已关闭异常排查与解决请求

问题背景

运行在Tomcat 9上的Web应用,使用Hibernate、PostgreSQL及Tomcat JDBC连接池,应用处于autocommit=false、CURRENT_SESSION_CONTEXT_CLASS=thread模式。

Tomcat数据源配置

<Resource name="xxx" auth="Container"
          type="javax.sql.DataSource" driverClassName="org.postgresql.Driver" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory"
          url="jdbc:postgresql://localhost:5432/db"
          username="xxx" password="xxx" maxActive="200" maxIdle="50" minIdle="30"
          defaultAutoCommit="false" jmxEnabled="false"
          jdbcInterceptors="org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport"
            removeAbandoned="true"
            removeAbandonedTimeout="30"
            logAbandoned="true"
            suspectTimeout="20"
            validationQuery="SELECT 1"
            testWhileIdle="true"
            timeBetweenEvictionRunsMillis="5000"
            minEvictableIdleTimeMillis="60000"
            testOnBorrow="true"
            validationInterval="10000"
            closeMethod="close"
/>

Hibernate SessionFactory配置

ServiceRegistry serviceRegistry = new StandardServiceRegistryBuilder().applySettings(...)
.applySetting( AvailableSettings.TRANSACTION_COORDINATOR_STRATEGY, "jdbc" )
.applySetting( AvailableSettings.CURRENT_SESSION_CONTEXT_CLASS, "thread" )
.applySetting( AvailableSettings.USE_NEW_ID_GENERATOR_MAPPINGS, false )
.applySetting( AvailableSettings.DIALECT, "org.hibernate.dialect.PostgreSQL10Dialect" )
.applySetting( AvailableSettings.DEFAULT_SCHEMA, "public" )
.applySetting( AvailableSettings.DRIVER, "org.postgresql.Driver" )
.applySetting( "hibernate.temp.use_jdbc_metadata_defaults", false )
.build();
Metadata metadata = new MetadataSources(serviceRegistry)
            .addAnnotatedClass(…).getMetadataBuilder().build();
sessionFactory = metadata.getSessionFactoryBuilder().build();

数据库操作代码模式

Session s = sessionFactory.getCurrentSession();
s.beginTransaction();
try {
  …
  …
  s.getTransaction().commit();
} catch (Exception x) {
  x.printStackTrace();
  s.getTransaction().rollback();
}

当try-catch中操作超时或因代码bug导致事务泄漏(未执行commit()/rollback())时,Tomcat会将连接标记为“abandoned”并从池中移除,此时Hibernate会大量抛出以下异常:

org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() for servlet [dispatcher] in context with path
 [] threw exception [Request processing failed; nested exception is org.hibernate.TransactionException: JDBC begin transaction failed: ] with root cause
        java.sql.SQLException: Connection has already been closed.
                at org.apache.tomcat.jdbc.pool.ProxyConnection.invoke(ProxyConnection.java:121)
                at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:109)
                at org.apache.tomcat.jdbc.pool.interceptor.AbstractCreateStatementInterceptor.invoke(AbstractCreateStatementInterceptor.java:79)
                at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:109)
                at org.apache.tomcat.jdbc.pool.TrapException.invoke(TrapException.java:39)
                at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:109)
                at org.apache.tomcat.jdbc.pool.interceptor.AbstractCreateStatementInterceptor.invoke(AbstractCreateStatementInterceptor.java:79)
                at org.apache.tomcat.jdbc.pool.JdbcInterceptor.invoke(JdbcInterceptor.java:109)
                at org.apache.tomcat.jdbc.pool.DisposableConnectionFacade.invoke(DisposableConnectionFacade.java:81)
                at jdk.proxy2/jdk.proxy2.$Proxy92.setAutoCommit(Unknown Source)
                at org.hibernate.resource.jdbc.internal.AbstractLogicalConnectionImplementor.begin(AbstractLogicalConnectionImplementor.java:72)
                at org.hibernate.resource.jdbc.internal.LogicalConnectionManagedImpl.begin(LogicalConnectionManagedImpl.java:285)
                at org.hibernate.resource.transaction.backend.jdbc.internal.JdbcResourceLocalTransactionCoordinatorImpl$TransactionDriverControlImpl.begin(JdbcResourceLocalTransactionCoordinatorImpl.java:246)
                at org.hibernate.engine.transaction.internal.TransactionImpl.begin(TransactionImpl.java:83)
                at org.hibernate.internal.AbstractSharedSessionContract.beginTransaction(AbstractSharedSessionContract.java:503)
                at jdk.internal.reflect.GeneratedMethodAccessor191.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)
                at org.hibernate.context.internal.ThreadLocalSessionContext$TransactionProtectionWrapper.invoke(ThreadLocalSessionContext.java:351)
                at jdk.proxy3/jdk.proxy3.$Proxy91.beginTransaction(Unknown Source)

异常抛出点在Session.beginTransaction()。


问题1:如何强制Hibernate在此场景下从Tomcat JDBC连接池获取新的可用连接,并输出告警日志?

解决方案:

  1. 自定义Hibernate连接检查逻辑
    在调用beginTransaction()前,主动检查当前Session关联的连接有效性,若连接已关闭则销毁旧Session并重新获取,同时输出告警日志:

    Session s = sessionFactory.getCurrentSession();
    try {
        // 验证连接有效性
        if (!s.getJdbcConnectionAccess().obtainConnection().isValid(0)) {
            s.close();
            s = sessionFactory.getCurrentSession();
            LOGGER.warn("检测到废弃连接,已重新初始化Session和连接");
        }
        s.beginTransaction();
        // 业务逻辑执行
        s.getTransaction().commit();
    } catch (SQLException | HibernateException e) {
        if (s.getTransaction().isActive()) {
            s.getTransaction().rollback();
        }
        LOGGER.error("事务执行失败", e);
    }
    

    注:isValid(0)中的0表示使用驱动默认超时时间,可根据实际调整。

  2. 扩展Tomcat连接池拦截器
    继承org.apache.tomcat.jdbc.pool.JdbcInterceptor实现自定义拦截器,在invoke()方法中捕获Connection has already been closed异常,自动重新获取连接并记录告警:

    public class ReconnectInterceptor extends JdbcInterceptor {
        private static final Logger LOGGER = LoggerFactory.getLogger(ReconnectInterceptor.class);
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                return super.invoke(proxy, method, args);
            } catch (SQLException e) {
                if ("Connection has already been closed.".equals(e.getMessage())) {
                    ConnectionPool pool = getPool();
                    Connection newConn = pool.borrowConnection();
                    LOGGER.warn("废弃连接已替换为新连接");
                    return method.invoke(newConn, args);
                }
                throw e;
            }
        }
    }
    

    然后在Tomcat数据源配置中添加该拦截器:

    jdbcInterceptors="org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport;com.yourpackage.ReconnectInterceptor"
    
  3. 启用Hibernate连接验证配置
    在Hibernate配置中添加:

    hibernate.connection.test_on_borrow=true
    hibernate.connection.validation_query=SELECT 1
    

    让Hibernate在获取连接时自动验证有效性,失效则从连接池重新获取,配合Tomcat连接池的testOnBorrow=true配置,形成双重验证。


问题2:采用哪些方法可以定位代码中未提交事务的bug?

定位方法:

  1. 利用Tomcat连接池的废弃连接日志
    当前配置已开启logAbandoned="true",Tomcat会在标记连接为废弃时,打印该连接的完整调用栈。直接在日志中搜索AbandonedConnection关键词,即可定位到未正确提交/回滚事务的代码路径。

  2. 添加事务监控拦截器
    自定义拦截器(如Spring MethodInterceptor),在事务结束后检查状态,若发现未提交/回滚的事务则打印告警和调用栈:

    public Object invoke(MethodInvocation invocation) throws Throwable {
        Session s = sessionFactory.getCurrentSession();
        Transaction tx = null;
        boolean txStarted = false;
        try {
            tx = s.beginTransaction();
            txStarted = true;
            Object result = invocation.proceed();
            tx.commit();
            return result;
        } catch (Exception e) {
            if (txStarted && tx.isActive()) {
                tx.rollback();
            }
            throw e;
        } finally {
            if (txStarted && tx.isActive()) {
                LOGGER.error("检测到未提交事务,调用栈:", new Exception());
            }
        }
    }
    
  3. 开启Hibernate统计功能
    配置hibernate.generate_statistics=true,通过JMX或日志查看事务统计数据,对比事务启动数与提交/回滚数,找到异常差值对应的业务模块,再结合日志定位问题代码。

  4. 静态代码分析
    使用SonarQube等工具扫描代码,识别事务泄漏风险点:比如try块中启动事务,但catch块未处理回滚、分支路径跳过commit()/rollback()等情况。

  5. 运行时动态追踪
    使用Arthas等Java诊断工具,在生产环境中动态追踪Session.beginTransaction()、commit()、rollback()方法的调用,统计每个方法的调用次数,定位只调用beginTransaction()但无后续提交/回滚的线程和代码路径。


内容的提问来源于stack exchange,提问作者Jakub Królikowski

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 18:34:59