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

ɲeu*_*urɳ 19 hibernate jpa

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


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

具体到org.hibernate.event.internal.AbstractFlushingEventListener's 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();
Run Code Online (Sandbox Code Playgroud)

...但这似乎是一个相当丑陋的解决方案 - 传递责任,知道是否将事物刷新到查询方法而不是将其保留在更新方法中.它也意味着我要么必须在所有查询方法上将flush模式设置为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(); }
}
Run Code Online (Sandbox Code Playgroud)

基准代码

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

Place place = em.createQuery(
    "select p from Place p where p.name = :name", Place.class)
    .setParameter("name", name)
    .getSingleResult();
Run Code Online (Sandbox Code Playgroud)

为了进行比较,为了确保它不是数据库中的东西,我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();
Run Code Online (Sandbox Code Playgroud)

(注意,我为基准测试的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
Run Code Online (Sandbox Code Playgroud)

其他观察结果:在Hibernate查询期间(没有任何明确的调用),java进程以接近100%的利用率挂起核心.JVM从未超过500MB堆.在查询过程中也有很多GC活动,但CPU利用率明显受Hibernate代码的支配.

Mar*_*ten 8

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

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

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

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

但是让我们看一下代码:

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

DefaultFlushEventListener.onFlush(..)
Run Code Online (Sandbox Code Playgroud)

- > 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 );
        }
    }
Run Code Online (Sandbox Code Playgroud)

如您所见,检索并迭代持久化上下文中的所有实体的映射.

这意味着对于每次调用查询,您都会迭代所有以前的结果以检查脏对象.甚至更多的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();
    }
}
Run Code Online (Sandbox Code Playgroud)

所以这就是解释.每次发出查询时,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²).


Svi*_*nov 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();

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