【问题标题】:Spring JPA bulk upserts is slow (1,000 entities took 20 seconds)Spring JPA 批量更新很慢(1,000 个实体需要 20 秒)
【发布时间】:2021-01-04 05:13:17
【问题描述】:

当我尝试更新测试数据(1,000 个实体)时,花了 1m 5s。

所以我阅读了很多文章,然后将处理时间缩短到 20 秒

但这对我来说仍然很慢,我相信有比我使用的方法更多的好解决方案。有没有人有很好的做法来处理这个问题?

我也想知道是哪一部分让它变慢了?

  1. 持久性上下文
  2. 其他选择

谢谢!


@Entity 类

该实体类是从用户手机中收集到用户步行步数的健康数据。

PK是userIdrecorded_at(PK的recorded_at来自请求数据)

@Getter
@NoArgsConstructor
@IdClass(StepId.class)
@Entity
public class StepRecord {
    @Id
    @ManyToOne(targetEntity = User.class, fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", referencedColumnName = "id", insertable = false, updatable = false)
    private User user;

    @Id
    private ZonedDateTime recordedAt;

    @Column
    private Long count;

    @Builder
    public StepRecord(User user, ZonedDateTime recordedAt, Long count) {
        this.user = user;
        this.recordedAt = recordedAt;
        this.count = count;
    }
}

Id 类

Id class(here) 中的用户字段,它是 UUID 类型In Entity class,用户是用户实体类型。没问题,会不会有问题?

@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class StepId implements Serializable {
    @Type(type = "uuid-char")
    private UUID user;
    private ZonedDateTime recordedAt;
}

请求数据示例

// I'll get user_id from logined user
// user_id(UUID) like 'a167d363-bfa4-48ae-8d7b-2f6fc84337f0'

[{
    "count": 356,
    "recorded_at": "2020-09-16T04:02:34.822Z"
},
{
    "count": 3912,
    "recorded_at": "2020-09-16T08:02:34.822Z"
},
{
    "count": 8912,
    "recorded_at": "2020-09-16T11:02:34.822Z"
},
{
    "count": 9004,
    "recorded_at": "2020-09-16T11:02:34.822Z" // <-- if duplicated, update
}
]

数据库数据示例


|user_id (same user here)            |recorded_at        |count|
|------------------------------------|-------------------|-----|
|a167d363-bfa4-48ae-8d7b-2f6fc84337f0|2020-09-16 04:02:34|356  | <-insert
|a167d363-bfa4-48ae-8d7b-2f6fc84337f0|2020-09-16 08:21:34|3912 | <-insert
|a167d363-bfa4-48ae-8d7b-2f6fc84337f0|2020-09-16 11:02:34|9004 | <-update


解决方案 1:SaveAll() 与批处理

  1. application.properties
spring:
  jpa:
    properties:
      hibernate:
        jdbc.batch_size: 20
        jdbc.batch_versioned_data: true
        order_inserts: true
        order_updates: true
        generate_statistics: true
  1. 服务
public void saveBatch(User user, List<StepRecordDto.SaveRequest> requestList) {
        List<StepRecord> chunk = new ArrayList<>();

        for (int i = 0; i < requestList.size(); i++) {
            chunk.add(requestList.get(i).toEntity(user));

            if ( ((i + 1) % BATCH_SIZE) == 0 && i > 0) {
                repository.saveAll(chunk);
                chunk.clear();
                //entityManager.flush(); // doesn't help
                //entityManager.clear(); // doesn't help 
            }
        }

        if (chunk.size() > 0) {
            repository.saveAll(chunk);
            chunk.clear();
        }
    }

我读过文章说如果我在 Entity 类中添加“@Version”字段,但它仍然是额外的选择。并且花费了几乎相同的时间(20 秒)。

链接在这里⇒https://persistencelayer.wixsite.com/springboot-hibernate/post/the-best-way-to-batch-inserts-via-saveall-iterable-s-entities

但这对我没有帮助。我想我将 PK 密钥与数据一起传递,所以它总是调用 merge()。

(如果我对@Version 有误解,请告诉我)


解决方案2:Mysql Native Query (insert into~ on duplicate key update~)

我猜Insert into ~ on duplicate key update ~在mysql原生查询中可能比merge() &lt;- select/insert

mysql原生查询也可以选择检查重复键,但我猜mysql引擎优化得很好。

