簡體   English   中英

在 Spring Boot 中優化數據獲取和插入

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

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM