问题描述
我正在使用Java Spring Boot开发一个Web应用程序,并且正在将H2用作数据库。
插入数据时出现一些性能问题。我正确地进行了批量插入,但是我注意到的是一段时间后插入速度变慢了很多。例如,插入第一个 N 个元素需要 100 秒,但是随后插入 N 元素,然后接下来的 N 个元素 400 秒,依此类推。
我正在努力寻找并解决问题。有人可以帮忙吗?
为了进行批处理,我设置了应用程序属性:
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++;
}
}
我还必须提到,如果我删除并重新创建数据库,我会在表演中看到相同的模式。
解决方法
好的,所以您在这里混合了两件事: JDBC批处理大小和 JPA 会话。
JDBC批处理大小将使基础JDBC数据库驱动程序将多个插入批处理在一起,因此可以节省数据库往返。 JPA构建在JDBC之上,并管理“会话”或EntityManager中的状态。我总是喜欢将持久性实体称为“托管”实体,以明确表示在当前会话中有某些东西可以保存和管理每个实体的状态。
注意事项::我不确定流操作的内存效率如何,我没有查找。
您应该使用诸如VisualVM(它是JDK的一部分)之类的探查器,甚至使用Windows TaskManager,您也应该能够看到内存消耗量的增长。
您处于一项事务中,您保留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是许多日常事物的不错的,舒适的抽象,但是它不允许精确的控制,并且对于许多对性能至关重要的任务或更复杂的任务还不够。