繁体   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