hibernate Spring +休眠:查询计划缓存内存使用情况

brccelvz  于 2022-11-24  发布在  Spring
关注(0)|答案(9)|浏览(180)

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

IS在查询计划缓存周围寻找可能的源,但我找到的唯一源是this,但这并没有发挥作用。

spring.jpa.properties.hibernate.query.plan_cache_max_soft_references=1024
spring.jpa.properties.hibernate.query.plan_cache_max_strong_references=64

数据库查询都是由Spring的Query magic生成的,使用的存储库接口如本文档所示。使用这种技术生成的查询大约有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);
}

List<SomeObject> findByNameAndUrlIn(String name, Collection<String> urls);

作为IN使用示例。
问题是:为什么查询计划缓存会不断增长(它不会停止,而是以满堆结束)?如何防止这种情况?有人遇到过类似的问题吗?
版本:

  • Spring Boot1.2.5
  • 休眠4.3.10
nfg76nw0

nfg76nw01#

我也遇到过这个问题,基本上可以归结为IN子句中的值的数量是可变的,而Hibernate试图缓存这些查询计划。
关于这个主题,有两篇很棒的博客文章。The first
在项目中使用Hibernate 4.2和MySQL,并包含子句内查询,例如:select t from Thing t where t.id in (?)
Hibernate会缓存这些解析的HQL查询。具体来说,Hibernate SessionFactoryImpl包含QueryPlanCachequeryPlanCacheparameterMetadataCache。但事实证明,当in-clause的参数数量很大且变化不定时,这将是一个问题。
这些缓存会随着每个不同的查询而增长,因此这个包含6000个参数的查询与6001不同。
子句内查询会扩充为集合中的参数数目。查询计划中会包含查询中每个参数的中继数据,包括产生的名称,例如x10_、x11_等。
假设子句内参数计数有4000个不同的变化,每个变化平均有4000个参数,每个参数的查询元数据在内存中迅速增加,填满了堆,因为它不能被垃圾收集。
这种情况一直持续到查询参数计数中的所有不同变体都被缓存或者JVM耗尽堆内存并开始抛出java.lang.OutOfMemoryError:Java堆空间。
可以选择避免使用in-clause,也可以对参数使用固定的集合大小(或至少使用较小的大小)。
若要设定查询计划快取大小上限,请参阅属性hibernate.query.plan_cache_max_size,预设值为2048(对于具有许多参数的查询而言,很容易太大)。
second(也从第一个引用):
Hibernate内部使用MapHQL语句的cache(作为字符串)设置为query plans。缓存由一个有界Map组成,默认情况下限制为2048个元素(可配置)。所有HQL查询都通过此缓存加载。如果未命中,该条目会自动添加到缓存中。这使得它非常容易发生系统颠簸-在这种情况下,我们不断地将新条目放入缓存,而从不重用它们,从而阻止缓存带来任何性能提升(它甚至增加了一些缓存管理开销)。更糟糕的是,很难偶然地检测到这种情况-您必须显式地分析缓存以便注意到那里存在问题。稍后我将简单介绍如何实现这一点。
因此高速缓存抖动是由大量新查询产生的结果。这可能是由许多问题引起的。我见过的两个最常见的问题是-Hibernate中的错误,它导致参数在JPQL语句中呈现,而不是作为参数传递,以及使用“in”子句。
由于Hibernate中存在一些不明显的错误,有时会出现参数未被正确处理并被呈现到JPQL查询中的情况(例如, checkout HHH-6280)。如果您有一个受此类缺陷影响的查询,并且该查询的执行率很高,则它将使您的查询计划缓存出现问题,因为生成的每个JPQL查询几乎都是唯一的(例如,包含您的实体的ID)。
第二个问题在于Hibernate处理带有“in”子句的查询的方式(例如,给我所有公司ID字段为1,2,10,18之一的人员实体)。对于“in”子句中的每个不同数量的参数,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个参数设置为不可能的值(例如,-1表示用于外键的ID)。我同意这是一个丑陋的黑客,但可以完成工作。因此,您的缓存中最多只有6个唯一查询,从而减少了系统颠簸。
那么,如何发现问题呢?您可以编写一些额外的代码,并通过缓存中的条目数(例如,通过JMX)公开度量,调整日志记录并分析日志等。如果您不想(或不能)修改应用程序,则可以转储堆并对其运行此OQL查询(例如,使用mat):SELECT l.query.toString() FROM INSTANCEOF org.hibernate.engine.query.spi.QueryPlanCache$HQLQueryPlanKey l。它将输出当前位于堆上任何查询计划缓存中的所有查询。应该很容易发现您是否受到上述任何问题的影响。

