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

时间:2015-07-22 07:53:28

标签: spring hibernate postgresql spring-boot

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

enter image description here

正在寻找查询计划缓存周围的可能来源,但我发现的唯一事情是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魔法生成,使用in this documentation等存储库接口。使用此技术生成了大约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 Boot 1.2.5
  • Hibernate 4.3.10

8 个答案:

答案 0 :(得分:33)

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

关于此主题有两篇很棒的博客文章。 The first

  

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

     

Hibernate缓存这些已解析的HQL查询。特别是Hibernate   SessionFactoryImpl QueryPlanCache queryPlanCacheparameterMetadataCache   hibernate.query.plan_cache_max_size。但事实证明这是一个问题   in子句的参数数量很大并且各不相同。

     

这些缓存会针对每个不同的查询而增长。所以这个查询用6000   参数与6001不同。

     

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

     

想象一下4000个不同的in-clause参数变化   计数,每个平均有4000个参数。查询   每个参数的元数据在内存中快速加起来,填满了   堆,因为它不能被垃圾收集。

     

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

     

避免使用in-clauses是一种选择,也可以使用固定的集合   参数的大小(或至少较小的大小)。

     

有关配置查询计划缓存最大大小的信息,请参阅该属性   2048,默认为select x from Person x where x.company.id in (:id0_)(也很容易   对于包含许多参数的查询来说很大。)

second(也从第一个引用):

  

Hibernate内部使用映射HQL语句的cache(如   字符串)到query plans。缓存由有界限制的地图组成   默认为2048个元素(可配置)。加载所有HQL查询   通过这个缓存。如果未命中,则自动输入   添加到缓存中。这使得它很容易受到颠簸 - a   我们不断地将新条目放入缓存的情况   永远重用它们,从而防止缓存带来任何   性能提升(甚至增加了一些缓存管理开销)。至   事情变得更糟,很难偶然发现这种情况 - 你   必须明确地分析缓存,以便注意到你有   那里有问题。我将就如何做到这一点说几句话   以后。

     

因此,缓存抖动会导致生成新查询   高利率。这可能是由许多问题引起的。最两个   我见过的常见问题是 - hibernate中导致参数的错误   要在JPQL语句中呈现而不是作为传递   参数和使用&#34; in&#34; - 条款。

     

由于hibernate中存在一些模糊的错误,有时会出现这种情况   参数未正确处理并呈现在JPQL中   查询(例如,查看HHH-6280)。如果你有一个查询   如果受到这些缺陷的影响,它会以高利率执行   thrash您的查询计划缓存,因为生成的每个JPQL查询是   几乎是唯一的(例如包含您实体的ID)。

     

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

     

那你怎么知道你有这个问题?你可以写一些   附加代码并公开指标中的条目数   缓存例如通过JMX,调整日志记录并分析日志等。如果您这样做   不想(或不能)修改应用程序,你可以只是转储   堆并对其运行此OQL查询(例如,使用mat):{{1}}。它   将输出当前位于任何查询计划缓存中的所有查询   你的堆。你应该很容易发现自己是否受到影响   任何上述问题。

     

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

答案 1 :(得分:6)

我在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

有关更多信息,请参见this articleatlassian

答案 2 :(得分:2)

从Hibernate 5.2.12开始,您可以使用以下命令指定hibernate配置属性以更改文字绑定到基础JDBC预准备语句的方式:

hibernate.criteria.literal_handling_mode=BIND

从Java文档中,此配置属性有3个设置

  1. 自动(默认)
  2. BIND - 使用绑定参数增加jdbc语句缓存的可能性。
  3. INLINE - 内联值而不是使用参数(注意SQL注入)。

答案 3 :(得分:1)

我对这个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;
}
}

答案 4 :(得分:1)

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

答案 5 :(得分:1)

我有一个类似的问题,该问题是因为您正在创建查询而不使用PreparedStatement。因此,对于具有不同参数的每个查询,这里发生的事情是创建执行计划并对其进行缓存。 如果使用准备好的语句,则应该看到正在使用的内存有了重大改进。

答案 6 :(得分:1)

TL;DR:尝试用 ANY() 替换 IN() 查询或消除它们

说明:
如果查询包含 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 - Any(ARRAY[]) vs ANY(VALUES())
的表现 Index not used with =any() but used with in - IN 和 ANY 的不同表现
Understanding SQL Server query plan cache

希望这会有所帮助。无论是否有效,请务必留下反馈 - 以帮助像您这样的人。谢谢!

答案 7 :(得分:0)

我们曾经遇到过这样的问题,即查询计划缓存增长过快,并且由于genc无法收集它,旧的gen堆也随之增长。罪魁祸首是JPA查询在IN子句中获取了超过200000个id。为了优化查询,我们使用了联接,而不是从一个表中获取ID并将那些ID传递到另一表中的select查询。