多租户架构下调用@Transactional方法后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

