使用flushMode=autoHibernate查询要慢得多,直到CLEAR()被调用?

内容来源于 Stack Overflow,并遵循CC BY-SA 3.0许可协议进行翻译与使用

  • 回答 (2)
  • 关注 (0)
  • 查看 (88)

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

具体到org.hibernate.event.internal.AbstractFlushingEventListenerflushEntities()方法(至少在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();

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

这让我想知道:这是预期的行为吗?我是否在做脸红或我如何定义实体时出错?或者这是Hibernate的限制(或者可能存在缺陷)?

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

测试实体

@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个。名称列上有一个索引。

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个地名运行以下基于JDBC的查询(下):

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代码决定的。

提问于
用户回答回答于

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

为什么这很慢(呃)?

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

你怎么能解决它?

如果你的EntityManager实现是Hibernate,你可以使用StatelessSession来解决这些性能问题。我认为你可以通过:

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

用户回答回答于

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

O(n2)的复杂性来自查询必须处理的方式。由于Hibernate内部延迟更新并插入(尽可能使用这种机会将相似的更新/插入组合在一起,特别是如果设置了对象的多个属性)。

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

拦截机制还允许你访问任何被视为脏的对象,以允许你自己的代码执行额外的脏脏检查或执行额外的计算,如计算总和,平均值,日志附加信息等。

但让我们看一下代码:

清理调用以准备查询结果如下:

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 2)。

这解释了你的观察。为了保持一个小的cpu和内存占用,hibernate管理的持久化上下文应尽可能小。让Hibernate管理不止可以说100或1000个实体大大减慢了Hibernate的速度。这里应该考虑更改刷新模式,使用第二个会话进行查询,并使用第二个会话进行更改(如果可以的话)或使用StatelessSession。

所以你的观察是正确的,它是O(n2)继续。

扫码关注云+社区