至于性能的影响,很难说,因为它取决于太多的因素。我见过一个非常琐碎的查询,在创建一个新的HQL查询计划时会花费10-20毫秒的开销。一般来说,如果某个地方有缓存,那么一定有一个很好的理由a-一次未命中可能代价很高,所以你应该尽可能地避免未命中。最后但并非最不重要的是,您的数据库还必须处理大量的唯一SQL语句-导致它解析这些语句并且可能为每个语句创建不同的执行计划。

1aaf6o9v

1aaf6o9v2#

我在IN查询中有很多(〉10000)参数也有同样的问题。我的参数的数量总是不同的,我不能预测这一点,我的QueryCachePlan增长太快了。
对于支持执行计划高速缓存的数据库系统,如果可能的IN子句参数的数量减少,则命中高速缓存的机会更大。
幸运的是,5.2.18及更高版本的Hibernate提供了一种解决方案,可以在IN子句中填充参数。
Hibernate可以将绑定参数扩展为2的幂:4、8、16、32、64。这样,具有5、6或7个绑定参数的IN子句将使用8个IN子句,因此可以重用其执行计划。
如果要激活此功能,需要将此属性设置为true hibernate.query.in_clause_parameter_padding=true
如需详细信息,请参阅this articleatlassian

z8dt9xmd

z8dt9xmd3#

我在使用Spring Boot 1.5.7和Spring Data(Hibernate)时遇到了完全相同的问题,下面的配置解决了这个问题(内存泄漏):

spring:
  jpa:
    properties:
      hibernate:
        query:
          plan_cache_max_size: 64
          plan_parameter_metadata_max_size: 32
5lhxktic

5lhxktic4#

从Hibernate 5.2.12开始,可以使用以下命令指定Hibernate配置属性,以更改将文字绑定到底层JDBC预准备语句得方式:

hibernate.criteria.literal_handling_mode=BIND

在Java文档中,此配置属性有3个设置
1.自动(默认)

  1. BIND -使用绑定参数增加jdbc语句缓存的可能性。
  2. INLINE -内联值而不是使用参数(小心SQL注入)。
q3qa4bjr

q3qa4bjr5#

我也遇到过类似的问题,这是因为您在创建查询时没有使用PreparedStatement。因此,对于每个具有不同参数的查询,它都会创建一个执行计划并将其缓存。如果您使用了PreparedStatement,那么您应该会看到内存使用量的大幅提高。

7vux5j2d

7vux5j2d6#

TL;DR:尝试将IN()查询替换为ANY()或将其删除

说明:

如果一个查询包含IN(...),那么会为IN(...)中的每一个值创建一个计划,因为 query 每次都是不同的。所以如果你有IN('a','b','c')和IN('a','b','c','d','e')-这是两个不同的查询字符串/计划要缓存。这个answer告诉了更多关于它的信息。
在ANY(...)的情况下,可以传递单个(数组)参数,因此查询字符串将保持不变,并且预准备语句计划将被缓存一次(下面给出的示例)。

原因:

此行可能导致以下问题:

List<SomeObject> findByNameAndUrlIn(String name, Collection<String> urls);

因为它会为“urls”集合中的每个值生成不同的IN()查询。

警告:
您可能在不编写IN()查询的情况下,甚至在不知道它的情况下,就使用了它。

