为何此场景下Hikari Datasource会内存泄漏?如何缓存数据源?
问题
我在Spring Framework(搭配JPA)中,通过解析URL中的id动态创建并使用DataSource。用Caffeine缓存DataSource,但过期后对象仍未从内存释放,引发内存泄漏。就算移除缓存让DataSource只使用一次,内存泄漏问题还是存在。请问有什么合理缓存DataSource的方法?
相关代码
创建HikariDataSource的方法
private HikariDataSource getDataSourceRdsLocation(RdsLocationEntity rdsLocationEntity){ HikariConfig config = new HikariConfig(); config.setAllowPoolSuspension(true); config.setJdbcUrl("jdbc:mysql://"+rdsLocationEntity.getServerReadOnly()+"/"+rdsLocationEntity.getDatabaseName()); config.setUsername(""); config.setPassword(""); config.setPoolName(rdsLocationEntity.getName()); config.setMaximumPoolSize(10); config.setMinimumIdle(1); config.setIdleTimeout(3600000); config.setMaxLifetime(7200000); config.setConnectionTimeout(500); return new HikariDataSource(config); }
Caffeine缓存配置
new CaffeineCache(cache.getCacheName(), Caffeine.newBuilder().recordStats() .removalListener((key, value, cause) -> { if (cause.wasEvicted() && value instanceof HikariDataSource ds) { ds.close(); } }) .expireAfterWrite(cache.getExpiredAfter(), cache.getTimeUnit()) .maximumSize(cache.getMaximumSize()) .build()
解决方案
1. 修复缓存移除逻辑的漏洞
当前移除监听仅处理了wasEvicted()的场景,但缓存条目还可能因主动删除、条目替换、缓存关闭等情况被移除,这些场景下DataSource不会被关闭。修改监听逻辑,去掉cause.wasEvicted()判断,只要是DataSource就执行关闭:
.removalListener((key, value, cause) -> { if (value instanceof HikariDataSource ds) { try { ds.close(); } catch (Exception e) { // 记录关闭异常,避免中断缓存流程 log.error("Failed to close HikariDataSource for key: {}", key, e); } } })
2. 排查JPA/Hibernate的引用泄漏
单次使用仍有内存泄漏,大概率是JPA的EntityManagerFactory或Hibernate的SessionFactory未被正确销毁。动态创建DataSource时,要为每个DataSource绑定对应的EntityManagerFactory,并在DataSource关闭时同步销毁:
- 创建EntityManagerFactory时,明确关联当前动态生成的DataSource
- 在缓存移除监听中,除了关闭DataSource,还要调用
EntityManagerFactory.close() - 避免全局EntityManagerFactory引用持有动态创建的实例
3. 优化DataSource缓存策略
- 严格限制缓存容量:根据系统资源设置合理的
maximumSize,防止创建过多DataSource耗尽内存 - 使用软引用存储:如果允许内存紧张时自动回收DataSource,可配置Caffeine使用软引用:
注意:软引用回收时机由GC决定,可能导致DataSource频繁重建,需权衡性能与内存占用Caffeine.newBuilder() .softValues() // 其他缓存配置 - 定期主动清理:结合Spring定时任务,定期扫描缓存中过期的DataSource,强制关闭并移除
4. 修正HikariCP配置的内存隐患
- 关闭
allowPoolSuspension:该配置会额外持有连接池引用,非必要不要开启 - 调整连接池参数:将
minimumIdle设为0,避免闲置连接占用内存;maxLifetime不要设置过长,确保连接能及时回收 - 开启
registerMbeans:通过JMX监控连接池状态,排查是否存在连接未释放的情况
5. 用Spring AbstractRoutingDataSource替代手动缓存
Spring提供的AbstractRoutingDataSource支持动态切换DataSource,结合Caffeine缓存路由键对应的实例,同时借助Spring生命周期管理确保资源正确释放:
- 继承
AbstractRoutingDataSource,重写determineCurrentLookupKey()从ThreadLocal获取当前需要的DataSource键 - 缓存中存储DataSource实例,路由键不存在时创建新实例
- 实现
DisposableBean接口,在Bean销毁时遍历缓存关闭所有DataSource
内容的提问来源于stack exchange,提问作者SungHo Kim
相关产品推荐
相关产品推荐

