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

为何此场景下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使用软引用:
    Caffeine.newBuilder()
            .softValues()
            // 其他缓存配置
    
    注意:软引用回收时机由GC决定,可能导致DataSource频繁重建,需权衡性能与内存占用
  • 定期主动清理:结合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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 02:01:47