使用flushMode = AUTO时,Hibernate查询要慢得多,直到调用clear()为止

时间:2012-04-13 15:35:32

标签: hibernate jpa

我有一个使用Hibernate(通过JPA)的长期运行(但相当简单)的应用程序。它在运行时遇到了相当戏剧性的减速。我已经能够缩小到需要偶尔entityManager.clear()的电话。当Hibernate的实体管理器跟踪100,000个实体时,它比仅跟踪几个实体的速度慢约100倍(见下面的结果)。 我的问题是:为什么在跟踪很多实体时,Hiberate的速度如此之慢?还有其他方法吗?


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

特别是org.hibernate.event.internal.AbstractFlushingEventListener的{​​{1}}方法(至少在Hibernate 4.1.1.Final中)。其中有一个循环遍历持久化上下文中的 ALL 实体,执行一些广泛的检查来清除每个实体(尽管在我的示例中已经刷新了所有实体!)。

部分回答我的问题的第二部分,可以通过在查询上将刷新模式设置为flushEntities()来解决性能问题(请参阅下面的更新结果)。 e.g。

FlushModeType.COMMIT

...但这似乎是一个相当丑陋的解决方案 - 传递责任,知道是否将事物刷新到查询方法而不是将其保留在更新方法中。它也意味着我要么必须在所有查询方法上将刷新模式设置为COMMIT,要么更有可能在EntityManager上设置它。

这让我想知道:这是预期的行为吗?我是否在刷新或者如何定义实体时出错?或者这是Hibernate的限制(或可能是错误)?


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

测试实体

Place place = em.createQuery("from Place where name = :name", Place.class)
    .setParameter("name", name)
    .setFlushMode(FlushModeType.COMMIT)  // <-- yay!
    .getSingleResult();

基准代码

我测试的代码生成了100000个随机地名并插入它们。然后按名称随机查询5000个。名称列上有一个索引。

@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(); }
}

为了进行比较,为了确保它不是数据库中的东西,我在一个随机选择的5000个地名上运行了以下基于JDBC的查询(在Place place = em.createQuery( "select p from Place p where p.name = :name", Place.class) .setParameter("name", name) .getSingleResult(); 下):

em.unwrap(Session.class).doWork(...)

(注意,我为基准测试的每个5000查询创建并关闭了PreparedStatement。)

结果

以下所有结果均为5000次查询的平均值。 JVM被赋予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();

-Xmx1G

其他观察:在Hibernate查询期间(没有任何明确的调用),java进程在接近100%的利用率下固定核心。 JVM从未超过500MB堆。在查询过程中也有很多GC活动,但CPU利用率明显受Hibernate代码控制。

2 个答案:

答案 0 :(得分:8)

  

但主要是我很好奇为什么Hibernate似乎展示了查询的O(n)甚至O(n ^ 2)查找 - 似乎应该能够使用哈希表或二进制树来查看保持查询快速。当跟踪100000个实体与100个实体时,请注意2个数量级的差异。

O(n²)复杂性源于必须处理查询的方式。由于Hibernate内部延迟更新和插入,只要它可以(使用机会将类似的更新/插入组合在一起,特别是如果你设置了对象的多个属性)。

因此,在您可以保存查询数据库中的对象之前,Hibernate必须检测所有对象更改并清除所有更改。这里的问题是hibernate还有一些通知和拦截。因此,它迭代由持久化上下文管理的每个实体对象。即使对象本身不可变,它也可能包含可变对象甚至引用集合。

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

但是让我们看看代码一分钟:

准备查询的刷新调用结果为:

DefaultFlushEventListener.onFlush(..)

- &GT; AbstractFlushingEventListener.flushEverythingToExecution(事件)      - &GT; 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创建了一个新的Object并做了更多的事情。这是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²)正在进行。

答案 1 :(得分:6)

也许你很熟悉EntityManager跟踪持久对象(即通过调用em.createQuery(...).getSingleResult()创建的对象)。它们积累在所谓的持久化上下文会话(Hibernate术语)中,并允许非常整洁功能。例如,您可以通过调用mutator方法setName(...)来修改对象,EntityManager将在适当的时候将内存中的状态更改与数据库(将发出UPDATE语句)同步。发生这种情况时无需调用显式save()update()方法。您所需要的只是使用对象,就像它是普通的Java对象一样,EntityManager将处理持久性。

为什么这很慢(呃)?

首先,它确保内存中每个主键只有一个,单个实例。这意味着如果你加载一个相同的行两次,那么堆中只会创建一个对象(两个结果都是==)。这很有意义 - 想象一下,如果你有2个相同行的副本,EntityManager不能保证它可靠地同步Java对象,因为你可以独立地对这两个对象进行更改。如果有许多对象需要跟踪,也许还有很多其他低级操作最终会降低Entitymanager的速度。 clear()方法实际上删除了持久化上下文中的对象并使任务更容易(跟踪的对象越少=操作越快)。

你怎么能绕过它?

如果您的EntityManager实施是Hibernate,则可以使用旨在解决这些性能损失的StatelessSession。我想你可以通过:

StatelessSession session = ((Session) entityManager.getDelegate()).getSessionFactory().openStatelessSession();

(注意!代码未经测试,取自另一个question