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

多租户架构下调用@Transactional方法后C3P0连接池耗尽问题排查

C3P0连接池耗尽导致应用冻结的排查与解决方案

从你提供的代码和配置来看,连接池耗尽的核心原因是SessionFactory创建逻辑错误、连接未正确回收以及多租户配置与连接池管理的冲突,下面具体分析并给出修复方案:

一、问题根源分析

1. SessionFactory重复创建,导致多连接池实例

你的HibernateConf.sessionFactory()方法存在严重问题:

  • 手动调用configuration.buildSessionFactory(serviceRegistry)创建SessionFactory,并且覆盖了Spring管理的LocalSessionFactoryBean实例。
  • 每次调用sessionFactory()(比如事务管理器初始化时)都会创建一个新的SessionFactory,每个SessionFactory都会初始化自己的C3P0连接池。
  • 硬编码设置TenantContext.setCurrentTenant("tenant_1"),违背了多租户动态切换的逻辑,导致每个SessionFactory都绑定到固定租户,进一步加剧连接池资源浪费。

2. Session未正确回收,连接泄漏

Service方法中使用getSession()获取Session,但getSession()返回的是新Session,不会自动绑定到当前事务。如果没有手动关闭Session,连接会一直被占用,最终耗尽连接池。

3. C3P0配置冲突

你同时在Hibernate属性和数据源配置中设置C3P0参数,导致连接池的实际行为不符合预期:

  • Hibernate的hibernate.c3p0.*参数会尝试创建自己的连接池,而你又配置了ComboPooledDataSource作为数据源,双重连接池管理导致连接数不受控。

4. 多租户连接管理错误

自定义的DataSourceBasedMultiTenantConnectionProviderImpl可能没有正确实现连接的释放逻辑,切换租户时连接没有回到对应租户的连接池,导致连接泄漏。

二、分步解决方案

1. 修复SessionFactory的创建逻辑

Spring的LocalSessionFactoryBean会自动管理单例SessionFactory,无需手动创建。修改sessionFactory()方法:

@Bean
public LocalSessionFactoryBean sessionFactory() {
    LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
    // 移除手动指定数据源和硬编码租户的逻辑,多租户连接提供者会自动处理
    sessionFactory.setPackagesToScan("it.spi");
    sessionFactory.setConfigLocation(new ClassPathResource("hibernate_ehr/hibernate.cfg.xml"));
    sessionFactory.setHibernateProperties(hibernateProperties());
    return sessionFactory;
}

同时修改事务管理器,依赖注入Spring管理的SessionFactory,避免重复创建:

@Bean
public HibernateTransactionManager transactionManager(SessionFactory sessionFactory) {
    HibernateTransactionManager transactionManager = new HibernateTransactionManager();
    transactionManager.setSessionFactory(sessionFactory);
    return transactionManager;
}

2. 确保Session自动回收

在@Transactional方法中使用getCurrentSession()替代getSession(),getCurrentSession()会绑定到当前事务,事务结束后自动关闭Session,释放连接:

@Override
@Transactional
public String findPatientCountDao(Paziente paziente, boolean searchForAllPatients, boolean enabledDemetra) {
    String querySqlOrHql = this.createQuery(paziente, searchForAllPatients, true, 0, 0);
    // 使用getCurrentSession()获取绑定到事务的Session
    Query q = getSessionFactory().getCurrentSession().createQuery(querySqlOrHql);
    List<Object[]> obj = q.list();
    // 增加空指针判断,避免崩溃
    return obj.isEmpty() ? "0" : String.valueOf(obj.get(0)[0]);
}

3. 统一C3P0配置,避免冲突

移除Hibernate属性中的C3P0配置,直接在ComboPooledDataSource中设置连接池参数,确保连接池由数据源统一管理:

@Primary
@Bean(name = { "dataSource", "dataSource1" })
@ConfigurationProperties(prefix = "spring.multitenancy.datasource1")
public DataSource dataSource1() {
    ComboPooledDataSource dataSource = (ComboPooledDataSource) DataSourceBuilder
            .create(this.multitenancyProperties.getDatasource1().getClassLoader())
            .driverClassName(this.multitenancyProperties.getDatasource1().getDriverClassName())
            .username(this.multitenancyProperties.getDatasource1().getUsername())
            .password(this.multitenancyProperties.getDatasource1().getPassword())
            .url(this.multitenancyProperties.getDatasource1().getUrl())
            .type(ComboPooledDataSource.class)
            .build();
    
    // 直接配置C3P0参数
    try {
        dataSource.setMinPoolSize(50);
        dataSource.setMaxPoolSize(100);
        dataSource.setAcquireIncrement(5); // 减小每次获取的连接数,避免瞬间占满
        dataSource.setMaxIdleTime(300); // 缩短空闲连接回收时间,释放资源
        dataSource.setMaxStatements(600);
        dataSource.setTestWhileIdle(true);
        dataSource.setPreferredTestQuery("SELECT 1");
    } catch (SQLException e) {
        throw new RuntimeException("Failed to configure C3P0 data source", e);
    }
    return dataSource;
}

修改hibernateProperties(),移除C3P0相关配置:

private final Properties hibernateProperties() {
    Properties hibernateProperties = new Properties();
    hibernateProperties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
    hibernateProperties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, dataSources);
    hibernateProperties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantSchemaResolver);
    hibernateProperties.put(Environment.USE_STREAMS_FOR_BINARY, true);
    hibernateProperties.put(Environment.DIALECT, "org.hibernate.dialect.Oracle10gDialect");
    hibernateProperties.put(Environment.SHOW_SQL, false);
    hibernateProperties.put(Environment.FORMAT_SQL, false);
    hibernateProperties.put(Environment.USE_SQL_COMMENTS, false);
    // 移除所有C3P0相关配置
    return hibernateProperties;
}

4. 验证多租户连接提供者的实现

确保DataSourceBasedMultiTenantConnectionProviderImpl正确实现releaseConnection方法,将连接放回对应租户的连接池:

@Override
public void releaseConnection(Connection connection, String tenantIdentifier) throws SQLException {
    // 获取对应租户的数据源,释放连接
    DataSource dataSource = getDataSourceForTenant(tenantIdentifier);
    if (dataSource instanceof ComboPooledDataSource) {
        // C3P0会自动回收连接,这里直接关闭连接即可
        connection.close();
    } else {
        // 其他数据源的处理逻辑
        connection.close();
    }
}

5. 添加连接池监控(可选)

添加日志打印连接池状态,帮助排查连接泄漏:

@Autowired
private DataSource dataSource;

@Override
@Transactional
public String findPatientCountDao(Paziente paziente, boolean searchForAllPatients, boolean enabledDemetra) {
    if (dataSource instanceof ComboPooledDataSource) {
        ComboPooledDataSource cpds = (ComboPooledDataSource) dataSource;
        try {
            System.out.printf("Connection Pool Status - Total: %d, Busy: %d, Idle: %d%n",
                    cpds.getNumConnections(), cpds.getNumBusyConnections(), cpds.getNumIdleConnections());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    // 原有逻辑...
}

三、额外优化建议

  • 降低acquireIncrement到5,避免连接池耗尽时瞬间获取过多连接导致数据库压力过大。
  • 缩短maxIdleTime到300秒(5分钟),及时回收空闲连接。
  • 检查createQuery方法生成的SQL是否高效,避免慢查询导致连接长时间占用。

内容的提问来源于stack exchange,提问作者Joe Taras

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 09:23:59