【问题标题】:how do I load 100 million rows in to memory如何将 1 亿行加载到内存中
【发布时间】:2013-01-26 10:03:34
【问题描述】:

我需要将 1 亿多行从 MySQL 数据库加载到内存中。我的 java 程序因java.lang.OutOfMemoryError: Java heap space 而失败 我的机器中有 8GB RAM,并且在我的 JVM 选项中提供了 -Xmx6144m。

这是我的代码

public List<Record> loadTrainingDataSet() {

    ArrayList<Record> records = new ArrayList<Record>();
    try {
        Statement s = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY);
        s.executeQuery("SELECT movie_id,customer_id,rating FROM ratings");
        ResultSet rs = s.getResultSet();
        int count = 0;
        while (rs.next()) {

知道如何解决这个问题吗?


更新

我遇到了this post,并且根据下面的 cmets 我更新了我的代码。看来我能够以相同的 -Xmx6144m 量将数据加载到内存中,但这需要很长时间。

这是我的代码。

...
import org.apache.mahout.math.SparseMatrix;
...

@Override
public SparseMatrix loadTrainingDataSet() {
    long t1 = System.currentTimeMillis();
    SparseMatrix ratings = new SparseMatrix(NUM_ROWS,NUM_COLS);
    int REC_START = 0;
    int REC_END = 0;

    try {
        for (int i = 1; i <= 101; i++) {
            long t11 = System.currentTimeMillis();
            REC_END = 1000000 * i;
            Statement s = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY,
                    java.sql.ResultSet.CONCUR_READ_ONLY);
            s.setFetchSize(Integer.MIN_VALUE);
            ResultSet rs = s.executeQuery("SELECT movie_id,customer_id,rating FROM ratings LIMIT " + REC_START + "," + REC_END);//100480507
            while (rs.next()) {
                int movieId = rs.getInt("movie_id");
                int customerId = rs.getInt("customer_id");
                byte rating = (byte) rs.getInt("rating");
                ratings.set(customerId,movieId,rating);
            }
            long t22 = System.currentTimeMillis();
            System.out.println("Round " + i + " completed " + (t22 - t11) / 1000 + " seconds");
            rs.close();
            s.close();
        }

    } catch (Exception e) {
        System.err.println("Cannot connect to database server " + e);
    } finally {
        if (conn != null) {
            try {
                conn.close();
                System.out.println("Database connection terminated");
            } catch (Exception e) { /* ignore close errors */ }
        }
    }
    long t2 = System.currentTimeMillis();
    System.out.println(" Took " + (t2 - t1) / 1000 + " seconds");
    return ratings;
}

加载前 100,000 行需要 2 秒。加载第 29 个 100,000 行需要 46 秒。我在中间停止了这个过程,因为它花费了太多时间。这些时间是可以接受的吗?有没有办法提高这段代码的性能? 我在 8GB RAM 64bit windows 机器上运行它。

【问题讨论】:

  • 为什么你可能同时需要它们?在数据库端实现某种分页,并根据需要将它们分成块。
  • 不确定你的目的是什么,但你也可以使用lucene.apache.org/solr来加载它们,你可以在上面写jasper等,它比直接更快更高效与 DB 互动。
  • 考虑在小组中处理/显示数据,也就是分页。
  • 请说明需要。你为什么要随意走?
  • 在处理非常大的数据集时,ArrayList 是一个糟糕的选择。使用其默认构造函数初始化的数组列表 - 无限糟糕。但即使你切换到一个直接的数组,你也会遇到一次在主内存中拟合所有数据的问题。

标签: java sql jdbc out-of-memory


【解决方案1】:

一亿条记录意味着每条记录最多可能占用 50 个字节以适应 6 GB + 一些额外空间用于其他分配。在 Java 中 50 个字节算不了什么;仅仅Object[] 每个元素占用 32 个字节。您必须找到一种方法来立即在 while (rs.next()) 循环中使用结果,而不是完整保留它们。

【讨论】:

  • @Bulat 如何在没有 ResultSet 的情况下获得结果?
  • @Bulat 是的,和s.getResultSet()一模一样
  • @AlanB 如果您在executeQuery 获得OOME,这表明要么是糟糕的JDBC 驱动程序,要么是糟糕的数据库:它没有在结果集上提供游标,而是急切地尝试将整个结果集加载到内存中。你用的是什么数据库?
  • MySQL JDBC 驱动程序默认将所有行加载到 ResultSet 中。您需要指定 setFetchSize(Integer.MIN_VALUE) 才能实际获取它以逐行获取。
  • @AlanB MySQL 会忽略除Integer.MIN_VALUE 之外的setFetchSize 的所有值,请参阅implementation notes,在ResultSet
【解决方案2】:

您可以调用stmt.setFetchSize(50);conn.setAutoCommitMode(false); 来避免将整个ResultSet 读入内存。

文档是这样说的:

根据光标获取结果

默认情况下,驱动程序会一次收集所有查询结果。这对于大型数据集可能很不方便,因此 JDBC 驱动程序提供了一种将 ResultSet 基于数据库游标并仅获取少量行的方法。

少量行缓存在连接的客户端,当用尽时,通过重新定位游标来检索下一个行块。

注意:

  • 不能在所有情况下都使用基于游标的结果集。有一个 使驾驶员无声后退的限制数量 一次获取整个 ResultSet。
  • 与服务器的连接必须使用 V3 协议。这是 服务器版本 7.4 的默认值(并且仅受其支持)和 稍后。-
  • 连接不得处于自动提交模式。后台关闭 交易结束时的游标,因此在自动提交模式下后端 将关闭游标,然后才能从中获取任何内容。-
  • 必须使用 ResultSet 类型创建语句 ResultSet.TYPE_FORWARD_ONLY。这是默认设置,所以没有代码 需要重写以利用这一点,但这也意味着 您不能向后滚动或以其他方式在 结果集。-
  • 给定的查询必须是单个语句,而不是多个语句 用分号串在一起。

示例:设置 fetch size 以打开和关闭光标。

将代码更改为游标模式就像将语句的获取大小设置为适当的大小一样简单。将提取大小设置回 0 将导致所有行都被缓存(默认行为)。

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/test?useCursorFetch=true&user=root");
// make sure autocommit is off 
conn.setAutoCommit(false); 
Statement st = conn.createStatement();

// Turn use of the cursor on. 
st.setFetchSize(50);
ResultSet rs = st.executeQuery("SELECT * FROM mytable");
while (rs.next()) {
   System.out.print("a row was returned.");
} 
rs.close();

// Turn the cursor off. 
st.setFetchSize(0);
rs = st.executeQuery("SELECT * FROM mytable");
while (rs.next()) {
   System.out.print("many rows were returned.");
} 
rs.close();

// Close the statement. 
st.close();

【讨论】:

  • 另请注意,连接 URL 中需要 useCursorFetch=true 才能使此方法起作用。
  • @GordThompson 你是对的。我已经更新了我的答案。请检查。让我知道是否还有其他问题。谢谢
【解决方案3】:

问题是我在 s.executeQuery( 行 it self 中得到 java.lang.OutOfMemoryError

您可以将查询拆分为多个:

    s.executeQuery("SELECT movie_id,customer_id,rating FROM ratings LIMIT 0,300"); //shows the first 300 results
    //process this first result
    s.executeQuery("SELECT movie_id,customer_id,rating FROM ratings LIMIT 300,600");//shows 300 results starting from the 300th one
    //process this second result
    //etc

您可以做一段时间,当找不到更多结果时停止

【讨论】:

  • 我的表有1亿多行..恐怕不实用。
  • 不实用,但可以解决你的OutOfMemoryError
  • 你可以用st.setFetchSize(5000)
  • @Bulat 我用过。我也确保拥有setAutoCommit(false)。但结果还是一样。
【解决方案4】:

您将不得不重新设计并将数据分块加载到内存中。

例子

1) 使用适当的 SQL(sql 仅选择 100 万条)从 DB 加载前 100 万条记录并进行处理 2) 加载另一个类似的块。

单独使用 setFetchSize 并不能解决这个问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-10-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-12
    • 2015-07-04
    • 2015-12-24
    • 2011-09-07
    相关资源
    最近更新 更多