22

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


!!!更新:我已经能够将其缩小到 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();

...但这似乎是一个相当丑陋的解决方案——将了解事物是否刷新到查询方法而不是将其保留在更新方法中的责任。这也几乎意味着我必须在所有查询方法上将刷新模式设置为 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 查询期间(没有任何 clear 调用),java 进程将一个核心固定在接近 100% 的利用率。JVM 从未超过 500MB 堆。查询期间也有很多 GC 活动,但 CPU 利用率显然由 Hibernate 代码主导。

4

2 回答 2

9

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

O(n²) 复杂度源于必须处理查询的方式。由于 Hibernate 在内部尽可能地延迟更新和插入(利用机会将类似的更新/插入组合在一起,尤其是在您设置对象的多个属性时)。

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

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

但是让我们看一下代码:

准备查询的刷新调用导致:

DefaultFlushEventListener.onFlush(..)

-> AbstractFlushingEventListener.flushEverythingToExecution(事件) -> 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 管理超过 100 个或 1000 个实体会大大降低 Hibernate 的速度。在这里应该考虑更改刷新模式,使用第二个会话进行查询和一个更改(如果可能的话)或使用无状态会话。

所以你的观察是正确的,它是 O(n²) 。

于 2013-09-22T20:37:12.390 回答
8

也许您熟悉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();

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

于 2012-04-13T16:22:38.653 回答