【问题标题】:Using Hibernate's ScrollableResults to slowly read 90 million records使用 Hibernate 的 ScrollableResults 慢慢读取 9000 万条记录
【发布时间】:2011-02-19 01:23:55
【问题描述】:

我只需要使用 Hibernate 读取 MySQL 数据库中表中的每一行并基于它编写一个文件。但是有 9000 万行,而且非常大。因此,以下内容似乎是合适的:

ScrollableResults results = session.createQuery("SELECT person FROM Person person")
            .setReadOnly(true).setCacheable(false).scroll(ScrollMode.FORWARD_ONLY);
while (results.next())
    storeInFile(results.get()[0]);

问题是上面将尝试将所有 9000 万行加载到 RAM 中,然后再进入 while 循环......这会用 OutOfMemoryError: Java heap space exceptions 杀死我的内存:(。

所以我猜 ScrollableResults 不是我想要的?处理这个问题的正确方法是什么?我不介意这个 while 循环是否需要几天时间(我不希望这样)。

我想处理这个问题的唯一其他方法是使用 setFirstResult 和 setMaxResults 来遍历结果,并且只使用常规的 Hibernate 结果而不是 ScrollableResults。不过,这感觉效率很低,当我在第 89 百万行调用 setFirstResult 时,它会开始花费相当长的时间......

更新:setFirstResult/setMaxResults 不起作用,结果需要很长时间才能达到我担心的偏移量。这里一定有解决办法!这不是一个非常标准的程序吗?我愿意放弃 Hibernate 并使用 JDBC 或任何它需要的东西。

更新 2:我想出的解决方案可以正常工作,但不是很好,基本上是以下形式:

select * from person where id > <offset> and <other_conditions> limit 1

由于我有其他条件,即使所有条件都在索引中,它仍然没有我希望的那么快......所以仍然欢迎其他建议..

【问题讨论】:

标签: java mysql hibernate large-data-volumes scrollableresults


【解决方案1】:

使用 setFirstResult 和 setMaxResults 是我知道的唯一选择。

传统上,可滚动结果集只会根据需要将行传输到客户端。不幸的是,MySQL Connector/J 实际上是伪造的,它执行整个查询并将其传输到客户端,因此驱动程序实际上将整个结果集加载到 RAM 中,并将滴灌给您(您的内存不足问题证明了这一点) .你的想法是对的,这只是 MySQL java 驱动程序的缺点。

我发现没有办法解决这个问题,所以使用常规的 setFirst/max 方法加载大块。很抱歉带来坏消息。

只要确保使用无状态会话,就没有会话级缓存或脏跟踪等。

编辑:

除非您突破 MySQL J/Connector,否则您的 UPDATE 2 是您将获得的最好的。虽然没有理由不能提高查询的限制。如果您有足够的 RAM 来保存索引,这应该是一个便宜的操作。我会稍微修改一下,一次抓取一批,然后使用该批次的最高 id 来抓取下一批。

注意:这仅在 other_conditions 使用相等(不允许范围条件)并将索引的最后一列作为 id 时才有效。

select * 
from person 
where id > <max_id_of_last_batch> and <other_conditions> 
order by id asc  
limit <batch_size>

【讨论】:

  • 使用 StatelessSession 是一个特别好的技巧!
  • setFirstResult 和 setMaxResults 不是一个可行的选择。我的猜测是正确的,它会非常缓慢。也许这适用于小桌子,但很快它就会花费太长时间。您可以通过简单地运行“select * from any limit 1 offset 3000000”在 MySQL 控制台中对此进行测试。这可能需要 30 分钟...
  • 运行“select * from geoplanet_locations limit 1 offset 1900000;”针对 YAHOO Geoplanet 数据集(5 百万行),在 1.34 秒内返回。如果您有足够的 RAM 将索引保存在 RAM 中,那么我认为您的 30 分钟数字已经过时了。有趣的是“从 id > 56047142 限制 10 的 geoplanet_locations 中选择 *;”基本上立即返回(普通客户端只返回 0.00)。
  • @Michael 您如何发现 MySQL 连接器伪造了滚动?是不是写在什么地方?我很感兴趣,因为我喜欢在 NHibernate 中使用滚动功能,并且我正在使用 .NET 的 mysql 连接器,我想检查 Mysql .Net 连接器是否也伪造它,或者取决于版本?
  • 有人知道 MySQL 连接器伪造滚动是否仍然存在?
【解决方案2】:

您应该能够使用ScrollableResults,尽管它需要一些魔法咒语才能使用 MySQL。我在一篇博文 (http://www.numerati.com/2012/06/26/reading-large-result-sets-with-hibernate-and-mysql/) 中写下了我的发现,但我会在这里总结一下:

“[JDBC] 文档说:

To enable this functionality, create a Statement instance in the following manner:
stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY,
                java.sql.ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE);

这可以使用 Hibernate API 3.2+ 版本中的 Query 接口(这也应该适用于 Criteria)来完成:

Query query = session.createQuery(query);
query.setReadOnly(true);
// MIN_VALUE gives hint to JDBC driver to stream results
query.setFetchSize(Integer.MIN_VALUE);
ScrollableResults results = query.scroll(ScrollMode.FORWARD_ONLY);
// iterate over results
while (results.next()) {
    Object row = results.get();
    // process row then release reference
    // you may need to evict() as well
}
results.close();

这允许您流式传输结果集,但是 Hibernate 仍会将结果缓存在 Session 中,因此您需要经常调用 session.evict()session.clear()。如果您只是读取数据,则可以考虑使用StatelessSession,但您应该事先阅读其文档。”

【讨论】:

  • 你为什么要使用只读会话 Session#flush() ?你确定你不是指 Session#evict(row) 或 Session#clear() 这将有助于控制一级缓存大小。
  • (对于追随者,代码示例曾经提到flush,但现在提到evict或clear)
  • 我将它与 Postgres 一起使用,但我忽略了 setFetchSize。如果你把它留在里面,它就会出错。
【解决方案3】:

将查询中的提取大小设置为如下所示的最佳值。

另外,当不需要缓存时,最好使用StatelessSession。

ScrollableResults results = session.createQuery("SELECT person FROM Person person")
        .setReadOnly(true)
        .setFetchSize( 1000 ) // <<--- !!!!
        .setCacheable(false).scroll(ScrollMode.FORWARD_ONLY)

【讨论】:

  • 这是要走的路。如需更多参考,请参阅javaquirks.blogspot.dk/2007/12/mysql-streaming-result-set.html
  • 那么你们是说对于 MYSql 使用 Integer.MIN_VALUE 但对于 Oracle 或其他人应该将获取大小设置为合理的数字?
  • 此解决方案不依赖于数据库。同样适用于任何数据库。
【解决方案4】:

FetchSize 必须为Integer.MIN_VALUE,否则不起作用。

字面意思一定是从官方参考:https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html

【讨论】:

    【解决方案5】:

    实际上,如果您使用此处提到的答案,您可以得到您想要的——使用 MySQL 的低内存可滚动结果:

    Streaming large result sets with MySQL

    请注意,Hibernate 延迟加载会出现问题,因为它会在滚动完成之前执行的任何查询引发异常。

    【讨论】:

      【解决方案6】:

      拥有 9000 万条记录,听起来您应该对 SELECT 进行批处理。在将初始加载到分布式缓存中时,我已经使用 Oracle 完成了。查看 MySQL 文档,等效项似乎是使用 LIMIT 子句:http://dev.mysql.com/doc/refman/5.0/en/select.html

      这是一个例子:

      SELECT * from Person
      LIMIT 200, 100
      

      这将返回 Person 表的第 201 到 300 行。

      您需要先从表中获取记录数,然后将其除以批量大小,然后从那里计算出循环和 LIMIT 参数。

      这样做的另一个好处是并行性 - 您可以在此并行执行多个线程以加快处理速度。

      处理 9000 万条记录听起来也不是使用 Hibernate 的最佳选择。

      【讨论】:

      • 这也不起作用...尝试在偏移量为数百万的情况下进行选择(批处理或其他方式),这将需要很长时间。我愿意绕过 Hibernate,在没有 Hibernate 的情况下这样做有什么建议吗?
      • 试试这篇文章以获得 LIMIT 性能的解决方案:facebook.com/note.php?note_id=206034210932
      【解决方案7】:

      问题可能是,Hibernate 会保留对会话中所有对象的引用,直到您关闭会话。这与查询缓存无关。在将对象写入文件之后,从会话中 evict() 对象可能会有所帮助。如果它们不再被会话引用,垃圾收集器可以释放内存,您就不会再耗尽内存。

      【讨论】:

      • 问题是在检索到所有行之前,hibernate 甚至不会从查询中返回,所以我什至无法 evict() 任何东西,直到它全部加载为止。
      • 对不起,我错过了这个问题。如果这确实是 MySQL 驱动程序的问题,那么可能没有其他选择,然后将查询自己拆分为多个查询,因为我已经发布了。我将 ScrollableResults 与用于 MSSQL 的 jTDS 驱动程序一起使用,这有助于在处理来自数据库的大型数据集时防止 OutOfMemoryErrors,因此这个想法本身可能没有错。
      【解决方案8】:

      我建议的不仅仅是sample code,而是基于Hibernate 的查询模板来为您解决此问题(paginationscrollingclearing Hibernate 会话)。

      它也可以很容易地适应使用EntityManager

      【讨论】:

        【解决方案9】:

        我之前成功使用过 Hibernate 滚动功能,但没有读取整个结果集。有人说 MySQL 不做真正的滚动游标,但它声称基于 JDBC dmd.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE) 和搜索它似乎其他人已经使用它。确保它没有缓存会话中的 Person 对象——我在没有实体缓存的 SQL 查询中使用了它。您可以在循环结束时调用 evict 来确定或使用 sql 查询进行测试。还可以使用 setFetchSize 来优化访问服务器的次数。

        【讨论】:

          【解决方案10】:

          最近我解决了这样一个问题,我写了一篇关于如何面对这个问题的博客。非常喜欢,希望对大家有帮助。 我使用带有部分获取的惰性列表方法。我将查询的限制和偏移量或分页替换为手动分页。 在我的示例中,选择返回 1000 万条记录,我获取它们并将它们插入到“时态表”中:

          create or replace function load_records ()
          returns VOID as $$
          BEGIN
          drop sequence if exists temp_seq;
          create temp sequence temp_seq;
          insert into tmp_table
          SELECT linea.*
          FROM
          (
          select nextval('temp_seq') as ROWNUM,* from table1 t1
           join table2 t2 on (t2.fieldpk = t1.fieldpk)
           join table3 t3 on (t3.fieldpk = t2.fieldpk)
          ) linea;
          END;
          $$ language plpgsql;
          

          之后,我可以不计算每一行而是使用分配的序列进行分页:

          select * from tmp_table where counterrow >= 9000000 and counterrow <= 9025000
          

          从 java 的角度来看,我通过使用惰性列表的部分获取来实现此分页。这是一个从抽象列表扩展并实现 get() 方法的列表。 get方法可以使用数据访问接口继续获取下一组数据并释放内存堆:

          @Override
          public E get(int index) {
            if (bufferParcial.size() <= (index - lastIndexRoulette))
            {
              lastIndexRoulette = index;
              bufferParcial.removeAll(bufferParcial);
              bufferParcial = new ArrayList<E>();
                  bufferParcial.addAll(daoInterface.getBufferParcial());
              if (bufferParcial.isEmpty())
              {
                  return null;
              }
          
            }
            return bufferParcial.get(index - lastIndexRoulette);<br>
          }
          

          另一方面,数据访问接口使用查询进行分页,并实现一种方法进行逐步迭代,每25000条记录完成。

          可以在此处查看此方法的结果 http://www.arquitecturaysoftware.co/2013/10/laboratorio-1-iterar-millones-de.html

          【讨论】:

          • 请注意 link-only answers 是不鼓励的,所以答案应该是寻找解决方案的终点(而不是另一个参考中途停留,随着时间的推移往往会变得陈旧)。请考虑在此处添加独立的概要,并保留链接作为参考。
          【解决方案11】:

          如果您“内存不足”,另一种选择是只请求一列而不是整个对象 How to use hibernate criteria to return only one element of an object instead the entire object?(节省大量 CPU 进程启动时间)。

          【讨论】:

            【解决方案12】:

            对我来说,设置 useCursors=true 时它可以正常工作,否则 Scrollable Resultset 会忽略所有获取大小的实现,在我的情况下它是 5000,但 Scrollable Resultset 一次获取了数百万条记录,导致内存使用过多。底层数据库是 MSSQLServer。

            jdbc:jtds:sqlserver://localhost:1433/ACS;TDS=8.0;useCursors=true

            【讨论】:

            • 知道它对你有用很有用,但是最初的问题是关于 MySQL,所以你可能会建议检查 mysql 驱动程序是否有 useCursors 选项,或者询问用户是否尝试过
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2016-04-12
            • 1970-01-01
            • 2011-03-10
            • 1970-01-01
            • 2021-11-07
            • 1970-01-01
            • 2020-05-31
            相关资源
            最近更新 更多