  1. 存储库
public interface StepRecordRepository extends JpaRepository<StepRecord, Long> {
    @Query(value = "insert into step_record(user_id, recorded_at, count) values (:user_id, :recorded_at, :count) on duplicate key update count = :count", nativeQuery = true)
    void upsertNative(@Param("user_id") String userId, @Param("recorded_at") ZonedDateTime recorded_at, @Param("count") Long count);
}
  1. 服务
public void saveNative(User user, List<StepRecordDto.SaveRequest> requestList) {
        requestList.forEach(x ->
                repository.upsertNative(user.getId().toString(), x.getRecordedAt(), x.getCount()));
    }

对于 1000 个实体,这两种方法都需要 20 秒。

【问题讨论】:

  • 您是否尝试为您的实体覆盖 equals() 和 hashCode()?
  • 也许这个链接对你有用:stackoverflow.com/questions/14936266/…
  • @Zogger No. 为什么需要equals()和hashCode()?
  • 我读了你的链接。我已经尝试过entityManager.flush()entityManager.clear(),但它没有帮助:(。仍然是 20 秒
  • 数据库和环境在同一个网络吧?

标签: spring jpa batch-processing bulkinsert


【解决方案1】:

我自己回答了,但我还在等待你的意见。

是时候更新插入以使用本机查询了

  • 1,000 个实体 => 0.8 秒
  • 10,000 个实体 => 2.5 ~ 4.2 秒

这比问题中的上述两种方法要快。这是因为数据直接存储在 DB 中,无需经过持久化上下文。

专业人士

  • 不要额外选择
  • 不需要考虑持久性上下文

缺点

  • 不可读?
  • 太原始了?

如何

服务

@RequiredArgsConstructor
@Service
public class StepRecordService {
    private final StepRecordRepository repository;

    @Transactional
    public void save(User user, List<StepRecordDto.SaveRequest> requestList) {
        int chunkSize = 100;
        Iterator<List<StepRecordDto.SaveRequest>> chunkList = StreamUtils.chunk(requestList.stream(), chunkSize);
        chunkList.forEachRemaining(x-> repository.upsert(user, x));
    }
}

StreamUtils 中的块函数

public class StreamUtils {
    public static <T> Iterator<List<T>> chunk(Stream<T> iterable, int chunkSize) {
        AtomicInteger counter = new AtomicInteger();
        return iterable.collect(Collectors.groupingBy(x -> counter.getAndIncrement() / chunkSize))
                .values()
                .iterator();
    }
}

存储库

@RequiredArgsConstructor
public class StepRecordRepositoryImpl implements StepRecordRepositoryCustom {
    private final EntityManager entityManager;

      @Override
    public void upsert(User user, List<StepRecordDto.SaveRequest> requestList) {
        String insertSql = "INSERT INTO step_record(user_id, recorded_at, count) VALUES ";
        String onDupSql = "ON DUPLICATE KEY UPDATE count = VALUES(count)";
        StringBuilder paramBuilder = new StringBuilder();

          for ( int i = 0; i < current.size(); i ++ ) {
              if (paramBuilder.length() > 0)
                  paramBuilder.append(",");

              paramBuilder.append("(");
              paramBuilder.append(StringUtils.quote(user.getId().toString()));
              paramBuilder.append(",");
              paramBuilder.append(StringUtils.quote(requestList.get(i).getRecordedAt().toLocalDateTime().toString()));
              paramBuilder.append(",");
              paramBuilder.append(requestList.get(i).getCount());
              paramBuilder.append(")");
          }

          Query query = entityManager.createNativeQuery(insertSql + paramBuilder + onDupSql);
          query.executeUpdate();
    }
}

【讨论】:

    猜你喜欢
    • 2018-01-23
    • 2021-08-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-11
    • 2019-02-09
    • 1970-01-01
    • 2018-07-30
    相关资源
    最近更新 更多