Spring Boot JPARepository在save()方法上的性能表现

3

我有一个问题,就是我的Spring Boot应用程序在插入数据时性能非常慢。

我正在从一个数据库中提取大量的数据子集,并将这些数据插入到另一个数据库中。

以下是我的实体类:

@Entity
@Table(name = "element")
public class VXMLElementHistorical {

@Id
@Column(name = "elementid")   
private long elementid;

@Column(name = "elementname")
private String elementname; 

Getter/Setter methods...    

我已经配置了一个JPA仓库

public interface ElementRepository extends JpaRepository<Element, Long> {

}

并调用save()方法保存我的对象。
@Transactional 
public void processData(List<sElement> hostElements) 
throws DataAccessException { 

List<Element> elements = new ArrayList<Element>();    

for (int i = 0; i < hostElements.size(); i++) {
        Element element = new Element();
        element.setElementid(hostElements.get(i).getElementid());
        element.setElementname(hostElements.get(i).getElementname());
        elements.add(element);
    }

   try{
   elementRepository.save(elements);{
   //catch etc...

}

每个项目执行插入操作需要6到12秒钟。我已经开启了Hibernate跟踪日志和统计信息,当我调用保存函数时,Hibernate会执行两个查询,一个选择和一个插入。选择查询占用了总时间的99%。我在数据库中直接运行了选择查询,结果以纳秒为单位返回。这使我相信它不是索引问题,但我不是DBA。
我在我的开发环境中创建了一个负载测试,并且使用类似的负载大小,整个过程的时间远远不及我的生产环境。有什么建议吗?

我们在项目中使用了Spring Data JPA,但插入元素从来没有花费这么长时间。根本原因可能与Spring Data JPA不同。 - Prashant
首先,你不应该进行转换,而是将它放入列表中,并保存所有元素。这样你就有效地复制了所有元素(这将增加内存)。此外,你在一个大型事务中完成了所有操作,这也会导致问题。相反,直接保存创建的元素,并且每X个元素(比如50)执行一次 flushclear。最好还要将刷新模式设置为手动(以防止脏检查和在之间刷新)。 - M. Deinum
3个回答

1

不要创建元素列表并保存,而是保存单个元素。时不时地进行flushclear操作,以防止脏检查成为瓶颈。

@PersistenceContext
private EntityManager entityManager;

@Transactional 
public void processData(List<sElement> hostElements) 
throws DataAccessException {     

for (int i = 0; i < hostElements.size(); i++) {
        Element element = new Element();
        element.setElementid(hostElements.get(i).getElementid());
        element.setElementname(hostElements.get(i).getElementname());
        elementRepository.save(element)
        if ( (i % 50) == 0) {
            entityManager.flush();
            entityManager.clear();
        }
}
entityManager.flush(); // flush the last records.

您希望每隔 x 个元素(这里是50,但您可能希望找到最佳数字)进行清除和刷新。

现在,由于您正在使用Spring Boot,您可能还希望添加一些其他属性。例如配置批处理大小。

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

如果您的JDBC驱动程序支持,这将把50个单独的插入语句转换为1个大批量插入。即50次插入变成1次插入。
也请参阅https://vladmihalcea.com/how-to-batch-insert-and-update-statements-with-hibernate/

0

正如 @M. Deinum 在评论中所说,您可以通过在插入一定数量后调用 flush()clear() 来进行优化,代码如下。

int i = 0;
for(Element element: elements) {
    dao.save(element);
    if(++i % 20 == 0) {
        dao.flushAndClear();
    }

}

由于大部分时间用于加载数据,清除缓存可能会降低性能。 - Jens Schauder

0

由于加载实体似乎是瓶颈,而您只想进行插入操作,即您知道实体不存在于数据库中,因此您可能不应使用Spring Data JPA的标准save方法。

原因是它执行了一个merge,触发Hibernate加载可能已经存在于数据库中的实体。

相反,向您的存储库添加一个自定义方法,该方法在实体管理器上执行persist。由于您提前设置了Id,请确保具有版本属性,以便Hibernate可以确定这确实是一个新实体。

这应该使选择消失。

其他答案中给出的其他建议值得考虑作为第二步:

  • 启用批处理。
  • 尝试中间刷新和清除会话。
  • 一次只保存一个实例,而不是将它们收集到集合中,因为调用mergepersist并不会实际触发写入数据库,只有刷新才会(这是一个简化,但对于此上下文来说足够了)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接