首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >在调用clear()之前,Hibernate使用flushMode=AUTO查询的速度要慢得多

在调用clear()之前,Hibernate使用flushMode=AUTO查询的速度要慢得多
EN

Stack Overflow用户
提问于 2012-04-13 23:35:32
回答 2查看 10.7K关注 0票数 22

我有一个长时间运行(但相当简单)的应用程序,它使用Hibernate (通过JPA)。它在运行过程中经历了相当戏剧性的减速。我已经能够将范围缩小到偶尔需要一个entityManager.clear()调用。当Hibernate的实体管理器跟踪100,000个实体时,它比仅跟踪少数几个实体慢约100倍(请参见下面的结果)。我的问题是:为什么在跟踪大量实体时会如此缓慢?还有没有其他办法绕过它呢?

!更新:我已经能够将范围缩小到Hibernate的自动刷新代码。!

特别是org.hibernate.event.internal.AbstractFlushingEventListenerflushEntities()方法(至少在Hibernate 4.1.1.Final中是这样),其中有一个循环,它遍历持久化上下文中的所有实体,围绕刷新每个实体执行一些广泛的检查(即使在我的示例中所有实体都已经刷新了!)。

因此,部分回答了我的问题的第二部分,可以通过将查询的刷新模式设置为FlushModeType.COMMIT来解决性能问题(请参见下面的更新结果)。例如:

代码语言:javascript
复制
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 )?

我用来隔离问题的示例代码如下:

测试实体

代码语言:javascript
复制
@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列上有一个索引。

代码语言:javascript
复制
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(...)下):

代码语言:javascript
复制
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

代码语言:javascript
复制
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代码控制的。

EN

回答 2

Stack Overflow用户

发布于 2013-09-23 04:37:12

但我很好奇为什么Hibernate对查询的查找次数似乎是O(n)甚至O(n^2) --它似乎应该能够在幕后使用哈希表或二叉树来保持查询的速度。当它跟踪100000个实体与100个实体时,请注意两个数量级的差异。

O(n²)复杂性源于查询必须被处理的方式。因为Hibernate在内部尽可能地延迟更新和插入(以便利用将相似的更新/插入分组在一起的机会,特别是当您设置对象的多个属性时)。

因此,在可以安全地查询数据库中的对象之前,Hibernate必须检测所有对象更改并刷新所有更改。这里的问题是hibernate也有一些通知和拦截正在进行。所以它遍历持久化上下文管理的每个实体对象。即使对象本身不是可变的,它也可能包含可变对象,甚至引用集合。

此外,拦截机制允许您访问任何被认为是脏的对象,以允许您自己的代码实现额外的脏检查或执行额外的计算,如计算总和、平均值、记录额外的信息等。

但让我们花一分钟时间看一下代码:

用于准备查询的flush调用的结果如下:

代码语言:javascript
复制
DefaultFlushEventListener.onFlush(..)

AbstractFlushingEventListener.flushEverythingToExecution(event) AbstractFlushingEventListener.prepareEntityFlushes(..) -> ->

该实现使用:

代码语言:javascript
复制
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的代码:

代码语言:javascript
复制
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²)在进行。

票数 9
EN

Stack Overflow用户

发布于 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)

票数 8
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/10143880

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档