ORM(如Hibernate)可能会在后台生成它们-有时在意外的地方,有时以非最佳的方式。因此,考虑启用查询日志以查看您的实际查询。

修正:

下面是一个(伪)代码,可以解决这个问题:

query = "SELECT * FROM trending_topic t WHERE t.name=? AND t.url=?"
PreparedStatement preparedStatement = connection.prepareStatement(queryTemplate);
currentPreparedStatement.setString(1, name); // safely replace first query parameter with name
currentPreparedStatement.setArray(2, connection.createArrayOf("text", urls.toArray())); // replace 2nd parameter with array of texts, like "=ANY(ARRAY['aaa','bbb'])"

"但是"
不要把任何解决方案当作现成的答案。确保在投入生产之前在实际/大数据上测试最终性能--无论你选择哪种答案。为什么?因为IN和ANY都有优缺点,如果使用不当,它们会带来严重的性能问题(请参见下面参考资料中的示例)。还确保使用parameter binding以避免安全问题。

参考资料:

100x faster Postgres performance by changing 1 line-任意(ARRAY[])与任意(VALUES())的性能比较
Index not used with =any() but used with in-IN和ANY的不同性能
Understanding SQL Server query plan cache
希望这能有所帮助。无论是否有效,请务必留下反馈--以便帮助像你这样的人。谢谢!

rkue9o1l

rkue9o1l7#

我在这个queryPlanCache上遇到了一个大问题,所以我做了一个Hibernate缓存监视器来查看queryPlanCache中的查询。我在QA环境中每5分钟使用一次Spring任务。我发现我必须更改哪些IN查询来解决缓存问题。详细信息如下:我正在使用Hibernate 4.2.18,我不知道是否会对其他版本有用。

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.hibernate.ejb.HibernateEntityManagerFactory;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.internal.util.collections.BoundedConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.dao.GenericDAO;

