使用Hibernate的ScrollableResults慢慢读取9000万条记录

时间:2010-05-13 11:21:38

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

我只需要使用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堆空间异常:(。

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

我想处理这个问题的另一种方法是使用setFirstResult和setMaxResults迭代结果,只使用常规的Hibernate结果而不是ScrollableResults。这感觉就像效率低下一样,当我在第89行第89行调用setFirstResult时,会开始花费一段可笑的时间......

UPDATE:setFirstResult / setMaxResults不起作用,事实证明需要花费很长时间才能达到我担心的偏移量。这里一定有解决方案!这不是一个很标准的程序吗?我愿意放弃Hibernate并使用JDBC或其他任何东西。

更新2:我提出的解决方案可行,但不是很好,基本上是以下形式:

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

由于我还有其他条件,即使是索引中的所有条件,它仍然没有我想要的那么快......所以仍然可以提供其他建议..

12 个答案:

答案 0 :(得分:29)

使用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>

答案 1 :(得分:19)

你应该可以使用ScrollableResults,虽然它需要一些神奇的咒语才能使用MySQL。我在一篇博文(http://www.numerati.com/2012/06/26/reading-large-result-sets-with-hibernate-and-mysql/)中写了我的发现,但我在这里总结一下:

&#34; [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,但您应事先阅读其文档。&#34;

答案 2 :(得分:17)

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

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

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

答案 3 :(得分:7)

FetchSize必须为Integer.MIN_VALUE,否则无效。

必须从官方参考资料中提取:https://dev.mysql.com/doc/connector-j/5.1/en/connector-j-reference-implementation-notes.html

答案 4 :(得分:3)

实际上你可能已经得到了你想要的东西 - 使用MySQL的低内存可滚动结果 - 如果你使用了这里提到的答案:

Streaming large result sets with MySQL

请注意,您将遇到Hibernate延迟加载的问题,因为它会在滚动完成之前执行的任何查询上抛出异常。

答案 5 :(得分:1)

拥有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的最佳位置。

答案 6 :(得分:1)

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

答案 7 :(得分:1)

我建议的不只是sample code,而是基于Hibernate的查询模板为您执行此解决方法(paginationscrollingclearing Hibernate会话)。

它也可以很容易地适应EntityManager

答案 8 :(得分:0)

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

答案 9 :(得分:0)

最近我解决了这样的问题,我写了一篇关于如何面对这个问题的博客。非常喜欢,我希望对任何人都有帮助。 我使用懒惰列表方法与部分征收。我将查询的限制和偏移量或分页替换为手动分页。 在我的例子中,select返回了10百万条记录,我得到它们并将它们插入“时态表”中:

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的角度来看,我通过带有惰性列表的部分请求实现了这个分页。这是一个从Abstract列表扩展并实现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

答案 10 :(得分:0)

如果你“用完RAM”的另一个选择是请求说,一列而不是整个对象How to use hibernate criteria to return only one element of an object instead the entire object?(节省了大量的CPU处理时间来启动)。

答案 11 :(得分:0)

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

JDBC:JTDS:SQLSERVER://本地主机:1433 / ACS; TDS = 8.0; useCursors =真