我有一个长时间运行(但相当简单)的应用程序,它使用Hibernate (通过JPA)。它在运行过程中经历了相当戏剧性的减速。我已经能够将范围缩小到偶尔需要一个entityManager.clear()
调用。当Hibernate的实体管理器跟踪100,000个实体时,它比仅跟踪少数几个实体慢约100倍(请参见下面的结果)。我的问题是:为什么在跟踪大量实体时会如此缓慢?还有没有其他办法绕过它呢?
!更新:我已经能够将范围缩小到Hibernate的自动刷新代码。!
特别是org.hibernate.event.internal.AbstractFlushingEventListener
的flushEntities()
方法(至少在Hibernate 4.1.1.Final中是这样),其中有一个循环,它遍历持久化上下文中的所有实体,围绕刷新每个实体执行一些广泛的检查(即使在我的示例中所有实体都已经刷新了!)。
因此,部分回答了我的问题的第二部分,可以通过将查询的刷新模式设置为FlushModeType.COMMIT
来解决性能问题(请参见下面的更新结果)。例如:
Place place = em.createQuery("from Place where name = :name", Place.class)
.setParameter("name", name)
.setFlushMode(FlushModeType.COMMIT) // <-- yay!
.getSingleResult();
..。但这似乎是一个相当丑陋的解决方案--将知道事物是否被刷新到query方法的责任传递给query方法,而不是将其保留在更新方法中。这也意味着我必须在所有查询方法上设置刷新模式来提交,或者更有可能的是,在EntityManager上设置它。
这让我想知道:这是预期的行为吗?我在刷新或定义实体的方式上做错了什么吗?或者这是Hibernate的限制(或者可能是Hibernate中的bug )?
我用来隔离问题的示例代码如下:
测试实体
@Entity @Table(name="place") @Immutable
public class Place {
private Long _id;
private String _name;
@Id @GeneratedValue
public Long getId() { return _id; }
public void setId(Long id) { _id = id; }
@Basic(optional=false) @Column(name="name", length=700,
updatable=false, nullable=false, unique=true,
columnDefinition="varchar(700) character set 'ascii' not null")
public String getName() { return _name; }
public void setName(String name) { _name = name; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { return getName().hashCode(); }
}
基准代码
我的测试代码生成100000个随机的地名并插入它们。然后按名称随机查询出其中的5000个。name列上有一个索引。
Place place = em.createQuery(
"select p from Place p where p.name = :name", Place.class)
.setParameter("name", name)
.getSingleResult();
为了进行比较,并确保它不在数据库中,我对一个单独的随机选择的5000个地名运行了以下基于JDBC的查询(在em.unwrap(Session.class).doWork(...)
下):
PreparedStatement ps = c.prepareStatement(
"select id, name from place where name = ?");
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
while (rs.next()) {
Place place = new Place();
place.setId(rs.getLong(1));
place.setName(rs.getString(2));
}
rs.close();
ps.close();
(请注意,我确实为基准测试的5000个查询中的每个查询创建并关闭了一个PreparedStatement )。
结果
下面的所有结果都是5000次查询的平均值。JVM被赋予了-Xmx1G
。
Seconds/Query Approach
0.000160s JDBC
0.000286s Hibernate calling clear() after import and every 100 queries
0.000653s Hibernate calling clear() once after the import
0.012533s Hibernate w/o calling clear() at all
0.000292s Hibernate w/o calling clear(), and with flush-mode COMMIT
其他观察结果:在Hibernate查询期间(没有任何清晰调用),java进程将内核的利用率锁定在接近100%的水平。JVM堆大小从未超过500MB。在查询期间也有大量的GC活动,但是CPU利用率显然是由Hibernate代码控制的。
发布于 2013-09-23 04:37:12
但我很好奇为什么Hibernate对查询的查找次数似乎是O(n)甚至O(n^2) --它似乎应该能够在幕后使用哈希表或二叉树来保持查询的速度。当它跟踪100000个实体与100个实体时,请注意两个数量级的差异。
O(n²)复杂性源于查询必须被处理的方式。因为Hibernate在内部尽可能地延迟更新和插入(以便利用将相似的更新/插入分组在一起的机会,特别是当您设置对象的多个属性时)。
因此,在可以安全地查询数据库中的对象之前,Hibernate必须检测所有对象更改并刷新所有更改。这里的问题是hibernate也有一些通知和拦截正在进行。所以它遍历持久化上下文管理的每个实体对象。即使对象本身不是可变的,它也可能包含可变对象,甚至引用集合。
此外,拦截机制允许您访问任何被认为是脏的对象,以允许您自己的代码实现额外的脏检查或执行额外的计算,如计算总和、平均值、记录额外的信息等。
但让我们花一分钟时间看一下代码:
用于准备查询的flush调用的结果如下:
DefaultFlushEventListener.onFlush(..)
AbstractFlushingEventListener.flushEverythingToExecution(event) AbstractFlushingEventListener.prepareEntityFlushes(..) -> ->
该实现使用:
for ( Map.Entry me : IdentityMap.concurrentEntries( persistenceContext.getEntityEntries() ) ) {
EntityEntry entry = (EntityEntry) me.getValue();
Status status = entry.getStatus();
if ( status == Status.MANAGED || status == Status.SAVING || status == Status.READ_ONLY ) {
cascadeOnFlush( session, entry.getPersister(), me.getKey(), anything );
}
}
如您所见,将检索并迭代持久性上下文中所有实体的映射。
这意味着每次调用查询时,您都要迭代所有以前的结果,以检查脏对象。更多的cascadeOnFlush会创建一个新的对象,做更多的事情。下面是cascadeOnFlush的代码:
private void cascadeOnFlush(EventSource session, EntityPersister persister, Object object, Object anything)
throws HibernateException {
session.getPersistenceContext().incrementCascadeLevel();
try {
new Cascade( getCascadingAction(), Cascade.BEFORE_FLUSH, session )
.cascade( persister, object, anything );
}
finally {
session.getPersistenceContext().decrementCascadeLevel();
}
}
所以这就是解释。每次发出查询时,Hibernate只检查持久化上下文管理的每个对象。
因此,对于阅读本文的每个人来说,复杂性计算如下: 1.查询:0实体2.查询:1实体3.查询:2实体..100。查询: 100个实体。。。100k +1查询: 100k条
所以我们有O(0+1+2...+n) = O(n(n+1)/2) = O(n²)。
这就解释了你的观察结果。为了保持较小的cpu和内存占用,hibernate托管持久化上下文应该保持尽可能小。让Hibernate管理超过100或1000个实体会大大降低Hibernate的速度。在这里,人们应该考虑更改刷新模式,使用第二个会话进行查询,使用一个会话进行更改(如果这是可能的),或者使用StatelessSession。
所以你的观察是正确的,它是O(n²)在进行。
发布于 2012-04-14 00:22:39
您可能很熟悉,EntityManager
会跟踪持久对象(即通过调用em.createQuery(...).getSingleResult()
创建的对象)。它们累积在所谓的持久上下文或会话( Hibernate术语)中,并允许非常、整洁的特性。例如,您可以通过调用赋值器方法setName(...)
来修改对象,EntityManager
将在适当的时候将内存中的状态更改与数据库同步(将发出UPDATE语句)。这不需要调用显式的save()
或update()
方法。您所需要做的就是像处理普通Java对象一样处理对象,EntityManager
将负责持久化。
为什么这很慢(Er)?
首先,它确保在内存中每个主键只有一个,即单个实例。这意味着,如果您两次加载同一行,那么在堆中将只创建一个对象(两个结果都为==
)。这很有意义-想象一下,如果你有同一行的两个副本,EntityManager
不能保证它可靠地同步了Java对象,因为你可以独立地在这两个对象中进行更改。如果有许多对象需要跟踪,那么可能还有许多其他的低级操作最终会降低Entitymanager
的速度。clear()
方法实际上删除了持久上下文中的对象,并使任务变得更容易(跟踪的对象更少=操作更快)。
你怎么绕过它呢?
如果您的EntityManager
实现是Hibernate,您可以使用StatelessSession,它旨在解决这些性能损失。我想你可以做到的:
StatelessSession session = ((Session) entityManager.getDelegate()).getSessionFactory().openStatelessSession();
(注意!代码未经过测试,取自另一个question)
https://stackoverflow.com/questions/10143880
复制相似问题