[英]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.