我正在使用最新版本的Spring Boot编写应用程序。我最近成了堆增长的问题,不能被垃圾收集。使用Eclipse MAT对堆进行分析表明,在运行应用程序的一小时内,堆增长到630MB,而Hibernate的SessionFactoryImpl占整个堆的75%以上。
正在寻找查询计划缓存周围的可能来源,但我发现的唯一事情是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使用的示例。
问题是:为什么查询计划缓存不断增长(它不会停止,它以完整堆结束)以及如何防止这种情况?有没有人遇到类似的问题?
版本:
答案 0 :(得分:33)
我也遇到了这个问题。它基本上归结为在IN子句中有可变数量的值,而Hibernate试图缓存这些查询计划。
关于此主题有两篇很棒的博客文章。 The first:
在具有子句查询的项目中使用Hibernate 4.2和MySQL 例如:
select t from Thing t where t.id in (?)
Hibernate缓存这些已解析的HQL查询。特别是Hibernate
SessionFactoryImpl
QueryPlanCache
queryPlanCache
和parameterMetadataCache
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_)
, 2SELECT 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 article,atlassian。
答案 2 :(得分:2)
从Hibernate 5.2.12开始,您可以使用以下命令指定hibernate配置属性以更改文字绑定到基础JDBC预准备语句的方式:
hibernate.criteria.literal_handling_mode=BIND
从Java文档中,此配置属性有3个设置
答案 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)
说明:
如果查询包含 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查询。