使用Hibernate进行数千次插入时的CPU利用率很高

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用Hibernate进行数千次插入时的CPU利用率很高相关的知识,希望对你有一定的参考价值。

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

此过程每个事务可以插入多达10,000条记录,但平均值小于该值,可能至少减半。我们可能会在同一个JVM下同时在不同的线程中运行此进程的一些实例。

我们遇到了一个生产问题,即运行该流程的服务是将机器上的所有24个核心连接起来。 (他们增加了12只是为了试图适应这种情况。)我们将这种高利用率缩小到了Hibernate。

我花了几天研究,除了使用hibernate.jdbc.batch_size和hibernate.order_inserts之外,找不到任何可以改善我们性能的东西。不幸的是,我们使用IDENTITY作为我们的生成策略,因此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次往返。

你应该至少batch multiple entities在一起:

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数据库连接。达到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>

这允许在更新/插入循环时将单个命令中的多个更新查询发送到数据库(对您来说是透明的)。

Session session = SessionFactory.openSession();
Transaction tx = session.beginTransaction();
for ( int i=0; i<100000; i++ ) {
    Employee employee = new Employee(.....);
    session.save(employee);
}
tx.commit();
session.close();

参考文献:http://www.tutorialspoint.com/hibernate/hibernate_batch_processing.htm

另一答案

解决方案(或至少是一种大大降低CPU使用率的方法)是从合并切换到持久化。我在帖子中提到过,我曾尝试过转换为持续存在而没有明显区别。

我随后找到了一种更好的方法来分析重载,并且能够显示出改进。对于我正在运行的特定负载,从持久性切换到合并,将平均CPU百分比从16降低到5。

我们不需要合并。对于永久性修复,我们需要稍微重新编写代码,以便能够使用相同的EntityManager来加载根对象,然后将其持久化(然后将级联完整的“树”结构)。这样,我们的对象不会分离,我们不需要使用合并。

感谢ddalton指向那个方向。

以上是关于使用Hibernate进行数千次插入时的CPU利用率很高的主要内容,如果未能解决你的问题,请参考以下文章

HIbernate 批量插入或更新在 Spring Boot 中不起作用

基于网络利用率或每秒请求数的 Kubernetes 扩展

使用 JPA + Hibernate 进行大规模插入

使用 Spring JPA / Hibernate 进行条件插入

使用 CPU 时的 OpenCL 段错误

使用 Hibernate 和 Spring 进行批量插入