Java 使用Hibernate执行数千次插入时CPU利用率高

Java 使用Hibernate执行数千次插入时CPU利用率高,java,performance,hibernate,jpa,transactions,Java,Performance,Hibernate,Jpa,Transactions,我们最近使用Hibernate和EntityManager(无Spring)实现了DB绑定,以将记录写入数据库。为了简单起见,我将只讨论只进行插入的过程的变化。(另一个非常类似的过程将现有记录更新一次以设置状态,但除此之外,只插入一组记录。) 这个过程可以在每个事务中插入多达10000条记录,尽管平均值比这个数字要小,可能至少要少一半。我们可能会在同一JVM下的不同线程中同时运行该进程的几个实例 我们在生产过程中遇到了一个问题,该进程运行时所使用的服务将机器上的所有24个内核都挂断。(他们增加了

我们最近使用Hibernate和EntityManager(无Spring)实现了DB绑定,以将记录写入数据库。为了简单起见,我将只讨论只进行插入的过程的变化。(另一个非常类似的过程将现有记录更新一次以设置状态,但除此之外,只插入一组记录。)

这个过程可以在每个事务中插入多达10000条记录,尽管平均值比这个数字要小,可能至少要少一半。我们可能会在同一JVM下的不同线程中同时运行该进程的几个实例

我们在生产过程中遇到了一个问题,该进程运行时所使用的服务将机器上的所有24个内核都挂断。(他们增加了12只是为了适应这种情况。)我们已经将这种高利用率缩小到Hibernate

我花了好几天的时间进行研究,除了使用hibernate.jdbc.batch_size和hibernate.order_插入外,找不到任何可以提高性能的方法。不幸的是,我们使用标识作为生成策略,因此Hibernate可以/不会批处理这些插入

我花了好几天的时间进行研究,在做大量插入时,没有发现任何其他性能提示。(我见过很多关于读取、更新和删除的提示,但很少有关于插入的。)

我们有一个根JobPO对象。我们只需在此基础上调用merge,所有插入都通过级联注释进行处理。我们需要在一笔交易中完成这项工作

我们只插入了8个不同的表,但是记录的层次结构有点复杂

public void saveOrUpdate(Object dataHierarchyRoot) {
    final EntityManager entityManager = entityManagerFactory.createEntityManager();
    final EntityTransaction transaction = entityManager.getTransaction();

    try {
        transaction.begin();

        // This single call may result in inserting up to 10K records
        entityManager.merge(dataHierarchyRoot);
        transaction.commit();
    } catch (final Throwable e) {
        // error handling redacted for brevity
    } finally {
        entityManager.close();
    }
}
我们只创建一次EntityManagerFactory

有什么想法吗

补充说明:

  • 没有人抱怨这个过程占用了太多内存

  • 对于只执行插入的流程变体,我们可以使用“persist”而不是“merge”。我们正在共享代码,所以我们进行合并。我试着坚持下去,但没有明显的进步

  • 我们确实有注释,可以在一些字段上实现双向级联。我试图删除这些,但由于不熟悉Hibernate,无法将其正确保存。但据我所知,这似乎不会导致插入的性能下降。我没有使用显式的“反向”设置,因为这似乎对插入也不重要。不过,我在这两方面都有点动摇。这方面还有改进的余地吗

  • 我们在一个事务中运行了SQL事件探查器。似乎没有什么不对劲,我也没有发现改进的余地。(有大量exec sp_prepexec语句,与插入的记录数大致相同。这就是报告的全部内容。)

  • 在生产中显示此行为的代码在commit()之前显式调用entityManager.flush()。我在本地环境中删除了该代码。它没有明显的改进,但我不会再添加它,因为我们没有理由调用flush()


如果为每个要保存的对象打开和关闭一个会话,那么对于10k对象,实际上就是打开和关闭10k会话,刷新10k次,并进入数据库进行10k往返

你们至少应该在一起:

for (Object entity: entities) {    
    if(entity.getId() == null) {
        entityManager.persist(entity);
    } else {
        entityManager.merge(entity);
    }   
    if ((i % batchSize) == 0) {
        entityManager.getTransaction().commit();
        entityManager.clear();          
        entityManager.getTransaction().begin();
    }
}
entityManager.getTransaction().commit();
em.getTransaction().commit();
在本例中,您实际上使用的是一个数据库连接,因此即使使用连接池,您也不必获取/释放10k DB连接。会话在达到
batchSize
阈值后被清除,因此减少了JVM垃圾收集

如果在会话中存储10k个实体并立即提交事务,则会遇到以下问题:

  • 数据库将在更长的时间内持有锁,并将创建巨大的撤消事务日志(如果您的数据库使用MVCC)
  • 实体不会被垃圾收集,因为它们仍然连接到Hibernate会话

如果为每个要保存的对象打开和关闭一个会话,那么对于10k对象,实际上就是打开和关闭10k会话,刷新10k次,并进入数据库进行10k往返

你们至少应该在一起:

for (Object entity: entities) {    
    if(entity.getId() == null) {
        entityManager.persist(entity);
    } else {
        entityManager.merge(entity);
    }   
    if ((i % batchSize) == 0) {
        entityManager.getTransaction().commit();
        entityManager.clear();          
        entityManager.getTransaction().begin();
    }
}
entityManager.getTransaction().commit();
em.getTransaction().commit();
在本例中,您实际上使用的是一个数据库连接,因此即使使用连接池,您也不必获取/释放10k DB连接。会话在达到
batchSize
阈值后被清除,因此减少了JVM垃圾收集

如果在会话中存储10k个实体并立即提交事务,则会遇到以下问题:

  • 数据库将在更长的时间内持有锁,并将创建巨大的撤消事务日志(如果您的数据库使用MVCC)
  • 实体不会被垃圾收集,因为它们仍然连接到Hibernate会话

嗯,您应该避免在每次更新时打开和关闭连接,因为这会影响性能。相反,您可以将持久性提供程序配置为使用批处理并设置合理的数量,然后执行批更新

<persistence-unit name="pu" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect"/>
            <property name="hibernate.connection.username" value="***"/>
            <property name="hibernate.connection.password" value="***"/>
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver"/>
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@***"/>
            <property name="hibernate.jdbc.batch_size" value="100"/>   
        </properties>
    </persistence-unit>

org.hibernate.ejb.HibernatePersistence
这允许在更新/插入循环时,在单个命令中将多个更新查询发送到数据库(这对您是透明的)

Session Session=SessionFactory.openSession();
事务tx=会话.beginTransaction();

对于(int i=0;i那么,您应该避免在每次更新时打开和关闭连接,因为这会影响性能。相反,您可以将持久性提供程序配置为使用批处理并设置合理的数量,然后执行批处理更新

<persistence-unit name="pu" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect"/>
            <property name="hibernate.connection.username" value="***"/>
            <property name="hibernate.connection.password" value="***"/>
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver"/>
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@***"/>
            <property name="hibernate.jdbc.batch_size" value="100"/>   
        </properties>
    </persistence-unit>