如何在 Spring Boot 中以内存高效的方式迭代 MySQL 中的大量记录

Posted

技术标签:

【中文标题】如何在 Spring Boot 中以内存高效的方式迭代 MySQL 中的大量记录【英文标题】:How to iterate over large number of records in MySQL with memory efficient manner in Spring Boot 【发布时间】:2019-12-20 01:35:06 【问题描述】:

我想使用 findAll 从表中获取所有记录并对每条记录进行一些处理,但我不确定如果记录数量巨大(如数百万)是否会出现内存问题。

我研究了Pageable,但我不确定如何使用Pageable 方法迭代所有数据。是否甚至可以一次获取少量记录并处理它们并再次获取它们,直到处理完所有记录?

什么会更好?使用findAll()方法或Pageable方法获取Iterable中的所有记录?

【问题讨论】:

您打算更新记录还是只读取数据? 您似乎使用了错误的工具。一旦你需要扩大规模,问题空间变成了大数据问题——你应该考虑使用大数据工具,即 spark、kafka、... @MaciejKowalski 我打算只读取数据并对其进行一些处理。 【参考方案1】:

如果它可以是数百万..

1) 不要使用findAll() 并检索实际托管实体的列表。如果您只需要读取数据,请使用投影查询以及 Spring Data JPA 投影接口。这将绕过持久化上下文并节省大量时间和内存。

2) 使用分页(节省内存)并确保在新事务中进行每个调用 (@Transactional(propagation = REQUIRES_NEW))。这将允许其他事务不会永远挂起,如果您不使用分页并且只触发了一个,可能会出现这种情况,请给我全部,查询。

3) 它看起来也适合过夜批处理作业。考虑一下。

【讨论】:

【参考方案2】:

如果实体很多,不要使用 findAll。

如果你想使用分页,你可以这样做:

    Pageable pageRequest = PageRequest.of(0, 200);
    Page<Qmail> onePage = repository.findAll(pageRequest);

    while (!onePage.isEmpty()) 
        pageRequest = pageRequest.next();

        //DO SOMETHING WITH ENTITIES
        onePage.forEach(entity -> System.out.println(entity.getId()));

        onePage = repository.findAll(pageRequest);
    

【讨论】:

你不认为 onePage = repository.findAll(pageRequest); 这会再次获取相同的结果吗?由于 pageRequest 仍然有页码 0 和大小 200? 不,因为有一个pageRequest.next() 将移至下一页 pageRequest.next() 将返回 Pageable 而不是 PageRequest。所以这种方法现在会失败。但是,您可以将最后一行更改为 onePage = repository.findAll(pageRequest.nextPageable());。它应该可以工作。 使用 while (!onePage.isLast()) 而不是 while (!onePage.isEmpty()),这样会节省最后一个额外的循环【参考方案3】:

自 Spring Data 1.8 起,您可以Stream 超过结果。

Stream<Record> findAll();

重要的是在这里添加一个QueryHint 关于数据库的获取大小。如果设置它在内部使用页面流过结果。

将此用于 mysql 数据库:

@QueryHints(value = @QueryHint(name = org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE, value = "-2147483648"))
Stream<Record> findAll();

对于非 MySQL 数据库,您可以使用 fetch size:

@QueryHints(value = @QueryHint(name = org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE, value = "5000"))
Stream<Record> findAll();

而且,如果您不更新/删除记录,请不要忘记将您的事务设置为只读:

@Transactional(readOnly = true)

【讨论】:

【参考方案4】:

您需要的是批量读取数据并处理每个数据,并且可能会在其他地方保持不变或从中生成报告。

这是 ETL 用例。

这个案例可以用Spring Batch,可以很好的处理。

Reader 一次读取一个数据并在处理器中处理它。 Writer 将根据您设置的块/批量大小持久化或生成报告。

这样您就不会在内存中保存大量数据。

【讨论】:

以上是关于如何在 Spring Boot 中以内存高效的方式迭代 MySQL 中的大量记录的主要内容,如果未能解决你的问题,请参考以下文章

在 Spring Boot 中以编程方式加密/解密数据库密码

在 Spring Boot Transaction 中以只读方式执行 RxJava observable?

在 Spring Boot 应用程序中以编程方式重新启动 HikariPool?

如何在 Spring Boot Rest API 中以 XML 形式返回对象列表

Spring Boot 工程中Bean对象的核心特性

Spring Boot 程序化日志配置