Spring + Hibernate:查询计划缓存内存使用情况

Las*_*Elb 26 postgresql spring hibernate spring-boot

我正在使用最新版本的Spring Boot编写应用程序.我最近成了堆增长的问题,不能被垃圾收集.使用Eclipse MAT对堆进行的分析表明,在运行应用程序的一小时内,堆增长到630MB,而Hibernate的SessionFactoryImpl使用了整个堆的75%以上.

在此输入图像描述

是在寻找查询计划缓存的可能来源,但我发现的唯一的事情就是这个,但是没有发挥出来.属性设置如下:

spring.jpa.properties.hibernate.query.plan_cache_max_soft_references=1024
spring.jpa.properties.hibernate.query.plan_cache_max_strong_references=64
Run Code Online (Sandbox Code Playgroud)

数据库查询都是由Spring的Query魔法生成的,使用本文档中的存储库接口.使用此技术生成了大约20个不同的查询.没有使用其他本机SQL或HQL.样品:

@Transactional
public interface TrendingTopicRepository extends JpaRepository<TrendingTopic, Integer> {
    List<TrendingTopic> findByNameAndSource(String name, String source);
    List<TrendingTopic> findByDateBetween(Date dateStart, Date dateEnd);
    Long countByDateBetweenAndName(Date dateStart, Date dateEnd, String name);
}
Run Code Online (Sandbox Code Playgroud)

要么

List<SomeObject> findByNameAndUrlIn(String name, Collection<String> urls);
Run Code Online (Sandbox Code Playgroud)

作为IN使用的示例.

问题是:为什么查询计划缓存不断增长(它不会停止,它以完整堆结束)以及如何防止这种情况?有没有人遇到类似的问题?

版本:

  • Spring Boot 1.2.5
  • Hibernate 4.3.10

Nee*_*aks 42

我也遇到了这个问题.它基本上归结为在IN子句中具有可变数量的值,而Hibernate试图缓存这些查询计划.

关于这个主题有两篇很棒的博客文章. 第一个:

在具有子句查询的项目中使用Hibernate 4.2和MySQL,例如: select t from Thing t where t.id in (?)

Hibernate缓存这些解析的HQL查询.具体地说休眠 SessionFactoryImpl具有QueryPlanCachequeryPlanCacheparameterMetadataCache.但是当插入子句的参数数量很大并且变化时,这被证明是一个问题.

这些缓存会针对每个不同的查询而增长.因此,6000个参数的查询与6001不同.

子句查询扩展为集合中的参数数量.元数据包含在查询中的每个参数的查询计划中,包括生成的名称,如x10_,x11_等.

想象一下4000个不同的子句内参数计数变化,每个变量平均有4000个参数.每个参数的查询元数据在内存中快速累加,填满堆,因为它不能被垃圾回收.

这将持续到查询参数计数中的所有不同变体被缓存或JVM用尽堆内存并开始抛出java.lang.OutOfMemoryError:Java堆空间.

避免使用子句是一种选择,也可以使用固定的参数集合大小(或至少更小的大小).

有关配置查询计划缓存最大大小的信息,请参阅该属性 hibernate.query.plan_cache_max_size,默认为2048(对于具有许多参数的查询而言,它太大).

第二(也从第一参考):

Hibernate内部使用缓存将HQL语句(作为字符串)映射到查询计划.缓存由有界映射组成,默认情况下限制为2048个元素(可配置).所有HQL查询都通过此缓存加载.如果未命中,则该条目将自动添加到缓存中.这使得它非常容易受到颠簸 - 我们不断地将新条目放入缓存而不重用它们从而阻止缓存带来任何性能提升(甚至增加了一些缓存管理开销).更糟糕的是,很难偶然发现这种情况 - 你必须明确地分析缓存,以便注意到你有问题.我将在稍后谈谈如何做到这一点.

因此,缓存抖动是由于以高速率生成新查询的结果.这可能是由许多问题引起的.我见过的两个最常见的是 - 休眠中的错误,它导致参数在JPQL语句中呈现,而不是作为参数传递和使用"in" - 子句.

由于hibernate中存在一些模糊的错误,有些情况下参数未正确处理并被呈现到JPQL查询中(例如,检查HHH-6280).如果您的查询受到此类缺陷的影响并且以高速执行,则会破坏您的查询计划缓存,因为生成的每个JPQL查询几乎都是唯一的(例如,包含您的实体的ID).

第二个问题是hibernate使用"in"子句处理查询的方式(例如,给我公司id字段为1,2,10,18之一的所有人员实体).对于"in"-clause中的每个不同数量的参数,hibernate将产生不同的查询 - 例如, select x from Person x where x.company.id in (:id0_)对于1个参数, select x from Person x where x.company.id in (:id0_, :id1_)对于2个参数,依此类推.就查询计划缓存而言,所有这些查询都被认为是不同的,从而再次导致缓存抖动.你可以通过编写一个实用程序类来产生一定数量的参数来解决这个问题 - 例如1,10,100,200,500,1000.例如,如果你传递22个参数,它将返回100个列表包含22个参数的元素和剩余的78个参数设置为不可能的值(例如,对于用于外键的ID为-1).我同意这是一个丑陋的黑客,但可以完成工作.因此,您的缓存中最多只能有6个唯一查询,从而减少颠簸.

那你怎么知道你有这个问题?您可以编写一些额外的代码并公开包含缓存中条目数量的指标,例如通过JMX,调整日志记录和分析日志等.如果您不想(或不能)修改应用程序,您可以直接转储堆和对其运行此OQL查询(例如,使用)SELECT l.query.toString() FROM INSTANCEOF org.hibernate.engine.query.spi.QueryPlanCache$HQLQueryPlanKey l.它将输出当前位于堆上任何查询计划缓存中的所有查询.应该很容易发现您是否受到任何上述问题的影响.

就性能影响而言,很难说它取决于太多因素.我看到了一个非常简单的查询,导致在创建新的HQL查询计划上花费了10-20毫秒的开销.一般来说,如果某个地方有缓存,那么必须有一个很好的理由 - 错过可能是昂贵的,所以你应尽量避免错过.最后但同样重要的是,您的数据库也必须处理大量独特的SQL语句 - 导致它解析它们,并为每个SQL语句创建不同的执行计划.

  • 非常感谢!我们遇到了同样的问题,并做了大量工作来优化我们的代码。但是,只有在我们在启动tomcat时为java启用heapDumpOnOutOfMemoryErrors选项后才找到原因。堆转储显示出与您上面描述的完全相同的问题。 (3认同)
  • 只要您使用的是 Hibernate 5.2.17 或更高版本,请参阅下面 Alex 的答案,了解使用“hibernate.query.in_clause_parameter_padding=true”提供此操作的更简单方法。 (2认同)

Ale*_*lex 11

我对 IN 查询中的许多(> 10000)个参数有同样的问题。我的参数数量总是不同的,我无法预测这一点,我的QueryCachePlan增长太快了。

对于支持执行计划缓存的数据库系统,如果可能的 IN 子句参数数量减少,则更有可能命中缓存。

幸运的是,5.3.0 及更高版本的 Hibernate 有一个在 IN 子句中填充参数的解决方案。

Hibernate 可以将绑定参数扩展为 2 的幂:4、8、16、32、64。这样,具有 5、6 或 7 个绑定参数的 IN 子句将使用 8 IN 子句,因此重用其执行计划.

如果要激活此功能,则需要将此属性设置为 true hibernate.query.in_clause_parameter_padding=true

有关更多信息,请参阅本文atlassian