[英]Optimizing data fetching and insertion in Spring Boot
我在 CSV 文件中有 270000 条记录,其中包含 user_id、book_ISBN 和 book_rating 列,我需要将记录插入到多对多表中。 我用 openCSV 库解析了数据,结果是一个列表。
public List<UserRatingDto> uploadRatings(MultipartFile file) throws IOException{
BufferedReader fileReader = new BufferedReader(new
InputStreamReader(file.getInputStream(), "UTF-8"));
List<UserRatingDto> ratings = new CsvToBeanBuilder<UserRatingDto>(fileReader)
.withType(UserRatingDto.class)
.withSeparator(';')
.withIgnoreEmptyLine(true)
.withSkipLines(1)
.build()
.parse();
return ratings;
}
这没有性能问题,解析大约需要 1 分钟。 但是,为了将这些插入到表中,我需要从数据库中获取书籍和用户以形成关系,我尝试使用 @Async 注释使方法异步,我尝试并行流,我尝试放置对象进入堆栈并使用 saveAll() 批量插入,但仍然需要太多时间。
public void saveRatings(final MultipartFile file) throws IOException{
List<UserRatingDto> userRatingDtos = uploadRatings(file);
userRatingDtos.parallelStream().forEach(bookRating->{
UserEntity user = userRepository.findByUserId(bookRating.getUserId());
bookRepository.findByISBN(bookRating.getBookISBN()).ifPresent(book -> {
BookRating bookRating1 = new BookRating();
bookRating1.setRating(bookRating.getBookRating());
bookRating1.setUser(user);
bookRating1.setBook(book);
book.getRatings().add(bookRating1);
user.getRatings().add(bookRating1);
bookRatingRepository.save(bookRating1);
});
});
}
这就是我现在所拥有的,有什么我可以改变的来让它更快吗?
问题是数据正在被一一获取和持久化。 访问数据的最高效方式通常是well defined batches
,然后遵循以下模式:
对于您的特定用例,您可以执行以下操作:
public void saveRatings(final MultipartFile file) throws IOException {
List<UserRatingDto> userRatingDtos = uploadRatings(file);
// Split the list into batches
getBatches(userRatingDtos, 100).forEach(this::processBatch);
}
private void processBatch(List<UserRatingDto> userRatingBatch) {
// Retrieve all data required to process a batch
Map<String, UserEntity> users = userRepository
.findAllById(userRatingBatch.stream().map(UserRatingDto::getUserId).toList())
.stream()
.collect(toMap(UserEntity::getId, user -> user));
Map<String, Book> books = bookRepository.findAllByIsbn(userRatingBatch.stream().map(UserRatingDto::getBookISBN).toList())
.stream()
.collect(toMap(Book::getIsbn, book -> book));
// Process each rating in memory
List<BookRating> ratingsToSave = userRatingBatch.stream().map(bookRatingDto -> {
Book book = books.get(bookRatingDto.getBookISBN());
if (book == null) {
return null;
}
UserEntity user = users.get(bookRatingDto.getUserId());
BookRating bookRating = new BookRating();
bookRating.setRating(bookRatingDto.getBookRating());
bookRating.setUser(user);
bookRating.setBook(book);
book.getRatings().add(bookRating);
user.getRatings().add(bookRating);
return bookRating;
}).filter(Objects::nonNull).toList();
// Save data in batches
bookRatingRepository.saveAll(ratingsToSave);
bookRepository.saveAll(books.values());
userRepository.saveAll(users.values());
}
public <T> List<List<T>> getBatches(List<T> collection, int batchSize) {
List<List<T>> batches = new ArrayList<>();
for (int i = 0; i < collection.size(); i += batchSize) {
batches.add(collection.subList(i, Math.min(i + batchSize, collection.size())));
}
return batches;
}
请注意,所有 I/O 应始终分批完成。 如果您有单个数据库查找或保存在内部处理循环中,这根本不起作用。
您可以尝试不同的batch sizes
,看看什么会带来更好的性能 - 批次越大,事务将保持打开状态的时间越长,而且并非总是更大的批次会带来更好的整体性能。
此外,请确保您优雅地处理错误 - 例如:
编辑:根据 OP 的评论,这提高了 10 倍以上的性能。 此外,如果排序不重要,通过并行处理每个批次仍然可以大大提高性能。
编辑 2:作为一般模式,理想情况下,我们不会将所有记录都保存在内存中,而是检索要分批处理的数据。 这将进一步提高性能并避免 OOM 错误。
此外,这可以在许多并发模式中完成,例如有专用线程来获取数据,工作线程来处理它,以及另一组线程来持久化结果。
最简单的模式是让每个工作单元独立——他们被赋予了他们应该处理的内容(例如一组从数据库中获取的 id),然后检索必要的数据进行处理,在内存中处理它,并保存结果。
为什么不只使用这样的临时登台表(可能使用NOLOGGING
和其他优化,如果可用):
CREATE TEMPORARY TABLE load_book_rating (
user_id BIGINT,
book_isbn TEXT,
rating TEXT
);
然后将 CSV 数据批量加载到该临时表中,然后批量插入真实表中的所有数据,如下所示:
INSERT INTO book_rating (user_id, book_id, book_rating)
SELECT l.user_id, b.id, l.book_rating
FROM load_book_rating AS l
JOIN book AS b ON l.book_isbn = b.isbn
我可能忽略了您的架构中的一些细节,但我的主要观点是,您可能只是因为您没有将ISBN
自然键用作BOOK
表的主键,所以您正在做所有这些箍,所以您有执行查找?
或者,使用 RDBMS 的本机 CSV 导入功能。 他们中的大多数都可以做到,例如PostgreSQL 的COPY
命令
我很确定纯基于 SQL 的方法将胜过您可能在 Java 中实现的任何其他方法。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.