Spring JPA 批量插入与更新优化

10,039

Spring JPA 中批量插入和更新是使用 SimpleJpaRepository#saveAll,saveAll 会循环调用 save 方法,save 方法会根据实体 id 查找记录,记录存在则更新,不存在则插入。n 个实体需要执行 2n 条语句,因而效率较低。

@Transactional
@Override
public <S extends T> S save(S entity) {

   if (entityInformation.isNew(entity)) {
      em.persist(entity);
      return entity;
   } else {
      return em.merge(entity);
   }
}

注:id 为基本类型且为 null 时,会直接插入记录,只执行 n 条语句。

此文中,将在主键自增的前提下,以借助 druid 监控,探讨 Spring JPA 批量插入与更新优化思路。

批量插入

使用 SimpleJpaRepository#saveAll,插入 5k 条记录。

image.png 总共执行事务 5000*2 次,用时 543 s。

Hibernate 本身支持批量执行,通过 spring.jpa.properties.hibernate.jdbc.batch_size 指定批处理的容量。 image.png 共执行事务 5000+5 次,用时 439 s。

利用 EntityManager 批量插入 5k 条记录。

private <S extends T> void batchExecute(Iterable<S> s, Consumer<S> consumer) {
    Session unwrap = entityManager.unwrap(Session.class);
    try {
        unwrap.getTransaction().begin();
        Iterator<S> iterator = s.iterator();
        int index = 0;
        while (iterator.hasNext()) {
            consumer.accept(iterator.next());
            index++;
            if (index % BATCH_SIZE == 0) {
                entityManager.flush();
                entityManager.clear();
            }
        }
        if (index % BATCH_SIZE != 0) {
            entityManager.flush();
            entityManager.clear();
        }
        unwrap.getTransaction().commit();
    } catch (Exception e) {
        unwrap.getTransaction().rollback();
    }
}

image.png 总共执行事务 5 次,用时 255 s,和 SimpleJpaRepository#saveAll 相比,节省了查询的性能消耗。

通过拼接 SQL 语句的方式,使用一条语句插入多条记录。

public void insertUsingConcat() {
    StringBuilder sb = new StringBuilder("insert into t_comment(id, content, name) values ");
    List<CommentPO> l = new ArrayList<>();
    for (int i = 10000; i < 15000; i++) {
        sb.append("(")
                .append(i)
                .append(",'content of demo batch#")
                .append(i)
                .append("','name of demo batch#")
                .append(i)
                .append("'),");
    }
    sb.deleteCharAt(sb.length() - 1);

    executeQuery(sb);
}

final EntityManager entityManager = ApplicationContextHolder.getApplicationContext()
        .getBean("entityManagerSecondary", EntityManager.class);

@Transactional
public void executeQuery(StringBuilder sb) {
    Session unwrap = entityManager.unwrap(Session.class);
    unwrap.setJdbcBatchSize(1000);
    try {
        unwrap.getTransaction().begin();
        Query query = entityManager.createNativeQuery(sb.toString());
        query.executeUpdate();
        unwrap.getTransaction().commit();
    } catch (Exception e) {
        e.printStackTrace();
        unwrap.getTransaction().rollback();
    }
}

image.png 执行一次事务,用时 0.2 s。

拼接语句需要注意 sql 语句长度限制,可以通过 show VARIABLES WHERE Variable_name LIKE 'max_allowed_packet'; 查询,这是 Server 一次接受的数据包大小,通过 my.ini 配置。

批量更新

批量更新和批量插入类似,也是四种写法,结论也一致。区别仅在于 sql 写法:

@PutMapping("/concatUpdateClause")
public void updateUsingConcat() {
    List<CommentPO> l = getDemoBatches(5000, 10000, "new");
    StringBuilder sb = new StringBuilder("update t_comment set content = case");
    for (CommentPO commentPO : l) {
        sb.append(" when id = ").append(commentPO.getId()).append(" then '").append(commentPO.getContent())
                .append("'");
    }
    sb.append(" else content end")
            .append(" where id in (")
            .append(l.stream().map(i -> String.valueOf(i.getId())).collect(Collectors.joining(",")))
            .append(")");
    executeQuery(sb);
}