public class CacheMonitor {

private final Logger logger  = LoggerFactory.getLogger(getClass());

@PersistenceContext(unitName = "MyPU")
private void setEntityManager(EntityManager entityManager) {
    HibernateEntityManagerFactory hemf = (HibernateEntityManagerFactory) entityManager.getEntityManagerFactory();
    sessionFactory = (SessionFactoryImpl) hemf.getSessionFactory();
    fillQueryMaps();
}

private SessionFactoryImpl sessionFactory;
private BoundedConcurrentHashMap queryPlanCache;
private BoundedConcurrentHashMap parameterMetadataCache;

/*
 * I tried to use a MAP and use compare compareToIgnoreCase.
 * But remember this is causing memory leak. Doing this
 * you will explode the memory faster that it already was.
 */

public void log() {
    if (!logger.isDebugEnabled()) {
        return;
    }

    if (queryPlanCache != null) {
        long cacheSize = queryPlanCache.size();
        logger.debug(String.format("QueryPlanCache size is :%s ", Long.toString(cacheSize)));

        for (Object key : queryPlanCache.keySet()) {
            int filterKeysSize = 0;
            // QueryPlanCache.HQLQueryPlanKey (Inner Class)
            Object queryValue = getValueByField(key, "query", false);
            if (queryValue == null) {
                // NativeSQLQuerySpecification
                queryValue = getValueByField(key, "queryString");
                filterKeysSize = ((Set) getValueByField(key, "querySpaces")).size();
                if (queryValue != null) {
                    writeLog(queryValue, filterKeysSize, false);
                }
            } else {
                filterKeysSize = ((Set) getValueByField(key, "filterKeys")).size();
                writeLog(queryValue, filterKeysSize, true);
            }
        }
    }

    if (parameterMetadataCache != null) {
        long cacheSize = parameterMetadataCache.size();
        logger.debug(String.format("ParameterMetadataCache size is :%s ", Long.toString(cacheSize)));
        for (Object key : parameterMetadataCache.keySet()) {
            logger.debug("Query:{}", key);
        }
    }
}

private void writeLog(Object query, Integer size, boolean b) {
    if (query == null || query.toString().trim().isEmpty()) {
        return;
    }
    StringBuilder builder = new StringBuilder();
    builder.append(b == true ? "JPQL " : "NATIVE ");
    builder.append("filterKeysSize").append(":").append(size);
    builder.append("\n").append(query).append("\n");
    logger.debug(builder.toString());
}

private void fillQueryMaps() {
    Field queryPlanCacheSessionField = null;
    Field queryPlanCacheField = null;
    Field parameterMetadataCacheField = null;
    try {
        queryPlanCacheSessionField = searchField(sessionFactory.getClass(), "queryPlanCache");
        queryPlanCacheSessionField.setAccessible(true);
        queryPlanCacheField = searchField(queryPlanCacheSessionField.get(sessionFactory).getClass(), "queryPlanCache");
        queryPlanCacheField.setAccessible(true);
        parameterMetadataCacheField = searchField(queryPlanCacheSessionField.get(sessionFactory).getClass(), "parameterMetadataCache");
        parameterMetadataCacheField.setAccessible(true);
        queryPlanCache = (BoundedConcurrentHashMap) queryPlanCacheField.get(queryPlanCacheSessionField.get(sessionFactory));
        parameterMetadataCache = (BoundedConcurrentHashMap) parameterMetadataCacheField.get(queryPlanCacheSessionField.get(sessionFactory));
    } catch (Exception e) {
        logger.error("Failed fillQueryMaps", e);
    } finally {
        queryPlanCacheSessionField.setAccessible(false);
        queryPlanCacheField.setAccessible(false);
        parameterMetadataCacheField.setAccessible(false);
    }
}

private <T> T getValueByField(Object toBeSearched, String fieldName) {
    return getValueByField(toBeSearched, fieldName, true);
}

@SuppressWarnings("unchecked")
private <T> T getValueByField(Object toBeSearched, String fieldName, boolean logErro) {
    Boolean accessible = null;
    Field f = null;
    try {
        f = searchField(toBeSearched.getClass(), fieldName, logErro);
        accessible = f.isAccessible();
        f.setAccessible(true);
    return (T) f.get(toBeSearched);
    } catch (Exception e) {
        if (logErro) {
            logger.error("Field: {} error trying to get for: {}", fieldName, toBeSearched.getClass().getName());
        }
        return null;
    } finally {
        if (accessible != null) {
            f.setAccessible(accessible);
        }
    }
}

private Field searchField(Class<?> type, String fieldName) {
    return searchField(type, fieldName, true);
}

private Field searchField(Class<?> type, String fieldName, boolean log) {

    List<Field> fields = new ArrayList<Field>();
    for (Class<?> c = type; c != null; c = c.getSuperclass()) {
        fields.addAll(Arrays.asList(c.getDeclaredFields()));
        for (Field f : c.getDeclaredFields()) {

            if (fieldName.equals(f.getName())) {
                return f;
            }
        }
    }
    if (log) {
        logger.warn("Field: {} not found for type: {}", fieldName, type.getName());
    }
    return null;
}
}
izkcnapc

izkcnapc8#

我们还有一个堆使用率不断增长的QueryPlanCache。我们重写了IN查询,此外,我们还有使用自定义类型的查询。结果是Hibernate类CustomType没有正确实现equals和hashCode,从而为每个查询示例创建了一个新键。这个问题现在在Hibernate 5.3中得到了解决。请参见https://hibernate.atlassian.net/browse/HHH-12463。您仍然需要正确实现equals/hashCode以使其正常工作。

3gtaxfhh

3gtaxfhh9#

我们曾经遇到过这个问题,查询计划缓存增长过快,旧的gen堆也随之增长,因为gc无法收集它。罪魁祸首是JPA查询在IN子句中使用了超过200000个id。为了优化查询,我们使用了连接,而不是从一个表中提取id并在其他表选择查询中传递这些id。

相关问题