【问题标题】:H2 Insert performancesH2 刀片性能
【发布时间】:2020-09-22 08:08:48
【问题描述】:

我正在使用 java spring boot 开发一个 Web 应用程序,并且我使用 H2 作为数据库。

我在插入数据时遇到了一些性能问题。我正确地进行了批量插入,但我注意到一段时间后插入速度变慢了很多。例如,插入前 N 个元素需要 100 秒,但随后需要 200 秒插入以下 N 个元素,然后 400 秒用于下一个 N 个元素,依此类推。

我正在努力寻找问题并解决它。有人可以帮忙吗?

为了进行批处理,我设置了应用程序属性:

spring.jpa.properties.hibernate.jdbc.batch_size=20

我正在插入这个实体:

@Getter
@Setter
@Entity
@Table(name = "entity_son")
public class EntitySon extends EntityFather{

    protected EntitySon (){}

   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name ="anotherEntityId")
   private AnotherEntity AnotherEntityId;
}

继承自该实体:

@Getter
@Setter
@MappedSuperclass
public abstract class EntityFather{

    @Id
    @SequenceGenerator(name = "SEQ", initialValue = 1, allocationSize = 20, sequenceName = "EntitySequence")
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ")
    @Column(name ="entityId")
    private Long entityFatherId;

}

我正在使用 liquibase 以这种方式生成序列:

databaseChangeLog:
  - changeSet:
      id: createSequence
      author: liquibase-docs
      changes:
        - createSequence:
            sequenceName: EntitySequence
            incrementBy: 20

最后我以这种方式进行批量插入:

private void saveEntitySon(List<EntitySon> entitySons){
    long BATCH_SIZE = 20L;
    long batchIter = 0;
    while(true) {
        List<EntitySon> batch = entitySons.stream().skip(batchIter*BATCH_SIZE)
                .limit(BATCH_SIZE*(batchIter+1)).collect(Collectors.toList());
        if (batch.size() < BATCH_SIZE) {
            logger.info("Saving line difference by line difference");
            for (EntitySon entitySon : batch) {
                entitySonRepository.save(entitySon)
            }
            return;
        }else{
                entitySonRepository.saveAll(batch)
        }
        batchIter++;
    }
}

我还必须提到,如果我删除并重新创建数据库,我会在性能中看到相同的模式。

【问题讨论】:

  • 也许你可以给我们看一些代码,否则将无法帮助你。从哪里开始?
  • 好的,我会提供一些代码。我仍然很想知道你对这个问题的看法。
  • 好吧,你的代码每次都在处理更多的实体——如果你删除数据库插入,它仍然表现出相同的行为吗?如果少于批次,也不确定为什么要插入单个项目。最后,它应该自动批处理——我不知道你为什么每次都手动创建一个列表;只需每batch 个项目刷新一次会话。
  • 另外,儿子扩展父亲只是……很奇怪。
  • 对不起,代码有点匿名,所以有些事情没有真正意义,但这里的关键是性能。我目前专注于插入性能,到目前为止读取不是问题

标签: spring-boot hibernate spring-data-jpa h2


【解决方案1】:

好的,所以您在这里混合了两件事:JDBC 批处理大小JPA 会话。

JDBC Batch 大小将使底层 JDBC 数据库驱动程序将多个插入批处理在一起,因此您可以节省数据库往返。 JPA 建立在 JDBC 之上,并在“会话”或 EntityManager 中管理状态。我总是喜欢将持久化实体称为“托管”实体,以明确在当前会话中存在和管理每个实体的状态。

警告:我不确定流操作的内存效率如何,也没有查找。

您应该使用分析器,例如 VisualVM(它是 JDK 的一部分),或者甚至使用 Windows 任务管理器,您应该能够看到您的内存消耗在增长。

您在一个事务中,您保留 100 个实体,因此您的会话包含 100 个状态,然后您添加的状态越来越多。这会减慢状态和垃圾收集的迭代速度。

您想要也可以在 JPA 级别上进行批处理。不幸的是,在这方面缺少 Spring JPA 存储库。如果您查看代码,saveAll 的作用与您的循环完全相同(遍历列表并调用 save())。 您需要一个实际的 EntityManager 以便您可以将语句刷新到数据库,然后清除会话以删除所有状态:

private void saveEntitySon(List<EntitySon> entitySons){
    long BATCH_SIZE = 20L;

    for (long i = 0L; i < entitySons.size(); i++) {

        if (i > 0 && i % BATCH_SIZE == 0) {
            entitySonRepository.flush(); // Could also use EntityManager, doesn't matter
            entityManager.clear(); // This will also detach all entities! So make sure you need to reload them if you want to use them!
        }

        EntitySon entitySon = entitySons.get(i);
        // Using repo so Spring Data JPA events get triggered
        entitySonRepository.save(entitySon);
    }
    
    // Flush out remainder
    entitySonRepository.flush();
    entityManager.clear();
} 

正如评论所说,请注意clear() 将分离所有实体。如果您在批处理之前加载/保留一个实体并希望在之后使用它,那将是一个问题。但通常这样的批处理作业无论如何都会自行运行。

编辑: 我假设你想在一个事务中做所有事情,所以你会坚持“全有或全无”。但当然,数据库端也存在状态管理开销,长时间运行的事务可能会导致冲突。

总是在处理 JPA 时查看 Vlad Mihalcea 的博客是个好主意,他有一篇关于批处理的文章 here

Hibernate User Guide 也有一个关于批处理的章节。

请注意,Spring Data JPA 对于许多日常事务来说是一个不错的、舒适的抽象,但它不允许精确控制,并且对于许多性能关键或更复杂的任务来说是不够的。

【讨论】:

  • 感谢您的详细回答,我将一一尝试这些步骤并运行我的基准测试,然后告诉您会发生什么:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-18
  • 1970-01-01
  • 1970-01-01
  • 2019-01-31
相关资源
最近更新 更多